Compare commits

...

119 Commits

Author SHA1 Message Date
Don Cote
837ff75f7e remove messaging 2021-09-16 21:52:22 -04:00
Anthony G
135f6f9a6b
Merge pull request #31 from FlipsideCrypto/dependabot/npm_and_yarn/elliptic-6.5.4
Bump elliptic from 6.4.1 to 6.5.4
2021-03-17 17:01:33 -04:00
dependabot[bot]
40a16998b2
Bump elliptic from 6.4.1 to 6.5.4
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.4.1 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.4.1...v6.5.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-08 23:25:42 +00:00
Jim Myers
23a7c4953a
Merge pull request #27 from FlipsideCrypto/v1.16.1
v1.16.1
2020-09-30 19:24:04 -04:00
Jim Myers
4844f92206 Default to 100% width of parent element for frame widget 2020-09-30 19:20:36 -04:00
Jim Myers
730f24a797 v1.16.1 2020-09-30 17:25:50 -04:00
Jim Myers
b2bbc6b955
Merge pull request #23 from FlipsideCrypto/v1.16.0
Add frame widget.
2020-08-14 22:09:25 -04:00
Jim Myers
56cd6f206a Use URLSearchParams function. 2020-08-14 22:05:26 -04:00
Jim Myers
cde8ae754c Add dark/light modes to frame. 2020-08-14 13:22:16 -04:00
Jim Myers
1326e490a7 Encode url params 2020-08-14 12:17:50 -04:00
Jim Myers
1d980b3ce6 Add frame widget. 2020-08-13 21:40:35 -04:00
Jim Myers
4318267c78 Add parameter to dynamic widget and remove . 2020-08-13 10:58:58 -04:00
Jim Myers
b038c1c427
Merge pull request #19 from FlipsideCrypto/v1.14.1
Version 1.14.1
2020-06-05 10:42:33 -04:00
Jim Myers
651b19c8e6 Default to autorefresh. 2020-06-05 10:36:46 -04:00
Jim Myers
866f129b9a use break not continue. 2020-06-05 10:27:46 -04:00
Jim Myers
99b05e6c39 Add autoRefreshToggle, fix bug where highlights do not fully appear in the spectrum. 2020-06-05 10:23:30 -04:00
Jim Myers
92208fad3e
Merge pull request #18 from FlipsideCrypto/hide-spectrum-header
Add showHeader option to spectrum widget
2020-05-15 07:58:38 -04:00
Jim Myers
95e2d7c262 Update default flipside link to api.flipsidecrypto.com 2020-05-15 07:57:35 -04:00
Bryan Cinman
3e5494daf7 Add showHeader option to spectrum widget 2020-05-13 14:03:41 -04:00
Jim Myers
6d885476dc
Merge pull request #17 from FlipsideCrypto/v1.13.2
Update link rendering for letter grade.
2020-04-16 13:20:12 -04:00
Jim Myers
c8d2400ffd Enable link bootstrap. 2020-04-16 13:16:47 -04:00
Jim Myers
f9e4d0a4f6 Update link rendering for letter grade. 2020-04-16 13:14:05 -04:00
Jim Myers
2b48937ac7 Merge branch 'master' of github.com:FlipsideCrypto/flipside-js 2020-04-14 21:43:27 -04:00
Jim Myers
e17d873d08 Bump version to 1.13.1 2020-04-14 21:42:41 -04:00
Jim Myers
c8f7055567
Merge pull request #16 from FlipsideCrypto/dynamic-dark
Add dark mode to dynamic widget
2020-04-14 17:18:47 -04:00
Jim Myers
3945d911c5 Update version to 1.13.0 2020-04-14 17:17:30 -04:00
Bryan Cinman
f283cc84a3 Add dark mode to dynamic widget 2020-04-14 13:42:44 -04:00
Jim Myers
2d51db81ec Update asset interface of dynamic widget. 2020-03-04 12:05:47 -05:00
Bryan Cinman
124d14c2a1 Use js_url properly, update. platform-api -> api 2020-02-27 14:03:03 -05:00
Bryan Cinman
c0268f4f40 Add dynamic widget 2020-02-26 13:29:21 -05:00
Jim Myers
2fb5d3da4e Reduce retries on failure. 2020-02-17 20:02:32 -05:00
Bryan Cinman
1fdb9926e0
Merge pull request #6 from FlipsideCrypto/bugfix/remove-dynamic-loading
remove dynamic highcharts loading
2019-05-01 15:09:55 -04:00
Bryan Cinman
210dc70234 remove dynamic highcharts loading 2019-05-01 13:00:08 -04:00
Jim Myers
a038c9bb4f Fix 0 point change issue in trend component. 2019-04-19 09:17:08 -04:00
Jim Myers
1460964b33 Utilize precomputed point change and grade. 2019-04-18 08:39:27 -04:00
Jim Myers
6f05f7fe4b Small issue fix. 2019-03-19 14:57:47 -04:00
Jim Myers
6d7b4d2c88 Update requests. 2019-03-15 14:40:53 -04:00
Jim Myers
eab5a73255 Merge branch 'bugfix/map-multitable-columns' into asset-ids 2019-03-13 23:22:15 -04:00
Bryan Cinman
3378e5625c add asset_ids to chart, add nodata messaging 2019-03-13 22:56:52 -04:00
Bryan Cinman
744681e3f8 add config column mapping 2019-03-13 16:27:17 -04:00
Bryan Cinman
a6d7a2d2a6 fix publicPath 2019-03-13 15:19:08 -04:00
Jim Myers
942fe5a7f4 Fix dual highcharts import issue. 2019-03-13 15:18:16 -04:00
Bryan Cinman
953107687e add ability for spectrum and chart to use asset ids 2019-03-13 04:27:31 -04:00
Jim Myers
5010dfd2ef Update handling of divider color. 2019-03-12 11:25:56 -04:00
Jim Myers
e3ee0fd590 Fix example. 2019-03-12 11:14:47 -04:00
Jim Myers
f4e0da2d8d Add ability to customize row and header style attribute on multi-table widget. 2019-03-12 11:13:19 -04:00
Jim Myers
b8abd9203f Update price table. 2019-03-12 10:48:16 -04:00
Jim Myers
3c5d940da6 Fixes for broken side-effects :( 2019-03-12 10:42:02 -04:00
Jim Myers
e6c9840ecb CMC updates 2019-03-12 10:09:00 -04:00
Bryan Cinman
33e93fcb3d remove dividers in test page multiTable 2019-03-11 17:49:53 -04:00
Bryan Cinman
95c1c69912 fix coin column color, set maximum decimal digits 2019-03-11 17:49:12 -04:00
Bryan Cinman
e48a219547 fix customlinks styling, add commas to currencies, add $ prefix 2019-03-11 17:12:09 -04:00
Bryan Cinman
24543e4231 added volume, price, and market cap data and api polling to multi-table 2019-03-08 16:39:25 -05:00
Jim Myers
a0c8c1a503 Update readme.md. 2019-03-04 21:00:20 -05:00
Jim Myers
518a20289d Add public path. 2019-03-04 20:42:39 -05:00
Jim Myers
194a1822be Comment out bundle analyzer (hangs during build). 2019-03-04 08:18:04 -05:00
Bryan Cinman
9413808bdf add code splitting 2019-03-03 18:00:43 -05:00
Jim Myers
5ae203dde5 Merge branch 'master' into charts 2019-03-01 14:55:20 -05:00
Jim Myers
3c8fd0f326 Configure. 2019-03-01 14:52:23 -05:00
Jim Myers
68796ce9bb Update links. 2019-03-01 12:38:02 -05:00
Bryan Cinman
0411a6742d fix svg text sizing on chart 2019-03-01 02:04:45 -05:00
Jim Myers
cbb8040d4c Update default link. 2019-02-28 23:59:06 -05:00
Jim Myers
7f979af89f Add new type of tracking link. 2019-02-28 18:17:58 -05:00
Jim Myers
d611b38729 Add emodule interop. 2019-02-28 18:12:59 -05:00
Jim Myers
b67904ad78 Merge branch 'master' into charts 2019-02-28 11:49:31 -05:00
Jim Myers
e0f0bfc748 Update link styles. 2019-02-27 08:40:52 -05:00
Jim Myers
b939b5d704 Add link to grade. 2019-02-27 08:15:32 -05:00
Jim Myers
f4d11d76d3 Merge branch 'master' into charts 2019-02-22 21:45:52 -05:00
Bryan Cinman
f60139c50c fcas -> health 2019-02-22 19:09:44 -05:00
Bryan Cinman
c949b0bda5 force spectrum font sizing 2019-02-22 19:00:07 -05:00
Jim Myers
88d736fda3
Merge pull request #1 from FlipsideCrypto/spectrum-updated
Spectrum updates
2019-02-21 09:56:52 -05:00
Bryan Cinman
e3b3e5985e add polyfills for ie11 and convert remaining js files to ts 2019-02-21 03:24:42 -05:00
Bryan Cinman
8120e14533 cleanup spectrum, port to typescript and css modules
move spectrumPlot to spectrum
2019-02-20 04:04:04 -05:00
Bryan Cinman
15291d473c allow highcharts config in chart widget 2019-02-18 16:14:29 -05:00
Bryan Cinman
2b3d98520e export chart, range selector, naming series, customLinks on chart 2019-02-18 03:41:37 -05:00
Bryan Cinman
8dd894dbec first pass at a linechart widget 2019-02-16 05:53:29 -05:00
Bryan Cinman
6c351d1c28 allow lodash function import with es6 syntax, start chart widget 2019-02-14 04:28:47 -05:00
Jim Myers
256a9fe375 Update examples. 2019-02-11 08:51:01 -05:00
Bryan Cinman
d5b5b1fbdc added customization options to customeWidgets, added customWidgets to the score widget 2019-02-11 02:19:59 -05:00
Bryan Cinman
5af454cafa Merge branch 'master' into score 2019-02-11 01:51:02 -05:00
Jim Myers
429fa530aa Add bootstrapping options to spectrum widget. 2019-02-07 11:42:52 -05:00
Jim Myers
22fdaa0bb2 Merge branch 'master' of github.com:FlipsideCrypto/flipside-js 2019-02-06 23:24:56 -05:00
Jim Myers
90131a8b83 FCAS to project health. 2019-02-06 23:24:31 -05:00
Bryan Cinman
102e230380 added css modules, cleanup, finishe score widget 2019-02-05 18:07:39 -05:00
Bryan Cinman
7b43d6654d added withFcas HOC, started Score component 2019-02-05 05:33:29 -05:00
Bryan Cinman
cf0a48879b fix logo casing 2019-02-03 22:54:24 -05:00
Bryan Cinman
6d48df3f62 Merge branch 'master' into config_updates 2019-02-03 22:15:12 -05:00
Bryan Cinman
8144c9fdb3 fix font-size in custom links 2019-02-03 22:14:58 -05:00
Jim Myers
731aafae7b Upgrade to version 1.9.0 2019-02-01 11:52:38 -05:00
Jim Myers
9a858c1a7f Add custom links to table widget. 2019-02-01 11:37:35 -05:00
Bryan Cinman
6795823cd8 use named widget links for rendering 2019-01-31 17:39:49 -05:00
Bryan Cinman
7d6db5c8e1 Merge branch 'config_updates' of github.com:FlipsideCrypto/flipside-js into config_updates 2019-01-31 15:33:58 -05:00
Bryan Cinman
6c41d15d66 fixed alignment of custom links, fixed defaults for array props 2019-01-31 15:33:47 -05:00
Jim Myers
8db0a87d79 Update index. 2019-01-31 09:20:17 -05:00
Bryan Cinman
3c895b6e51 fix conflicts 2019-01-31 05:53:19 -05:00
Bryan Cinman
ea8acb6e1e update config fields for rows and others. added default props 2019-01-31 05:52:38 -05:00
Jim Myers
cf5f308ef1 Update index.html 2019-01-30 20:10:45 -05:00
Bryan Cinman
3f553a851d carousel spectrum 2019-01-30 04:24:46 -05:00
Jim Myers
5d84f1d2d4 Upgrade to 1.8 2019-01-29 11:27:57 -05:00
Jim Myers
8281686462 Update index. 2019-01-29 11:25:59 -05:00
Jim Myers
eac27d5d4f Rename to spectrum. 2019-01-29 11:23:00 -05:00
Jim Myers
ac037b5939 Add 1.7 to index 2019-01-29 11:18:13 -05:00
Jim Myers
eaf4f49b1c Upgrade to 1.7 and add carousel config feature. 2019-01-29 11:17:36 -05:00
Jim Myers
8632519e8d Spectrum updates. 2019-01-29 10:56:45 -05:00
Jim Myers
c7aaf54f3e Update config. 2019-01-29 10:36:34 -05:00
Jim Myers
56987cf57f Merge branch 'table' 2019-01-29 08:05:26 -05:00
Bryan Cinman
6930f317be update types 2019-01-29 02:46:03 -05:00
Bryan Cinman
f0b2c4b223 spectrum -> spectrumPlot, fixed custom links wrapping 2019-01-29 02:34:26 -05:00
Jim Myers
4964a05242 Add updated links to js fiddles. 2019-01-29 01:13:19 -05:00
Jim Myers
ad4e464ced Upgrade to 1.6.0 2019-01-28 23:36:40 -05:00
Bryan Cinman
dc6f9a687a add market maturity 2019-01-28 23:22:09 -05:00
Bryan Cinman
7d422574f5 fix sorting based on 7d trend to use fcas_change 2019-01-28 23:11:50 -05:00
Bryan Cinman
95fe9498b2 use fcas_change for 7d column 2019-01-28 22:35:23 -05:00
Bryan Cinman
7dff56444c Merge branch 'table' of github.com:FlipsideCrypto/flipside-js into table 2019-01-28 22:28:39 -05:00
Bryan Cinman
ed5fef708b styling on multitable, split rank and trend components, local sorting 2019-01-28 22:28:33 -05:00
Jim Myers
3e6f2f3ea2 Update index.html 2019-01-28 17:23:52 -05:00
Bryan Cinman
112a078ec4 new config for spectrum widget 2019-01-28 15:40:52 -05:00
Bryan Cinman
63b6602f49 added typescript and started multitable widget 2019-01-28 03:32:45 -05:00
Jim Myers
7ec67a14d5 Remove console.log 2019-01-22 16:23:21 -05:00
76 changed files with 52174 additions and 3469 deletions

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
.vscode/
.DS_STORE
node_modules/
dist/
dist/
.log

View File

@ -8,12 +8,6 @@ To get started, request an API Key: api@flipsidecrypto.com.
[View Wiki](https://github.com/FlipsideCrypto/flipside-js/wiki)
## Live Examples
[View Live FCAS Widget Example](https://jsfiddle.net/flipsidejim/f7zpd0uj/29/)
<br>
[View Live Table Widget Example](https://jsfiddle.net/flipsidejim/vsh5dq9y/11/)
## Building
```

View File

@ -5,18 +5,35 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>flipside.js</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="flipside-v1.5.4.js"></script>
<script src="flipside-v1.16.1.js"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol";
padding: 0;
margin: 0;
background-color: #fff !important;
}
text {
font-weight: 700;
font-size: 24px;
}
.wrapper {
width: 1080px;
height: 800px;
font-weight: 700;
font-size: 20px;
}
.frameWidget {
width: 100%;
flex-wrap: wrap;
border: 1px solid #ccc;
padding: 20px;
width: 500px;
height: 100%;
}
.wrapper p {
font-size: 40px;
}
#widget-1 {
@ -29,36 +46,224 @@
</style>
</head>
<body>
<div class="wrapper"><div id="widget-0" style="width: 289px;"></div></div>
<div class="wrapper"><div id="widget-1" style="width: 289px;"></div></div>
<div class="wrapper"><div id="widget-2"></div></div>
<div class="wrapper">
<h3>Frame</h3>
<div id="frame-widget" class="frameWidget"></div>
</div>
<div class="wrapper">
<h3>Dynamic</h3>
<div id="widget"></div>
</div>
<div class="wrapper">
<h2>Chart</h2>
<div id="chart"></div>
</div>
<div class="wrapper dark" style="background-color: #000000">
<div id="chart2"></div>
</div>
<div class="wrapper"><div id="score"></div></div>
<div class="wrapper"><div id="widget-0" style="width: 289px"></div></div>
<div class="wrapper" style="background-color: #000000">
<div id="widget-1" style="width: 289px"></div>
</div>
<div class="wrapper" style="background-color: #33435e">
<div id="widget-2"></div>
</div>
<div class="wrapper"><div id="multiTable"></div></div>
<div class="wrapper">
<div id="multiTableFcas"></div>
</div>
<script>
const flipside = new Flipside("<Your-API-Key");
flipside.createFCAS("widget-0", "nmr", {
highlights: ["BTC", "ETH", "QTUM"]
const flipside = new Flipside("a7936778-4f87-4790-889f-3ab617271458");
flipside.frame("frame-widget", {
url:
"https://velocity.flipsidecrypto.com/public/question/3964591d-a657-4432-b3e8-8675bfbfb714",
});
flipside.createFCAS("widget-1", "zrx", {
logo: false,
mini: true,
dark: true,
flipside.dynamic("widget", {
widgetId: "578375af-3467-4742-9863-5ca6f2fa30d1",
darkMode: false,
data: {
asset_id: 1,
},
});
flipside.dynamic("widget", {
widgetId: "26dd8193-b061-4ef5-a49d-b7bf3c282853",
darkMode: false,
data: {
asset_id: 1,
metric: "fcas",
symbol: "BTC",
title: "BTC FCAS",
},
});
flipside.chart("chart", {
title: "Flipside 25 - Overall Industry Health",
chart: {
type: "area",
},
plotOptions: {
area: {
threshold: null,
fillColor: {
linearGradient: {
x1: 0,
y1: 0,
x2: 0,
y2: 1,
},
stops: [
[0, "#9EC9FE"],
[1, "rgba(255, 255, 255, 0)"],
],
},
},
},
series: [
{
name: "FLIP25",
symbol: "FLIP25",
metric: "fcas",
type: "line",
yAxis: 1,
},
],
colors: ["#9EC9FE"],
rangeSelector: {
enabled: true,
},
});
flipside.chart("chart2", {
mode: "light",
title: "Ethereum Health",
axisTitles: ["FCAS", "Dev"],
colors: ["#E44C27", "#7FCCC5"],
series: [
{ asset_id: 2, metric: "fcas", type: "line", yAxis: 0 },
{ asset_id: 2, metric: "dev", type: "line", yAxis: 1 },
],
});
flipside.spectrum("widget-0", {
asset: {
asset_id: 1,
highlights: ["BTC", "ETH", "QTUM"],
},
mode: "light",
fontFamily: "inherit",
relatedMarkers: {
enabled: true,
bucketDistance: 35,
lineDistance: 25,
fontFamily: "inherit",
},
showHeader: false,
name: { enabled: true, style: {} },
spectrum: { enabled: true },
icon: { enabled: true },
rank: { enabled: true },
trend: { enabled: true },
});
flipside.spectrum("widget-1", {
assets: [
{
asset_id: 1,
highlights: ["BTC", "NMR", "QTUM"],
},
{
asset_id: 2,
highlights: ["BTC", "NMR", "QTUM"],
},
],
mode: "dark",
bucketDistance: 100,
highlights: [
"BTC",
"ETH",
"eos",
"qtum",
"nmr",
"ltc",
"gnt",
"rep",
"mln"
]
showHeader: false,
relatedMarkers: {
bucketDistance: 55,
lineDistance: 35,
},
});
flipside.createTable("widget-2", "btc", {
dark: true,
borderColor: "#737e8d"
borderColor: "#737e8d",
});
// Price table
flipside.multiTable("multiTable", {
widgetType: "price-multi-table",
columns: ["market_cap", "price", "volume_24h"],
sortBy: "market_cap",
showFullName: true,
fontFamily: "inherit",
limit: 50,
mode: "light",
headers: {
style: {
fontWeight: "bold",
padding: "10px 0 10px 0",
},
},
rows: {
dividers: true,
dividersColor: "#eaeaea",
alternating: false,
style: {
padding: "10px 0 10px 0",
},
},
title: {
text: "Top 50 Coins By Market Cap",
style: { fontSize: "24px", fontWeight: 400 },
},
});
// FCAS
flipside.multiTable("multiTableFcas", {
autoWidth: false,
assets: null,
columns: [
"fcas",
"trend",
"userActivity",
"developerBehavior",
"marketMaturity",
"rank",
],
exlusions: null,
fontFamily: "inherit",
headers: {},
limit: 10,
mode: "light",
rows: {
alternating: true,
alternatingColors: "#eeeeee",
dividers: false,
},
title: {
text: "Top Coins",
style: {},
},
trend: {
enabled: true,
changeOver: 14,
style: {},
},
});
flipside.score("score", { symbol: "eth" });
</script>
</body>
</html>

4333
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,32 +1,44 @@
{
"name": "flipside-js",
"version": "1.5.4",
"version": "1.16.1",
"description": "FlipsideJS provides a library embeddable widgets that display data from the Flipside Platform API, including FCAS.",
"main": "index.js",
"scripts": {
"dev": "webpack-dev-server",
"start": "NODE_ENV=development webpack-dev-server",
"build": "webpack -p --env production",
"build:stats": "webpack --env production --json > stats.json"
"build:stats": "webpack -p --env production --json > stats.json"
},
"author": "Flipsidecrypto.com <hello@flipsidecrypto.com>",
"license": "MIT",
"dependencies": {
"axios": "^0.18.0",
"classnames": "^2.2.6",
"highcharts": "^7.0.3",
"load-js": "^3.0.3",
"lodash": "^4.17.11",
"preact": "^8.3.1"
},
"devDependencies": {
"@babel/core": "^7.1.6",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-react-jsx": "^7.1.6",
"@types/classnames": "^2.2.7",
"@types/highcharts": "^5.0.36",
"@types/lodash": "^4.14.121",
"@types/node": "^10.12.19",
"babel-loader": "^8.0.4",
"core-js": "^2.6.5",
"css-loader": "^1.0.1",
"node-sass": "^4.10.0",
"css-modules-typescript-loader": "^1.1.1",
"node-sass": "^4.13.1",
"sass-loader": "^7.1.0",
"style-loader": "^0.23.1",
"ts-loader": "^5.3.3",
"typescript": "^3.2.4",
"url-loader": "^1.1.2",
"webpack": "^4.26.0",
"webpack-bundle-analyzer": "^3.0.3",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.10"
"webpack": "^4.41.6",
"webpack-bundle-analyzer": "^3.6.0",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3"
}
}

View File

@ -1,47 +0,0 @@
import axios from "axios";
export default class API {
constructor(apiKey) {
this.key = apiKey;
this.client = axios.create({
baseURL: "https://platform-api.flipsidecrypto.com/api/v1",
params: { api_key: apiKey }
});
}
async _fetch(url, params = {}, retryCount = 0, retryMax = 15) {
let res;
try {
res = await this.client.get(url, { params: params });
if (res.status >= 200 && res.status < 300) {
return { data: res.data, success: true };
}
} catch (e) {
console.log(
`Failed to fetch data from: "${url}". \nError message: "${e}"`
);
}
if (retryCount < retryMax) {
return await this._fetch(url, params, retryCount + 1);
}
return { data: null, success: false };
}
async fetchAssetMetric(symbol, metric, days = 7) {
const sym = `${symbol}`.toUpperCase();
return await this._fetch(`/assets/${sym}/metrics/${metric}`, {
change_over: days
});
}
async fetchAssetMetrics(symbol) {
const sym = `${symbol}`.toUpperCase();
return await this._fetch(`/assets/${sym}/metrics`);
}
async fetchFCASDistribution(asset) {
return await this._fetch(`/metrics/FCAS/assets`, {
visual_distribution: true
});
}
}

113
src/api.ts Normal file
View File

@ -0,0 +1,113 @@
import axios, { AxiosInstance, AxiosPromise } from "axios";
export default class API {
key: string;
client: AxiosInstance;
constructor(apiKey: string) {
this.key = apiKey;
this.client = axios.create({
baseURL: "https://api.flipsidecrypto.com/api/v1",
params: { api_key: apiKey },
});
}
async _fetch(
method: string,
url: string,
params = {},
retryCount = 0,
retryMax = 1
): Promise<any> {
let res;
try {
res = await this.client.request({
url,
method,
params: params,
});
if (res.status >= 200 && res.status < 300) {
return { data: res.data, success: true };
}
} catch (e) {
console.log(
`Failed to fetch data from: "${url}". \nError message: "${e}"`
);
}
if (retryCount < retryMax) {
return await this._fetch("GET", url, params, retryCount + 1);
}
return { data: null, success: false };
}
async fetchAssetMetric(id: string, metric: string, days = 7) {
const sym = `${id}`.toUpperCase();
return await this._fetch("GET", `/assets/${sym}/metrics/${metric}`, {
change_over: days,
});
}
async fetchAssetMetrics(id: string) {
const sym = `${id}`.toUpperCase();
return await this._fetch("GET", `/assets/${sym}/metrics`);
}
async fetchFCASDistribution(fullDistribution: boolean = false) {
return await this._fetch("GET", `/metrics/FCAS/assets`, {
visual_distribution: !fullDistribution,
});
}
async fetchDynamic(id: string) {
return this._fetch("GET", `/widgets/dynamic/${id}`);
}
async fetchMetrics(payload: {
assets?: string[];
exclusions?: string[];
sort_by?: string;
sort_desc?: boolean;
page?: number;
size?: number;
metrics?: string[];
change_over?: number;
}) {
return await this.client.post(`/assets/metrics`, payload);
}
async fetchWidgetLinks(slug: WidgetLinksSlug): Promise<WidgetLinksResponse> {
return await this.client.get(`/widgets/${slug}/links`);
}
async fetchTimeseries(payload: APISeriesPayload) {
return await this.client.post("/timeseries", payload);
}
}
export type APISeries = {
symbol?: string;
asset_id?: number;
names: string[];
};
export type APISeriesPayload = {
start_date: string;
end_date: string;
series: APISeries[];
};
export type WidgetLinksSlug =
| "spectrum"
| "multi-table"
| "table"
| "score"
| "chart"
| "price-multi-table";
export type WidgetLinksLink = {
widget_id: string;
name: string;
link_html: string;
};
export type WidgetLinksResponse = {
data: WidgetLinksLink[];
};

84
src/chart/defaults.ts Normal file
View File

@ -0,0 +1,84 @@
export const DEFAULT_HIGHCHARTS: Highcharts.Options = {
chart: {
backgroundColor: "transparent",
style: {
fontFamily: "inherit"
}
},
title: {
text: undefined,
align: "left",
style: {
fontSize: "24px"
}
},
colors: ["#F5A623", "#2D57ED", "#48A748", "#006093", "#5A2788"],
tooltip: {
split: true,
borderWidth: 0,
shadow: false
},
credits: {
enabled: false
},
xAxis: {
type: "datetime",
crosshair: {
color: "#9B9B9B",
dashStyle: "Dash"
},
labels: { style: { color: "#9B9B9B" } },
lineColor: "#6c6c6c",
tickColor: "#6c6c6c"
},
navigator: {
enabled: false
},
scrollbar: {
enabled: false
},
rangeSelector: {
enabled: true,
allButtonsEnabled: false,
buttonTheme: {
fill: "none",
stroke: "none",
style: {
color: "#9B9B9B"
},
states: {
select: {
fill: "none",
style: {
color: "#000",
fontWeight: "normal",
textDecoration: "underline"
}
},
disabled: {
style: {
color: "#9B9B9B",
opacity: 0.5
}
}
} as any
},
inputEnabled: false
}
};
export const DEFAULT_YAXIS: Highcharts.YAxisOptions = {
title: { text: undefined },
gridLineColor: "#6c6c6c",
labels: {
style: { color: "#9B9B9B" }
}
};

70
src/chart/helpers.ts Normal file
View File

@ -0,0 +1,70 @@
import { ChartSeries } from ".";
import { APISeriesPayload, APISeries } from "../api";
import Highcharts, { SeriesLineOptions } from "highcharts";
import zipObject from "lodash/zipObject";
/**
* Converts an array of ChartSeries to an APISeriesPayload by grouping metric names by symbol.
*/
export function createApiSeries(chartSeries: ChartSeries[]): APISeries[] {
const series = chartSeries.reduce(
(acc, s) => {
const idKey = s.asset_id ? "asset_id" : "symbol";
// @ts-ignore
const existingIdx = acc.findIndex(i => i[idKey] === s[idKey]);
if (existingIdx >= 0) {
acc[existingIdx].names = acc[existingIdx].names.concat([s.metric]);
} else {
const apiSeries: APISeries = { names: [s.metric] };
if (idKey === "asset_id") {
apiSeries.asset_id = s.asset_id;
} else {
apiSeries.symbol = s.symbol;
}
acc.push(apiSeries);
}
return acc;
},
[] as APISeries[]
);
return series;
}
/**
* Creates a collection of Highchart.SeriesLineOptions from csv-like data returned by the API.
*/
export function createSeries(
series: ChartSeries[],
data: {
columns: string[];
prefixes: string[];
data: any[];
},
prefixes: { [k: string]: string }
): SeriesLineOptions[] {
const zippedData = data.data.map(row => {
return zipObject(data.columns, row);
});
return series.map(s => {
const idKey = s.asset_id ? "asset_id" : "symbol";
const data = zippedData
.filter(r => r[idKey] === s[idKey])
.map(r => [Date.parse(r.timestamp as string), r[s.metric]]);
return {
name: s.name || s.metric,
yAxis: s.yAxis,
tooltip: {
valuePrefix: prefixes[s.metric]
},
marker: { symbol: "circle" },
data
} as SeriesLineOptions;
});
}
function labelFormatter(name: string, prefixes: { [k: string]: string }) {
return function() {
return `${prefixes[name]}${this.value}`;
};
}

217
src/chart/index.tsx Normal file
View File

@ -0,0 +1,217 @@
import { h, Component } from "preact";
import merge from "lodash/merge";
import API from "../api";
import { createApiSeries, createSeries } from "./helpers";
import zipObject from "lodash/zipObject";
import { DEFAULT_HIGHCHARTS, DEFAULT_YAXIS } from "./defaults";
import CustomLinks from "../components/customLinks";
import * as css from "./style.css";
import NoDataMessage from "../components/noDataMessage";
import Highcharts from "highcharts/highstock";
require("highcharts/modules/exporting")(Highcharts);
// let Highcharts: any;
// // if (!window.Highcharts) {
// Highcharts = require("highcharts/highstock");
// require("highcharts/modules/exporting")(Highcharts);
// } else {
// Highcharts = window.Highcharts;
// }
type ChartType = "line" | "bar";
type ChartAxis = "left" | "right";
export type ChartSeries = {
symbol?: string;
asset_id?: number;
metric: string;
type: ChartType;
yAxis?: ChartAxis;
name?: string;
};
export type Props = {
mode?: "light" | "dark";
title?: string;
axisTitles?: string[];
startDate?: string;
endDate?: string;
series: ChartSeries[];
api: API;
exportingEnabled?: boolean;
};
type State = {
loading: boolean;
data: any;
};
class Chart extends Component<Props> {
static defaultProps: Partial<Props> = {
axisTitles: [],
mode: "light",
exportingEnabled: false,
};
state = {
loading: true,
};
container: HTMLElement;
async componentDidMount() {
const {
startDate,
endDate,
mode,
api,
series,
title,
...rest
} = this.props;
const apiSeries = createApiSeries(series);
let data;
try {
data = await api.fetchTimeseries({
series: apiSeries,
start_date: startDate,
end_date: endDate,
});
if (data.data.data.length > 0) {
this.setState({ loading: false, data: data.data.data });
} else {
this.setState({ loading: false });
return;
}
} catch (e) {
return;
}
const prefixes = zipObject(data.data.columns, data.data.prefixes) as {
[k: string]: string;
};
const loadedSeries = createSeries(series, data.data, prefixes);
const textColor = mode === "dark" ? "#fff" : "#000";
const tooltipBackground =
mode === "dark" ? "rgba(0,0,0,0.7)" : "rgba(255,255,255,0.7)";
const gridLineColor = mode === "dark" ? "#6c6c6c" : "#a3a3a3";
const options = merge(
{},
DEFAULT_HIGHCHARTS,
{
series: loadedSeries,
chart: {
renderTo: this.container,
},
title: {
text: this.props.title,
style: { color: textColor },
},
legend: {
enabled: series && series.length > 1,
itemStyle: { color: textColor },
itemHoverStyle: { color: textColor },
},
tooltip: {
backgroundColor: tooltipBackground,
style: {
color: textColor,
},
},
rangeSelector: {
buttonTheme: {
states: {
select: {
style: {
color: mode === "dark" ? "#fff" : "#000",
},
},
},
},
},
xAxis: {
lineColor: gridLineColor,
tickColor: gridLineColor,
},
yAxis: [
merge({}, DEFAULT_YAXIS, {
gridLineColor,
title: {
text: this.props.axisTitles[0],
style: { color: textColor },
},
}),
merge({}, DEFAULT_YAXIS, {
opposite: true,
gridLineColor,
title: {
text: this.props.axisTitles[1],
style: { color: textColor },
},
}),
],
},
rest
);
if (this.props.exportingEnabled) {
options.exporting = {
enabled: true,
buttons: {
contextButton: {
verticalAlign: "bottom",
horizontalAlign: "right",
x: 0,
y: 0,
color: "#ffffff",
symbolFill: "#ffffff",
theme: {
fill: "transparent",
cursor: "pointer",
states: { hover: { fill: "transparent", opacity: 0.7 } },
},
menuItems: [
"downloadCSV",
"separator",
"printChart",
"separator",
"downloadPNG",
"downloadJPEG",
"downloadPDF",
"downloadSVG",
],
},
},
};
} else {
options.exporting = { enabled: false };
}
Highcharts.chart(options);
}
render(_: Props, state: State) {
const { mode } = this.props;
return (
<div className={css[mode]}>
{!state.loading && !state.data && <NoDataMessage />}
{state.data && (
<CustomLinks
widget="chart"
api={this.props.api}
style={{ display: "block", textAlign: "right" }}
/>
)}
<div ref={(el) => (this.container = el)} />
</div>
);
}
}
export default Chart;

19
src/chart/style.css Normal file
View File

@ -0,0 +1,19 @@
.wrapper text {
font-size: 12px;
}
.wrapper a {
color: #20b7fc;
}
.dark text,
.light text {
font-size: 12px;
}
.dark a {
color: #20b7fc;
}
.light a {
color: #2d57ed;
}

5
src/chart/style.css.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
// This file is automatically generated.
// Please do not change this file!
export const dark: string;
export const light: string;
export const wrapper: string;

View File

@ -0,0 +1,152 @@
import { h, Component, render } from "preact";
import API, { WidgetLinksLink } from "../../api";
import find from "lodash/find";
import classNames from "classnames";
import * as css from "./style.css";
type Props = {
widget:
| "spectrum"
| "multi-table"
| "table"
| "score"
| "chart"
| "price-multi-table";
api: API;
style?: any;
linkClass?: string;
linkBootstrap?: WidgetLinksLink[];
};
type State = {
links: WidgetLinksLink[];
};
class CustomLinks extends Component<Props, State> {
state: State = {
links: [],
};
topLinkref: any = null;
setTopLinkRef = (dom: any) => (this.topLinkref = dom);
rightLinkref: any = null;
setRightLinkRef = (dom: any) => (this.rightLinkref = dom);
leftLinkref: any = null;
setLeftLinkRef = (dom: any) => (this.leftLinkref = dom);
sendParentMessage = (link: string) => {
parent.postMessage(
{
flipside: {
type: "linkAction",
linkAction: { href: link },
},
},
"*"
);
};
onClickLink = (e: any) => {
e.stopPropagation();
e.cancelBubble;
let href;
if (!e.target || (e.target && !e.target.getAttribute)) {
href = "https://flipsidecrypto.com";
} else {
href = e.target.getAttribute("href");
}
try {
this.sendParentMessage(href);
} catch (e) {
console.log(e);
}
window.location.assign(href);
};
handleLink = (ref: any, linkType: string) => {
const linkParent = ref;
if (!linkParent) return;
const link = linkParent.children[0];
if (!link) return;
link.removeEventListener("click", this.onClickLink);
link.addEventListener("click", this.onClickLink);
};
async componentDidMount() {
if (this.props.linkBootstrap) {
this.setState({ links: this.props.linkBootstrap });
return;
}
const res = await this.props.api.fetchWidgetLinks(this.props.widget);
this.setState({ links: res.data });
let that = this;
let interval = setInterval(() => {
that.handleLink(this.topLinkref, "top");
that.handleLink(this.rightLinkref, "right");
that.handleLink(this.leftLinkref, "left");
}, 100);
setTimeout(() => clearInterval(interval), 5000);
}
render(props: Props, state: State) {
const linkClass = classNames(css.link, props.linkClass);
if (state.links.length === 0) {
return (
<div class={css.wrapper} style={props.style}>
<span class={linkClass}>
<a
href="https://flipsidecrypto.com/fcas"
onClick={(e) => {
e.stopPropagation();
e.cancelBubble;
this.sendParentMessage("https://flipsidecrypto.com/fcas");
window.location.assign("https://flipsidecrypto.com/fcas");
}}
>
What's this?
</a>
</span>
</div>
);
}
const leftLink = find(state.links, { name: "left_link" });
const rightLink = find(state.links, { name: "right_link" });
const topLink = find(state.links, { name: "top_link" });
return (
<div class={css.wrapper} style={props.style}>
{topLink && (
<span
ref={this.setTopLinkRef}
class={linkClass}
dangerouslySetInnerHTML={{ __html: topLink.link_html }}
/>
)}
{leftLink && (
<span
ref={this.setLeftLinkRef}
class={linkClass}
dangerouslySetInnerHTML={{ __html: leftLink.link_html }}
/>
)}
{rightLink && (
<span
ref={this.setRightLinkRef}
class={linkClass}
dangerouslySetInnerHTML={{ __html: rightLink.link_html }}
/>
)}
</div>
);
}
}
export default CustomLinks;

View File

@ -0,0 +1,24 @@
.wrapper {
display: flex;
flex-grow: 1;
flex-wrap: wrap;
font-weight: 400;
}
.link {
font-size: 12px;
margin: 0 0 10px;
}
.link a {
font-weight: normal;
text-decoration: none;
}
.link a:hover {
text-decoration: underline;
}
.link:first-child {
margin-right: auto;
}

View File

@ -0,0 +1,4 @@
// This file is automatically generated.
// Please do not change this file!
export const link: string;
export const wrapper: string;

View File

@ -0,0 +1,6 @@
import { h } from "preact";
import * as css from "./style.css";
const NoDataMessage = () => <div></div>;
export default NoDataMessage;

View File

@ -0,0 +1,5 @@
.wrapper {
font-size: 14px !important;
font-weight: normal;
white-space: normal;
}

View File

@ -0,0 +1,3 @@
// This file is automatically generated.
// Please do not change this file!
export const wrapper: string;

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="7px" height="11px" viewBox="0 0 7 11" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.4 (67378) - http://www.bohemiancoding.com/sketch -->
<title>A</title>
<desc>Created with Sketch.</desc>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Tooltip" transform="translate(-189.000000, -407.000000)" fill="#000000">
<g id="Group-5" transform="translate(160.501044, 341.063787)">
<g id="Group-2" transform="translate(11.197399, 25.196325)">
<path d="M23.9809034,51.7280269 L22.2861929,51.7280269 L21.8661364,49.6856834 L19.4761599,49.6856834 L19.0705881,51.7280269 L17.3613929,51.7280269 L19.8962164,41.4149163 L21.4750493,41.4149163 L23.9809034,51.7280269 Z M21.5619576,48.2227281 L20.6783905,43.9062857 L19.7948234,48.2227281 L21.5619576,48.2227281 Z" id="A"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="7px" height="11px" viewBox="0 0 7 11" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.4 (67378) - http://www.bohemiancoding.com/sketch -->
<title>B</title>
<desc>Created with Sketch.</desc>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Tooltip" transform="translate(-189.000000, -428.000000)" fill="#000000">
<g id="Group-5" transform="translate(160.501044, 341.063787)">
<g id="Group-2" transform="translate(11.197399, 25.196325)">
<path d="M23.3870305,69.5377265 C23.3870305,70.7109877 23.213214,71.435223 22.720734,71.9421877 C22.2861929,72.3912136 21.6488658,72.5939995 20.6928752,72.5939995 L18.1290823,72.5939995 L18.1290823,62.2808889 L20.7073599,62.2808889 C21.6633505,62.2808889 22.2861929,62.4836748 22.720734,62.9182159 C23.1842446,63.3962112 23.3580611,64.0769924 23.3580611,65.0909218 C23.3580611,65.7861877 23.2856376,66.3655759 22.822127,66.8290865 C22.6627952,66.9884183 22.4889787,67.1042959 22.2861929,67.1912042 C22.5034634,67.2781124 22.6917646,67.4084748 22.8366117,67.5678065 C23.3435764,68.1182254 23.3870305,68.9003995 23.3870305,69.5377265 Z M21.7647434,69.5232418 C21.7647434,68.3789501 21.4026258,67.9154395 20.5335434,67.9154395 L19.7513693,67.9154395 L19.7513693,71.1310442 L20.5335434,71.1310442 C21.4171105,71.1310442 21.7647434,70.7254724 21.7647434,69.5232418 Z M21.735774,65.1343759 C21.735774,64.0914771 21.3881411,63.7438442 20.5190587,63.7438442 L19.7513693,63.7438442 L19.7513693,66.5393924 L20.5190587,66.5393924 C21.3736564,66.5393924 21.735774,66.1193359 21.735774,65.1343759 Z" id="B"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="7px" height="12px" viewBox="0 0 7 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.4 (67378) - http://www.bohemiancoding.com/sketch -->
<title>C</title>
<desc>Created with Sketch.</desc>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Tooltip" transform="translate(-189.000000, -448.000000)" fill="#000000">
<g id="Group-5" transform="translate(160.501044, 341.063787)">
<g id="Group-2" transform="translate(11.197399, 25.196325)">
<path d="M23.4232423,89.9176567 C23.3942729,90.8446779 23.090094,91.6413367 22.5686446,92.119332 C22.105134,92.5393885 21.5547152,92.7711438 20.7870258,92.7711438 C19.8020658,92.7711438 19.2226776,92.3945414 18.7881364,91.9020614 C18.020447,91.0329791 18.0349317,89.5410544 18.0349317,87.5131955 C18.0349317,85.4853367 18.020447,83.9644426 18.7881364,83.0953603 C19.2226776,82.6028803 19.8020658,82.2262779 20.7870258,82.2262779 C21.5691999,82.2262779 22.1196187,82.4580332 22.5831293,82.8925744 C23.090094,83.3705697 23.3797881,84.1527438 23.4087576,85.0652803 L21.7864705,85.0652803 C21.7719858,84.6886779 21.714047,84.3265603 21.5402305,84.0658355 C21.3953834,83.8340803 21.1781129,83.6892332 20.7870258,83.6892332 C20.3959387,83.6892332 20.1641834,83.848565 20.0193364,84.0803203 C19.7151576,84.5728003 19.6572187,85.6156991 19.6572187,87.4987108 C19.6572187,89.3817226 19.7151576,90.4246214 20.0193364,90.9171014 C20.1641834,91.1488567 20.3959387,91.3081885 20.7870258,91.3081885 C21.1781129,91.3081885 21.4098681,91.1633414 21.5547152,90.9315861 C21.7285317,90.6708614 21.7864705,90.2942591 21.8009552,89.9176567 L23.4232423,89.9176567 Z" id="C"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="5px" height="11px" viewBox="0 0 5 11" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.4 (67378) - http://www.bohemiancoding.com/sketch -->
<title>F</title>
<desc>Created with Sketch.</desc>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Tooltip" transform="translate(-190.000000, -468.000000)" fill="#000000">
<g id="Group-5" transform="translate(160.501044, 341.063787)">
<g id="Group-2" transform="translate(11.197399, 25.196325)">
<polygon id="F" points="22.9197646 103.866378 20.2111246 103.866378 20.2111246 106.792288 22.5431623 106.792288 22.5431623 108.255243 20.2111246 108.255243 20.2111246 112.716533 18.5888376 112.716533 18.5888376 102.403422 22.9197646 102.403422"></polygon>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 989 B

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="7px" height="11px" viewBox="0 0 7 11" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.4 (67378) - http://www.bohemiancoding.com/sketch -->
<title>S</title>
<desc>Created with Sketch.</desc>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Tooltip" transform="translate(-189.000000, -388.000000)" fill="#000000">
<g id="Group-5" transform="translate(160.501044, 341.063787)">
<g id="Group-2" transform="translate(11.197399, 25.196325)">
<path d="M23.4632093,29.6179791 C23.4632093,30.6753626 23.3183623,31.3851132 22.7969129,31.8920779 C22.3768564,32.2976497 21.6960752,32.5728591 20.7835387,32.5728591 C19.885487,32.5728591 19.2191905,32.3266191 18.7846493,31.8920779 C18.2921693,31.3995979 18.1183529,30.7622708 18.1183529,29.6759179 L19.7406399,29.6759179 C19.7406399,30.2263367 19.7985787,30.5739697 20.030334,30.8346944 C20.1751811,30.9940261 20.4214211,31.1099038 20.7835387,31.1099038 C21.1601411,31.1099038 21.4063811,31.0085108 21.5657129,30.8202097 C21.7829834,30.5739697 21.8409223,30.2263367 21.8409223,29.6759179 C21.8409223,28.5750803 21.6671058,28.2998708 20.870447,27.9812073 L19.639247,27.4742426 C18.5963481,27.0397014 18.2052611,26.4747979 18.2052611,24.8814803 C18.2052611,23.9544591 18.4804705,23.2012544 19.045374,22.6942897 C19.4943999,22.3032026 20.0737881,22.0714473 20.8125081,22.0714473 C21.6381364,22.0714473 22.246494,22.2887179 22.6665505,22.6942897 C23.2024846,23.2157391 23.3907858,23.9544591 23.3907858,24.9394191 L21.7684987,24.9394191 C21.7684987,24.4759085 21.7395293,24.1137908 21.5222587,23.8385814 C21.3774117,23.6502803 21.1456564,23.5344026 20.7980234,23.5344026 C20.4648752,23.5344026 20.2620893,23.6502803 20.1027576,23.8240967 C19.9144564,24.0413673 19.8275481,24.3890003 19.8275481,24.8380261 C19.8275481,25.6781391 19.9579105,25.9388638 20.6821458,26.2285579 L21.8988611,26.7210379 C23.1300611,27.2280026 23.4632093,27.8798144 23.4632093,29.6179791 Z" id="S"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,62 @@
import { h, Component } from "preact";
import classNames from "classnames";
import * as css from "./style.css";
type Props = {
score: number;
grade?: string;
kind?: "slim" | "normal" | "large";
class?: string;
};
type State = {
showTooltip: boolean;
};
export default class Rank extends Component<Props, State> {
static defaultProps = {
kind: "slim",
};
state: State = {
showTooltip: false,
};
showTooltip = () => {
this.setState({ showTooltip: true });
};
hideTooltip = () => {
this.setState({ showTooltip: false });
};
render(props: Props) {
let rankClass;
let css_: any = css;
if (props.grade) {
rankClass = css_[props.grade.toLowerCase()];
} else {
if (props.score <= 500) {
rankClass = css_.f;
} else if (props.score <= 649) {
rankClass = css_.c;
} else if (props.score <= 749) {
rankClass = css_.b;
} else if (props.score <= 899) {
rankClass = css_.a;
} else {
rankClass = css_.s;
}
}
let kindClass = css[props.kind];
const classes = classNames(css.rank, rankClass, kindClass);
return (
<div class={css.wrapper}>
{props.kind === "large" && "Rank"}
<span class={classes} />
</div>
);
}
}

View File

@ -0,0 +1,52 @@
.wrapper {
display: inline-block;
position: relative;
font-size: 10px;
font-weight: 600;
text-align: center;
}
.rank {
display: block;
border-radius: 3px;
background: center no-repeat;
}
.s {
background-color: #68ba66;
background-image: url(./images/S.svg);
}
.a {
background-color: #8fcb89;
background-image: url(./images/A.svg);
}
.b {
background-color: #b2dbad;
background-image: url(./images/B.svg);
}
.c {
background-color: #ff7a18;
background-image: url(./images/C.svg);
}
.f {
background-color: #ff2600;
background-image: url(./images/F.svg);
}
.slim {
background-size: auto 70%;
height: 14px;
width: 32px;
}
.normal {
background-size: auto 70%;
width: 54px;
height: 22px;
}
.large {
background-size: auto 48%;
height: 28px;
width: 32px;
}

12
src/components/rank/style.css.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
// This file is automatically generated.
// Please do not change this file!
export const a: string;
export const b: string;
export const c: string;
export const f: string;
export const large: string;
export const normal: string;
export const rank: string;
export const s: string;
export const slim: string;
export const wrapper: string;

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="13px" height="8px" viewBox="0 0 13 8" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.4 (67378) - http://www.bohemiancoding.com/sketch -->
<title>down</title>
<desc>Created with Sketch.</desc>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<polygon id="down" fill="#980000" transform="translate(6.563348, 3.988132) rotate(-180.000000) translate(-6.563348, -3.988132) " points="6.56334793 0 12.30853 7.97626368 0.818165821 7.97626368"></polygon>
</g>
</svg>

After

Width:  |  Height:  |  Size: 638 B

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="12px" height="8px" viewBox="0 0 12 8" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.4 (67378) - http://www.bohemiancoding.com/sketch -->
<title>up</title>
<desc>Created with Sketch.</desc>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<polygon id="up" fill="#417505" points="5.74518211 0 11.4903642 7.97626368 0 7.97626368"></polygon>
</g>
</svg>

After

Width:  |  Height:  |  Size: 531 B

View File

@ -0,0 +1,48 @@
import { h } from "preact";
import classNames from "classnames";
import * as css from "./style.css";
type Props = {
change?: number;
pointChange?: number;
value: number;
class?: string;
};
export function calculateTrendDiff(value: number, percent: number): number {
return Math.round(Math.abs(value * (percent / 100)));
}
const Trend = (props: Props) => {
let directionClass, icon;
let changeDeterminate = props.pointChange ? props.pointChange : props.change;
if (changeDeterminate < 0) {
directionClass = css.down;
icon = require("./images/down.svg");
} else if (changeDeterminate > 0) {
directionClass = css.up;
icon = require("./images/up.svg");
}
let difference;
if (props.pointChange) {
difference = props.pointChange;
} else {
difference = calculateTrendDiff(props.value, props.change);
}
if (!difference || difference === 0) {
return <span />;
}
const classes = classNames(css.wrapper, directionClass, props.class);
return (
<span class={classes}>
<img class={css.icon} src={icon} />
{difference}
</span>
);
};
export default Trend;

View File

@ -0,0 +1,18 @@
.wrapper {
display: flex;
align-items: center;
justify-content: flex-end;
}
.icon {
height: 6px;
margin-right: 2px;
}
.up {
color: #057511;
}
.down {
color: #980000;
}

6
src/components/trend/style.css.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
// This file is automatically generated.
// Please do not change this file!
export const down: string;
export const icon: string;
export const up: string;
export const wrapper: string;

View File

@ -0,0 +1,57 @@
import {
h,
Component,
AnyComponent,
ComponentFactory,
ComponentChildren,
Ref,
RenderableProps
} from "preact";
import API from "../../api";
type Data = {
asset_name: string;
percent_change: number;
slug: string;
symbol: string;
value: number;
};
type Props = RenderableProps<{
symbol: string;
api: API;
}>;
type State = {
loading: boolean;
data?: Data;
};
export type WithFcasProps = {
fcas?: Data;
};
const withFcas = <P extends Props>(C: ComponentFactory<P & WithFcasProps>) => {
return class WithFcas extends Component<P, State> {
state = {
loading: true
};
componentDidMount() {
this.loadData();
}
loadData = async () => {
const { api, symbol } = this.props;
const { data, success } = await api.fetchAssetMetric(symbol, "FCAS");
this.setState({ data, loading: false });
};
render(props: P, state: State) {
if (state.loading) return null;
return <C {...props} fcas={state.data} />;
}
};
};
export default withFcas;

8
src/custom.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
declare module "*.css" {
const css: any;
export default css;
}
interface Window {
Highcharts: any;
}

64
src/dynamic/index.tsx Normal file
View File

@ -0,0 +1,64 @@
import loadJS from "load-js";
import API from "../api";
import template from "lodash/template";
import mapValues from "lodash/mapValues";
import { stringify } from "querystring";
type DynamicOpts = {
widgetId: string;
darkMode?: boolean;
data?: object;
};
export default async function dynamic(api: API, el: string, opts: DynamicOpts) {
const res = await api.fetchDynamic(opts.widgetId);
if (!res.success) {
throw new Error(`Flipside: dynamic widget loading failed`);
}
if (res.data && res.data.js_url && res.data.js_url != "none") {
await loadJS([
{
url: res.data.js_url,
allowExternal: true,
},
]);
}
const flipside = new window.Flipside(api.key);
const fn: any = (flipside as any)[res.data.function_name];
if (!fn) {
throw new Error(
`Flipside: dynamic function name '${res.data.function_name}' doesn't exist`
);
}
const config = interpolateConfig(res.data.function_config, opts.data);
fn.call(flipside, el, { ...config, mode: opts.darkMode ? "dark" : "light" });
}
function interpolateConfig(functionConfigTemplate: Object, data: Object): any {
const jsonConfig = JSON.stringify(functionConfigTemplate);
const compiledTemplate = template(jsonConfig);
const walk = (d: any): any => {
if (typeof d === "string") {
let n = parseInt(d);
if (!n || (n && JSON.stringify(n).length != d.length)) return d;
return n;
}
if (Array.isArray(d)) {
return d.map((item) => {
return walk(item);
});
}
if (typeof d === "object") {
return mapValues(d, (item: any) => {
return walk(item);
});
}
return d;
};
return walk(JSON.parse(compiledTemplate({ ...data })));
}

4
src/dynamic/load-js.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module 'load-js' {
const fn: any;
export default fn;
}

View File

@ -1,72 +0,0 @@
import { h, Component } from "preact";
import Score from "./score";
import Plot from "./plot";
import "./styles.scss";
export default class FCAS extends Component {
constructor() {
super();
this.state = { loading: true, metric: null };
}
async _getData() {
const { data, success } = await this.props.api.fetchAssetMetric(
this.props.symbol,
"FCAS"
);
if (!success || !data) {
setTimeout(() => {
return this._getData();
}, 2000);
return success;
}
this.setState({
loading: false,
metric: {
fcas: Math.round(data.value),
change: data.percent_change,
name: data.asset_name
}
});
return success;
}
_update() {
this.interval = setInterval(async () => {
await this._getData();
}, 300000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
async componentDidMount() {
const success = await this._getData();
if (!success) {
this.setState({
loading: false,
metric: {
fcas: "NA",
change: "NA",
name: "NA"
}
});
}
this._update();
}
render({ opts, api, symbol }, { metric, loading }) {
if (loading) return null;
return (
<div class="fs-container">
{opts.score && <Score symbol={symbol} metric={metric} opts={opts} />}
{opts.plot && (
<Plot symbol={symbol} metric={metric} api={api} opts={opts} />
)}
</div>
);
}
}

View File

@ -1,280 +0,0 @@
import { h, Component } from "preact";
import { sortObjectArray } from "../../utils";
import "./styles.scss";
const PLOT_WIDTH = 240;
const PLOT_SCALE = PLOT_WIDTH / 1000;
const DEFAULT_BUCKET_DISTANCE = 35;
const DEFAULT_LINE_DISTANCE = 25;
export default class Plot extends Component {
constructor() {
super();
this.state = {
loading: true,
distribution: null,
highlights: [],
highlightedSymbols: []
};
}
async _getData() {
const { data, success } = await this.props.api.fetchFCASDistribution();
if (!success || !data) {
setTimeout(() => {
return this._getData();
}, 2000);
return success;
}
if (data && data.length > 0) {
this.setState({
loading: false,
distribution: data
});
}
return success;
}
async _getHighlights() {
const highlights = this.getHighlights();
const nextHighlightState = [];
const nextHighlightedSymbolState = [];
await Promise.all(
highlights.map(async asset => {
const { data, success } = await this.props.api.fetchAssetMetric(
asset,
"fcas"
);
if (success === true && data) {
nextHighlightState.push(data);
nextHighlightedSymbolState.push(data.symbol);
}
})
);
if (nextHighlightState.length < this.state.highlights.length) {
return;
}
this.setState({
highlights: nextHighlightState,
highlightedSymbols: nextHighlightedSymbolState
});
}
_update() {
this.interval = setInterval(() => {
this._getData();
}, 300000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
async componentDidMount() {
this._getHighlights();
const success = await this._getData();
if (!success) {
this.setState({
loading: false,
distribution: []
});
}
this._update();
}
getHighlights() {
let { symbol, opts } = this.props;
symbol = symbol.toLowerCase();
if (opts && opts.highlights && opts.highlights.length > 0) {
return opts.highlights;
}
let highlights = [];
if (symbol == "eth" || symbol == "btc") {
highlights = ["ZEC", "XRP"];
} else {
highlights = ["BTC"];
}
return highlights;
}
getBuckets() {
if (this.state.highlights.length == 0) {
return [];
}
let { bucketDistance } = this.props.opts;
if (!bucketDistance) {
bucketDistance = DEFAULT_BUCKET_DISTANCE;
}
let buckets = [];
let currentBucketIndex = 0;
let anchorX = 0;
let scoresToBuckets = {};
let highlightLength = this.state.highlights.length;
let sortedHighLights = sortObjectArray(this.state.highlights, "value");
for (let i = 0; i < highlightLength; i++) {
let currentAsset = sortedHighLights[i];
// If first item
if (i === 0) {
buckets[currentBucketIndex] = [];
buckets[currentBucketIndex].push(currentAsset);
scoresToBuckets[currentAsset.value] = currentBucketIndex;
anchorX = currentAsset.value;
continue;
}
const nextAsset =
i !== highlightLength - 1 ? sortedHighLights[i + 1] : null;
const prevDist = Math.abs(anchorX - currentAsset.value);
// const nextDist = nextAsset
// ? Math.abs(nextAsset.value - currentAsset.value)
// : 1000000;
if (prevDist <= bucketDistance) {
buckets[currentBucketIndex].push(currentAsset);
scoresToBuckets[currentAsset.value] = currentBucketIndex;
} else {
currentBucketIndex++;
buckets[currentBucketIndex] = [];
buckets[currentBucketIndex].push(currentAsset);
scoresToBuckets[currentAsset.value] = currentBucketIndex;
anchorX = currentAsset.value;
}
}
return { buckets, scoresToBuckets };
}
getYCoords(asset, buckets, scoresToBuckets) {
let { lineDistance } = this.props.opts;
if (!lineDistance) {
lineDistance = DEFAULT_LINE_DISTANCE;
}
let bucketIndex = scoresToBuckets[asset.value];
let bucket = buckets[bucketIndex];
let index = 0;
let toClose = false;
for (let i = 0; i < bucket.length; i++) {
let ba = bucket[i];
if (ba.symbol == asset.symbol) {
index = i;
if (i === 0) {
break;
}
const prevAsset = bucket[i - 1];
if (prevAsset && Math.abs(prevAsset.value - ba.value) <= lineDistance) {
toClose = true;
}
break;
}
}
return { y: 44 - 10 * index, toClose };
}
render({ opts, metric, symbol }, { loading, distribution }) {
if (loading) return null;
const highlightedSymbols = this.state.highlightedSymbols;
const highlights = this.state.highlights;
distribution = [...distribution, ...highlights];
const xPos = `${(metric.fcas / 1000) * 100}%`;
const highlightedAssets = distribution
.filter(i => highlightedSymbols.indexOf(i.symbol) > -1)
.filter(i => i.symbol != symbol.toUpperCase());
const { buckets, scoresToBuckets } = this.getBuckets();
console.log("buckets: ", buckets, "scoresToBuckets: ", scoresToBuckets);
return (
<svg class="fs-plot" width="100%" height="104" overflow="visible">
<defs>
<linearGradient id="gradient">
<stop stop-color="#ff2600" offset="0%" />
<stop stop-color="#ff7a18" offset="40%" />
<stop stop-color="#8fcb89" offset="68%" />
<stop stop-color="#30a92d" offset="92%" />
</linearGradient>
</defs>
<g fill={opts.dark ? "#fff" : "#000"}>
<circle cx="0" cy="44" r="2.5" />
<text x="6" y="47" font-size="8">
Coins
</text>
</g>
{/* Distribution Dots */}
<g fill={opts.dark ? "rgba(255, 255,255, 0.5)" : "rgba(0, 0, 0, 0.4)"}>
{distribution.map(i => (
<circle cx={`${(i.value / 1000) * 100}%`} cy="58" r="2.5" />
))}
</g>
{/* Gradient Line */}
<rect x="0" y="64" width="100%" height="6" fill="url(#gradient)" />
{/* Line Labels */}
<text
y="85"
text-anchor="middle"
class="fs-plot__x"
fill={opts.dark ? "#fff" : "#000"}
>
<tspan x="0">0</tspan>
<tspan x="50%">500</tspan>
<tspan x="100%">1000</tspan>
</text>
{highlightedAssets.map(a => {
const xPos = `${(a.value / 1000) * 100}%`;
let { y, toClose } = this.getYCoords(a, buckets, scoresToBuckets);
return (
<g fill={opts.dark ? "#fff" : "#000"}>
<text x={xPos} y={y} text-anchor="middle" font-size="10">
{a.symbol}
</text>
{toClose === false && (
<line
x1={xPos}
y1={y + 3}
x2={xPos}
y2="60"
style={`stroke:rgb(${
opts.dark ? "255, 255, 255" : "0,0,0"
}); stroke-width:0.5`}
/>
)}
</g>
);
})}
{/* Blue FCAS Marker */}
<text class="fs-plot__blue" text-anchor="middle" font-weight="bold">
<tspan x={xPos} y={opts.mini ? 26 : 14}>
{symbol.toUpperCase()}
</tspan>
{!opts.mini && (
<tspan x={xPos} y="104">
{metric.fcas}
</tspan>
)}
</text>
{/* Blue FCAS Marker Line */}
<line
x1={xPos}
y1={opts.mini ? 28 : 16}
x2={xPos}
y2={opts.mini ? 60 : 92}
style="stroke:rgb(45,87,237); stroke-width:1"
/>
</svg>
);
}
}

View File

@ -1,8 +0,0 @@
.fs-plot__blue {
fill: #2d57ed;
font-size: 12px;
}
.fs-plot__x {
font-size: 7px;
}

View File

@ -1,101 +0,0 @@
import { h, Component } from "preact";
function round(value) {
if (!value) {
return value;
}
return Math.round(value * 100) / 100;
}
export default class Data extends Component {
constructor() {
super();
this.state = {
showTooltip: false
};
this._showTooltip = this._showTooltip.bind(this);
this._hideTooltip = this._hideTooltip.bind(this);
}
_showTooltip() {
this.setState({ showTooltip: true });
}
_hideTooltip() {
this.setState({ showTooltip: false });
}
render({ opts, metric, rank }, { showTooltip }) {
let trendDir, trendIcon;
if (metric.change < 0) {
trendDir = "down";
trendIcon = require("./images/icon-down.svg");
} else if (metric.change == 0) {
trendDir = "eq";
trendIcon = require("./images/icon-eq.svg");
} else {
trendDir = "up";
trendIcon = require("./images/icon-up.svg");
}
const trendDiff = Math.abs(metric.fcas * (metric.change / 100));
return (
<div>
<div class="fs-fcas">
<h2 class="fs-fcas__header">FCAS</h2>
<span
class="fs-link fs-fcas__what"
onMouseEnter={this._showTooltip}
onMouseLeave={this._hideTooltip}
>
What's this?
{showTooltip && (
<div class="fs-tooltip">
<div class="fs-tooltip__content">
<p>
<b>FCAS</b> is Flipsides Crypto Asset Score, ranging from 0
- 1000. The score combines values from the 3 major market
factors to create a comparative metric across digital
assets.
</p>
<p>
Powered by{" "}
<a
target="_blank"
href="https://flipsidecrypto.com"
class="fs-link"
>
flipsidecrypto.com
</a>
</p>
</div>
</div>
)}
</span>
</div>
<div class="fs-data">
<h3 class="fs-value">{metric.fcas}</h3>
{opts.trend && (
<div class="fs-trend">
<div class={`fs-trend__change fs-trend__change--${trendDir}`}>
<img class="fs-trend__icon" src={trendIcon} />{" "}
{round(trendDiff)}
</div>
7d
</div>
)}
{opts.rank && (
<div class="fs-rank">
<b class={`fs-rank__letter fs-rank__letter--${rank}`}>{rank}</b>{" "}
Rank
</div>
)}
</div>
</div>
);
}
}

View File

@ -1,14 +0,0 @@
import { h } from "preact";
export default ({ opts, metric, rank }) => {
return (
<div class="fs-data-mini">
FCAS {metric.fcas}
<div
class={`fs-rank fs-rank__letter fs-rank__letter--mini fs-rank__letter--${rank}`}
>
{rank}
</div>
</div>
);
};

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="11px" height="12px" viewBox="0 0 11 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.4 (67378) - http://www.bohemiancoding.com/sketch -->
<title>icon-down</title>
<desc>Created with Sketch.</desc>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M4,7 L0,7 L5.5,0 L11,7 L7,7 L7,12 L4,12 L4,7 Z" id="icon-down" fill="#FF2700" transform="translate(5.500000, 6.000000) rotate(-180.000000) translate(-5.500000, -6.000000) "></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 630 B

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="8px" height="7px" viewBox="0 0 8 7" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.4 (67378) - http://www.bohemiancoding.com/sketch -->
<title>icon-eq</title>
<desc>Created with Sketch.</desc>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M0,4 L8,4 L8,7 L0,7 L0,4 Z M0,0 L8,0 L8,3 L0,3 L0,0 Z" id="icon-eq" fill="#808080"></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 534 B

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="11px" height="12px" viewBox="0 0 11 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.4 (67378) - http://www.bohemiancoding.com/sketch -->
<title>icon-up</title>
<desc>Created with Sketch.</desc>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M4,7 L0,7 L5.5,0 L11,7 L7,7 L7,12 L4,12 L4,7 Z" id="icon-up" fill="#057511"></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 531 B

View File

@ -1,55 +0,0 @@
import { h, Component } from "preact";
import Data from "./Data";
import DataMini from "./DataMini";
import "./styles.scss";
function round(value) {
if (!value) {
return value;
}
return Math.round(value * 100) / 100;
}
export default class Score extends Component {
render({ opts, symbol, metric }, { showTooltip }) {
let rank;
if (metric.fcas <= 500) {
rank = "f";
} else if (metric.fcas <= 649) {
rank = "c";
} else if (metric.fcas <= 749) {
rank = "b";
} else if (metric.fcas <= 899) {
rank = "a";
} else {
rank = "s";
}
let wrapperClass = "fs-score";
if (opts.dark) wrapperClass += " fs-score--dark";
if (opts.mini) wrapperClass += " fs-score--mini";
const DataComponent = opts.mini ? DataMini : Data;
return (
<div class={wrapperClass}>
{opts.header && (
<header class="fs-token">
{opts.logo && (
<img
class="fs-token__logo"
src={`https://s3.amazonaws.com/fsc-crypto-icons/svg/color/${symbol}.svg`}
/>
)}
<h1 class="fs-token__name">
{metric.name}
{opts.symbol && <span class="fs-token__sym">{symbol}</span>}
</h1>
</header>
)}
<DataComponent opts={opts} metric={metric} rank={rank} />
</div>
);
}
}

View File

@ -1,170 +0,0 @@
.fs-score {
margin-bottom: 25px;
}
.fs-score--dark {
color: #fff;
}
.fs-score--mini {
margin-bottom: 10px;
}
.fs-token {
display: flex;
align-items: center;
margin-bottom: 26px;
}
.fs-token__logo {
margin-right: 8px;
height: 24px;
width: 24px;
}
.fs-token__name {
font-size: 20px;
font-weight: 400;
margin: 0;
}
.fs-token__sym {
font-size: 12px;
color: #2d57ed;
text-transform: uppercase;
margin-left: 6px;
}
.fs-fcas {
display: flex;
align-items: center;
}
.fs-fcas__header {
margin: 0 10px 0 0;
font-size: 18px;
font-weight: 800;
}
.fs-fcas__what {
font-size: 8px;
position: relative;
}
.fs-tooltip {
padding-left: 15px;
position: absolute;
top: 50%;
left: 100%;
transform: translateY(-50%);
}
.fs-tooltip__content {
position: relative;
width: 200px;
border-radius: 8px;
background: #ffffff;
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.15);
font-size: 12px;
color: #000;
line-height: 14px;
padding: 5px 15px;
border: 1px solid rgba(200, 200, 200, 0.5);
&:before {
display: block;
content: "";
width: 20px;
height: 20px;
position: absolute;
left: -10px;
top: calc(50% - 10px);
transform: rotate(-45deg);
background: #fff;
box-shadow: -1px -1px 0 rgba(200, 200, 200, 0.5);
}
}
.fs-data {
display: flex;
align-items: center;
text-align: center;
font-size: 10px;
}
.fs-data-mini {
font-size: 14px;
color: #2d57ed;
display: flex;
align-items: center;
}
.fs-value {
font-size: 58px;
font-weight: bold;
margin: 0 14px 0 0;
}
.fs-trend {
margin-right: 16px;
}
.fs-trend__icon {
margin-right: 2px;
}
.fs-trend__change {
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
height: 28px;
margin-bottom: 5px;
&--up {
color: #057511;
}
&--down {
color: #ff2700;
}
&--eq {
color: #808080;
}
}
.fs-rank {
text-align: center;
font-size: 10px;
}
.fs-rank__letter {
width: 32px;
height: 28px;
display: block;
font-size: 18px;
font-weight: 400;
border-radius: 2px;
line-height: 28px;
margin-bottom: 5px;
text-transform: uppercase;
color: #000;
}
.fs-rank__letter--mini {
height: 22px;
line-height: 22px;
width: 50px;
margin: 0 0 0 auto;
}
.fs-rank__letter--s {
background: #68ba66;
}
.fs-rank__letter--a {
background: #8fcb89;
}
.fs-rank__letter--b {
background: #b2dbad;
}
.fs-rank__letter--c {
background: #ff7a18;
}
.fs-rank__letter--f {
background: #ff2600;
}

View File

@ -1,13 +0,0 @@
.fs-container {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
line-height: 1;
border-radius: 25px;
}
.fs-link {
color: #2d57ed;
text-decoration: underline;
cursor: pointer;
}

139
src/frame/index.tsx Normal file
View File

@ -0,0 +1,139 @@
import { h, Component } from "preact";
type Props = {
apiKey: string;
mode: string;
url: string;
width?: string;
height?: string;
data?: any;
messageKey?: string;
messagePayloadType?: string;
messagePayloadActionKey?: string;
};
type State = {
height: string;
width: string;
};
export default class Frame extends Component<Props, State> {
static defaultProps = {
width: "100%",
height: "100%",
mode: "light",
data: {},
messageKey: "flipside",
messagePayloadType: "linkAction",
messagePayloadActionKey: "href",
};
ref: any = null;
setRef = (dom: any) => (this.ref = dom);
handleResize = (height: string, width: string) => {
this.setState({ height: height, width: width });
};
handleMessage = (e: any) => {
const eventData = e.data;
if (!eventData) return;
const {
messageKey,
messagePayloadType,
messagePayloadActionKey,
} = this.props;
const message = eventData[messageKey];
if (!message) return;
if (message.type == "sizeAction") {
return this.handleResize(
message["sizeAction"].height,
message["sizeAction"].width
);
}
if (message.type !== messagePayloadType) return;
const payload = message[messagePayloadType];
if (!payload) return;
const messageAction = payload[messagePayloadActionKey];
if (!messageAction || typeof messageAction != "string") return;
window.location.assign(messageAction);
};
componentWillMount() {
window.addEventListener("message", this.handleMessage, false);
if (!this.ref) return;
this.setState({
height: this.props.height,
width: this.props.width,
});
const widgetData = {
mode: this.props.mode,
data: this.props.data,
window: {
location: {
origin: window.origin,
href: window.location && window.location.href,
pathName: window.location && window.location.pathname,
},
dimensions: {
innerHeight: window.innerHeight,
innerWidth: window.innerWidth,
outerHeight: window.outerHeight,
outerWidth: window.outerWidth,
},
},
};
let that = this;
let interval = setInterval(() => {
that.ref.contentWindow.postMessage(
{
flipsidePartner: {
type: "widgetData",
widgetData: widgetData,
},
},
"*"
);
}, 200);
setTimeout(() => clearInterval(interval), 5000);
}
componentWillUnmount() {
window.removeEventListener("message", this.handleMessage);
}
render(props: Props, state: State) {
let url = props.url;
let urlParams = { api_key: props.apiKey, mode: props.mode };
if (
props.data &&
typeof props.data === "object" &&
!Array.isArray(props.data)
) {
urlParams = { ...props.data, ...urlParams };
}
const urlEncodedParams = new URLSearchParams(urlParams).toString();
url = `${url}?${urlEncodedParams}`;
return (
<iframe
ref={this.setRef}
src={url}
style={{ width: state.width, height: state.height, border: 0 }}
width={state.width || "100%"}
height={state.height}
/>
);
}
}

View File

@ -1,41 +0,0 @@
import { h, render } from "preact";
import FCAS from "./fcas";
import API from "./api";
import Table from "./table";
class Flipside {
constructor(apiKey) {
this.api = new API(apiKey);
}
createTable(el, symbol, opts) {
const defaults = {
dark: false
};
const mergedOpts = Object.assign({}, defaults, opts);
const element = typeof el === "string" ? document.getElementById(el) : el;
render(<Table symbol={symbol} api={this.api} {...mergedOpts} />, element);
}
createFCAS(el, symbol, opts) {
symbol = symbol.toLowerCase();
const defaults = {
score: true,
plot: true,
symbol: true,
logo: true,
trend: true,
rank: true,
header: true,
dark: false
};
const mergedOpts = Object.assign({}, defaults, opts);
const element = typeof el === "string" ? document.getElementById(el) : el;
render(<FCAS symbol={symbol} api={this.api} opts={mergedOpts} />, element);
}
}
window.Flipside = Flipside;

73
src/index.tsx Normal file
View File

@ -0,0 +1,73 @@
// ie11 polyfills
import "core-js/fn/promise";
import "core-js/fn/object/assign";
import { h, render } from "preact";
import API from "./api";
import Table from "./table";
import { defaultsWithoutArrays } from "./utils";
import Spectrum, { Props as SpectrumProps } from "./spectrum";
import MultiTable, { Props as MultiTableProps } from "./multiTable";
import Score, { Props as ScoreProps } from "./score";
import Chart, { Props as ChartProps } from "./chart";
import Axios from "axios";
import dynamic from "./dynamic";
import Frame from "./frame";
export default class Flipside {
api: API;
constructor(apiKey: string) {
this.api = new API(apiKey);
}
async dynamic(el: string, opts: any) {
dynamic(this.api, el, opts);
}
multiTable(el: string, opts: MultiTableProps) {
const element = document.getElementById(el);
const props = defaultsWithoutArrays(MultiTable.defaultProps, opts);
render(<MultiTable {...props} api={this.api} />, element);
}
spectrum(el: string, opts: SpectrumProps): void {
const element = document.getElementById(el);
const props = defaultsWithoutArrays(Spectrum.defaultProps, opts);
render(<Spectrum {...props} api={this.api} />, element);
}
score(el: string, opts: ScoreProps) {
render(<Score {...opts} api={this.api} />, document.getElementById(el));
}
async chart(el: string, opts: any) {
render(<Chart {...opts} api={this.api} />, document.getElementById(el));
}
frame(el: string, opts: any) {
render(
<Frame {...opts} apiKey={this.api.key} />,
document.getElementById(el)
);
}
createTable(el: string, symbol: string, opts: object) {
const defaults = {
dark: false,
};
const mergedOpts = Object.assign({}, defaults, opts);
const element = typeof el === "string" ? document.getElementById(el) : el;
render(<Table symbol={symbol} api={this.api} {...mergedOpts} />, element);
}
}
declare global {
interface Window {
Flipside: any;
}
}
window.Flipside = Flipside;

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="8px" height="6px" viewBox="0 0 8 6" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.4 (67378) - http://www.bohemiancoding.com/sketch -->
<title>Triangle</title>
<desc>Created with Sketch.</desc>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Table-Widget" transform="translate(-405.000000, -268.000000)" fill="#000000">
<g id="Group-21" transform="translate(404.670464, 266.000000)">
<polygon id="Triangle" transform="translate(4.329536, 5.000000) rotate(-180.000000) translate(-4.329536, -5.000000) " points="4.32953555 2 8.32953555 8 0.329535553 8"></polygon>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 820 B

323
src/multiTable/index.tsx Normal file
View File

@ -0,0 +1,323 @@
import { h, Component } from "preact";
import classnames from "classnames";
import Rank from "../components/rank";
import Trend from "../components/trend";
import CustomLinks from "../components/customLinks";
import API from "../api";
import { Row, ColumnName, ColumnDefinition } from "./types";
import sortBy from "lodash/sortBy";
import intersection from "lodash/intersection";
import reverse from "lodash/reverse";
import "./style.scss";
function mapConfigColumnsNames(columns: string[]): string[] {
const actualColumnNames: { [k: string]: string } = {
marketMaturity: "market-maturity",
userActivity: "utility",
developerBehavior: "dev"
};
return columns.map(col => actualColumnNames[col] || col);
}
const COLUMNS: { [k: string]: ColumnDefinition } = {
symbol: {
header: "Coin",
renderItem: row => row.symbol
},
name: {
header: "Coin",
renderItem: row => row.asset_name || row.symbol
},
fcas: {
header: "FCAS",
renderItem: row => row.fcas,
sortKey: "fcas"
},
trend: {
header: "7D",
renderItem: row => <Trend change={row.fcas_change} value={row.fcas} />,
sortKey: "fcas_change"
},
userActivity: {
header: "User Activity",
renderItem: row => row.utility,
sortKey: "utility"
},
developerBehavior: {
header: "Developer Behavior",
renderItem: row => row.dev,
sortKey: "dev"
},
marketMaturity: {
header: "Market Maturity",
renderItem: row => row.market_maturity,
sortKey: "market_maturity"
},
rank: {
header: "Rank",
renderItem: row => <Rank score={row.fcas} />,
sortKey: "fcas"
},
volume_24h: {
header: "Volume",
renderItem: row =>
`$${row.volume_24h.toLocaleString(undefined, {
maximumFractionDigits: 0
})}`,
sortKey: "volume_24h"
},
market_cap: {
header: "Market Cap",
renderItem: row =>
`$${row.market_cap.toLocaleString(undefined, {
maximumFractionDigits: 0
})}`,
sortKey: "market_cap"
},
price: {
header: "Price",
renderItem: row => {
let price: any = row.price;
if (!price) return "NA";
let value: number = parseFloat(parseFloat(price).toFixed(2));
if (value === 0.0) {
value = parseFloat(parseFloat(price).toFixed(4));
}
return `$${value}`;
},
sortKey: "price"
}
};
export type Props = {
widgetType?:
| "spectrum"
| "multi-table"
| "table"
| "score"
| "chart"
| "price-multi-table";
mode?: "light" | "dark";
assets?: string[];
showFullName?: boolean;
exclusions?: string[];
autoWidth?: boolean;
sortBy?: ColumnName;
limit?: number;
page?: number;
columns?: ColumnName[];
fontFamily?: string;
title?: {
text: string;
style?: object;
};
trend?: {
changeOver?: number;
};
headers?: {
style?: object;
};
rows?: {
alternating?: boolean;
alternatingColors?: string[];
dividers?: boolean;
dividersColor?: string;
style?: object;
padding?: string;
headerBold?: boolean;
};
api?: API;
};
type State = {
loading: boolean;
priceFilterRequired: boolean;
filteredColumns: ColumnName[];
pageSortBy: ColumnName;
sortColumn: string;
sortOrder: "asc" | "desc";
rows?: Row[];
};
export default class MultiTable extends Component<Props, State> {
updateInterval: any;
constructor(props: Props) {
super(props);
// if price, market_cap, or volume_24h are included in columns then remove all other columns
let filteredColumns = props.columns;
let priceFilterRequired = false;
const includedMarketCapColumns = intersection(filteredColumns, [
"price",
"market_cap",
"volume_24h"
]) as ColumnName[];
if (includedMarketCapColumns.length > 0) {
filteredColumns = includedMarketCapColumns;
priceFilterRequired = true;
} else {
if (filteredColumns.indexOf("fcas") === -1) {
filteredColumns = ["fcas", ...filteredColumns];
}
}
this.state = {
loading: true,
pageSortBy: props.sortBy || props.columns[0],
sortColumn: props.sortBy || "fcas",
sortOrder: "desc",
priceFilterRequired: priceFilterRequired,
filteredColumns
};
}
static defaultProps: Props = {
mode: "light",
limit: 10,
page: 1,
fontFamily: "inherit",
columns: [
"fcas",
"trend",
"userActivity",
"developerBehavior",
"marketMaturity",
"rank"
],
headers: {
style: {}
},
rows: {
alternating: true,
alternatingColors: [],
dividers: false,
dividersColor: null,
style: {}
},
trend: {
changeOver: 7
},
widgetType: "multi-table"
};
async componentDidMount() {
await this._getData();
this.updateInterval = setInterval(this._getData, 60000);
}
componentWillUnmount() {
clearInterval(this.updateInterval);
}
_getData = async () => {
const res = await this.props.api.fetchMetrics({
assets: this.props.assets,
exclusions: this.props.exclusions,
page: this.props.page,
size: this.props.limit,
sort_by: COLUMNS[this.state.sortColumn].sortKey,
sort_desc: true,
metrics: mapConfigColumnsNames(this.state.filteredColumns),
change_over: this.props.trend.changeOver
});
this.setState({
loading: false,
rows: res.data
});
};
handleClickSort(col: string) {
if (COLUMNS[col].sortKey) {
const sortColumn = col;
const sortOrder = this.state.sortOrder === "asc" ? "desc" : "asc";
this.setState({ sortColumn, sortOrder });
}
}
render(props: Props, state: State) {
if (state.loading) return null;
const coinColumn = props.showFullName ? "name" : "symbol";
const columns = [coinColumn, ...state.filteredColumns];
const classes = classnames("fs-multi", `fs-multi-${props.mode}`, {
"fs-multi-alternating": props.rows.alternating,
"fs-multi-dividers": props.rows.dividers
});
const sortKey = COLUMNS[state.sortColumn].sortKey;
let sortedRows = sortBy(state.rows, sortKey);
if (state.sortOrder === "desc") {
sortedRows = reverse(sortedRows);
}
const { fontFamily, widgetType } = props;
return (
<div class={classes} style={{ fontFamily }}>
<header class="fs-multi-header">
{props.title && (
<h1 class="fs-multi-title" style={props.title.style}>
{props.title.text}
</h1>
)}
<CustomLinks widget={widgetType} api={this.props.api} />
</header>
<table>
<thead>
<tr>
{columns.map(col => {
const column = COLUMNS[col];
const classes = classnames(`fs-multi-${col}`, {
"fs-multi-sortable": !!column.sortKey
});
return (
<th
class={classes}
onClick={() => this.handleClickSort(col)}
style={props.headers.style}
>
<div class="fs-multi-colhead" style={props.headers.style}>
{column.sortKey && (
<span
class={classnames(
"fs-multi-caret",
`fs-multi-caret-${state.sortOrder}`,
{
"fs-multi-caret-active": col === state.sortColumn
}
)}
/>
)}
<span class="fs-multi-colhead-text">{column.header}</span>
</div>
</th>
);
})}
</tr>
</thead>
<tbody>
{sortedRows.map(asset => (
<tr>
{columns.map(col => (
<td
class={`fs-multi-${col}`}
style={{
borderBottom: props.rows.dividers
? `1px solid ${props.rows.dividersColor}`
: null,
...props.rows.style
}}
>
{COLUMNS[col].renderItem(asset)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
}

104
src/multiTable/style.scss Normal file
View File

@ -0,0 +1,104 @@
.fs-multi {
font-size: 12px;
table {
text-align: right;
border-collapse: collapse;
width: 100%;
}
th,
td {
padding: 5px 10px;
}
th {
font-weight: 400;
vertical-align: bottom;
}
td {
font-weight: 500;
font-variant-numeric: tabular-nums;
&:first-child {
text-align: left;
color: #2d57ed;
}
}
&-light {
a,
td.fs-multi-fcas,
td.fs-multi-coin {
color: #2d57ed;
}
}
&-dark {
a,
td.fs-multi-fcas,
td.fs-multi-coin {
color: #2d57ed;
}
}
&-colhead {
display: flex;
align-items: center;
&-text {
// flex-basis: 0;
user-select: none;
}
}
&-alternating {
tbody tr:nth-child(2n-1) {
background-color: #eeeeee;
}
}
&-dividers {
td {
border-bottom: 1px solid #979797;
}
}
&-coin {
text-align: left;
}
// sorted column icon
&-caret {
display: inline-block;
background: url(./images/caret.svg);
width: 8px;
height: 6px;
margin: 0 4px 0 auto;
visibility: hidden;
&-asc {
transform: rotate(180deg);
}
&-active {
visibility: visible;
}
}
// sortable columns headers
&-sortable {
cursor: pointer;
}
&-header {
display: flex;
align-items: baseline;
justify-content: space-between;
flex-wrap: wrap;
white-space: nowrap;
}
&-title {
margin: 0 10px 10px 0;
font-size: 24px;
font-weight: 400;
}
}

36
src/multiTable/types.d.ts vendored Normal file
View File

@ -0,0 +1,36 @@
export type Row = {
symbol: string;
asset_name: string;
fcas: number;
dev: number;
utility: number;
fcas_change: number;
dev_change: number;
utility_change: number;
market_maturity: number;
market_maturity_change: number;
percent_change_7d: number;
percent_change_24h: number;
percent_change_1h: number;
market_cap: number;
price: number;
volume_24h: number;
};
// Define the columns, the content of their header, and how their data is rendered.
type ColumnDefinition = {
header: string;
renderItem: (row: Row) => any;
sortKey?: string;
};
type ColumnName =
| "fcas"
| "trend"
| "developerBehavior"
| "userActivity"
| "marketMaturity"
| "rank"
| "volume_24h"
| "market_cap"
| "price";

51
src/score/index.tsx Normal file
View File

@ -0,0 +1,51 @@
import { h } from "preact";
import withFcas, { WithFcasProps } from "../components/withFcas";
import Trend from "../components/trend";
import Rank from "../components/rank";
import * as css from "./style.css";
import CustomLinks from "../components/customLinks";
import API from "../api";
import { defaultFlipsideLink } from "../utils";
export type Props = {
symbol: string;
api: API;
} & WithFcasProps;
const Score = (props: Props) => {
const { value, percent_change } = props.fcas;
return (
<div class={css.wrapper}>
<div class={css.header}>
<h5 class={css.fcas}>FCAS</h5>
<CustomLinks
widget="score"
api={props.api}
linkClass={css.link}
style={{ flexGrow: 0 }}
/>
</div>
<div class={css.score}>
<h2 class={css.value}>{value}</h2>
<a href={defaultFlipsideLink(props.api.key, "score")}>
<Rank kind="large" score={value} />
</a>
</div>
<div class={css.change}>
7D
<Trend change={percent_change} value={value} class={css.trend} />
<span class={percent_change >= 0 ? css.up : css.down}>
{percent_change}%
</span>
</div>
</div>
);
};
Score.defaultProps = {
symbol: "btc"
};
export default withFcas(Score);

51
src/score/style.css Normal file
View File

@ -0,0 +1,51 @@
.wrapper {
text-align: right;
}
.header {
font-size: 11px;
display: flex;
align-items: center;
justify-content: flex-end;
}
.fcas {
margin: 0 10px 0 0;
font-size: 11px;
}
.link {
color: #3578a8;
font-size: 11px;
margin: 0;
}
.score {
display: flex;
align-items: center;
justify-content: flex-end;
}
.value {
font-size: 52px;
margin: 0 16px 0 0;
}
.change {
font-size: 15px;
font-weight: 600;
display: flex;
justify-content: flex-end;
}
.trend {
margin: 0 10px;
}
.up {
color: #057511;
}
.down {
color: #980000;
}

12
src/score/style.css.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
// This file is automatically generated.
// Please do not change this file!
export const change: string;
export const down: string;
export const fcas: string;
export const header: string;
export const link: string;
export const score: string;
export const trend: string;
export const up: string;
export const value: string;
export const wrapper: string;

View File

@ -0,0 +1,51 @@
import { h, Component, ComponentChildren } from "preact";
import classNames from "classnames";
import * as css from "./style.css";
type Props = {
mode: "light" | "dark";
items: any[];
renderSlide: any;
};
type State = {
currentSlide: number;
};
export default class Carousel extends Component<Props, State> {
state = {
currentSlide: 0
};
slideTo = (slide: number) => {
this.setState({ currentSlide: slide });
};
render(props: Props, state: State) {
const carouselOffset = state.currentSlide * 100;
const carouselStyle = {
transform: `translateX(-${carouselOffset}%)`
};
return (
<div class={classNames(css.wrapper, css[props.mode])}>
<div class={css.carousel} style={carouselStyle}>
{props.items.map(item => (
<div class={css.slide}>{props.renderSlide(item)}</div>
))}
</div>
{props.items.length > 1 && (
<div class={css.dots}>
{props.items.map((_, i) => {
const classes = classNames(css.dotItem, {
[css.dotActive]: state.currentSlide === i
});
return <div class={classes} onClick={() => this.slideTo(i)} />;
})}
</div>
)}
</div>
);
}
}

View File

@ -0,0 +1,46 @@
.dark .dotItem {
background: rgba(255, 255, 255, 0.25);
}
.dark .dotActive {
background: rgba(255, 255, 255, 1);
}
.light .dotItem {
background: rgba(0, 0, 0, 0.15);
}
.light .dotActive {
background: rgba(0, 0, 0, 1);
}
.wrapper {
width: 100%;
overflow: hidden;
}
.carousel {
transition: transform 250ms ease-out;
white-space: nowrap;
}
.slide {
width: 100%;
display: inline-block;
}
.dots {
display: flex;
justify-content: center;
margin-top: 10px;
}
.dotItem {
border-radius: 5px;
height: 10px;
width: 10px;
margin: 0 5px;
transition: background 250ms ease-out;
cursor: pointer;
}
.dotActive {
}

View File

@ -0,0 +1,10 @@
// This file is automatically generated.
// Please do not change this file!
export const carousel: string;
export const dark: string;
export const dotActive: string;
export const dotItem: string;
export const dots: string;
export const light: string;
export const slide: string;
export const wrapper: string;

267
src/spectrum/index.tsx Normal file
View File

@ -0,0 +1,267 @@
import { h, Component } from "preact";
import CustomLinks from "../components/customLinks";
import Plot from "./plot";
import * as css from "./style.css";
import API, { WidgetLinksLink } from "../api";
import Rank from "../components/rank";
import Trend from "../components/trend";
import Carousel from "./components/carousel";
import { defaultFlipsideLink } from "../utils";
import NoDataMessage from "../components/noDataMessage";
import find from "lodash/find";
export type BootstrapAssetType = {
value: number;
percent_change: number;
asset_name: string;
};
type BootstrapHighlightType = {
symbol: string;
value: number;
};
export type AssetType = {
asset_id?: string;
symbol?: string;
highlights?: string[];
fullDistribution?: boolean;
bootstrapAsset?: BootstrapAssetType;
bootstrapHighlights?: BootstrapHighlightType[];
};
export type Props = {
asset?: AssetType;
assets?: AssetType[];
mode?: "light" | "dark";
fontFamily?: string;
autoWidth?: boolean;
showHeader?: boolean;
relatedMarkers?: {
bucketDistance?: number;
lineDistance?: number;
enabled?: boolean;
color?: string;
fontFamily?: string;
};
icon?: { enabled?: boolean };
name?: { enabled?: boolean; style?: object };
rank?: { enabled?: boolean };
spectrum?: { enabled: boolean };
trend?: { enabled: boolean };
api?: API;
bootstrapFCASDistribution?: any;
disableLinks?: boolean;
linkBootstrap?: WidgetLinksLink[];
autoRefresh?: boolean;
};
type State = {
loading: boolean;
data?: {
value: number;
symbol: string;
slug: string;
grade?: string;
percent_change: number;
point_change?: number;
asset_name: string;
has_rank: boolean;
};
metric: {
name: string;
fcas: number;
change: number;
};
widgetLinks: WidgetLinksLink[];
};
class Spectrum extends Component<Props, State> {
interval: number;
static defaultProps = {
mode: "light",
autoRefresh: true,
};
constructor() {
super();
this.state = { loading: true, metric: null, widgetLinks: [] };
}
async _getData() {
let data: BootstrapAssetType;
let success: boolean;
const assetId = this.props.asset.asset_id || this.props.asset.symbol;
if (!this.props.asset.bootstrapAsset) {
let result = await this.props.api.fetchAssetMetric(assetId, "FCAS");
data = result.data;
success = result.success;
} else {
data = this.props.asset.bootstrapAsset;
success = true;
}
let widgetLinks;
if (this.props.linkBootstrap) {
widgetLinks = this.props.linkBootstrap;
} else {
let widgetLinksResp = await this.props.api.fetchWidgetLinks("spectrum");
widgetLinks = widgetLinksResp.data;
}
if (!success || !data) {
setTimeout(() => {
return this._getData();
}, 120000);
return success;
}
this.setState({
loading: false,
data: data as any,
metric: {
fcas: Math.round(data.value),
change: data.percent_change,
name: data.asset_name,
},
widgetLinks: widgetLinks,
});
return success;
}
_update() {
if (this.props.autoRefresh !== true) {
return;
}
this.interval = window.setInterval(async () => {
await this._getData();
}, 90000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
async componentDidMount() {
const success = await this._getData();
if (!success) {
this.setState({
loading: false,
metric: {
name: "NA",
fcas: 0,
change: 0,
},
});
}
this._update();
}
render(props: Props, state: State) {
if (state.loading) return null;
if (!state.data) return <NoDataMessage />;
const { mode, rank, trend, api } = props;
const { metric, data } = state;
const fcas = Math.round(data.value);
let scoreLink = find(state.widgetLinks, { name: "score_link" });
if (!scoreLink) {
scoreLink = {
widget_id: "",
name: "score_link",
link_html: defaultFlipsideLink(api.key, "spectrum"),
};
}
return (
<div class={css[mode]}>
{props.showHeader && (
<div class={css.header}>
<img
class={css.icon}
src={`https://d301yvow08hyfu.cloudfront.net/svg/color/${data.symbol.toLowerCase()}.svg`}
/>
<span class={css.name}>{data.asset_name}</span>
</div>
)}
{props.showHeader && (
<div class={css.meta}>
<span class={css.symbol}>{data.symbol}</span>
<span class={css.fcas}>Health {fcas}</span>
{trend.enabled && (
<span class={css.trend}>
<Trend pointChange={data.point_change} value={fcas} />
</span>
)}
{rank.enabled && data.has_rank && (
<a href={scoreLink.link_html}>
<span class={css.rank}>
<Rank score={fcas} grade={data.grade} kind="normal" />
</span>
</a>
)}
</div>
)}
{props.spectrum.enabled && (
<Plot metric={metric} {...props} symbol={data.symbol} />
)}
{props.disableLinks === false && (
<CustomLinks
widget="spectrum"
api={props.api}
linkBootstrap={state.widgetLinks}
/>
)}
</div>
);
}
}
const MultiSpectrum = (props: Props) => {
let assets = [props.asset];
if (props.assets.length > 0) {
assets = props.assets;
}
return (
<Carousel
mode={props.mode}
items={assets}
renderSlide={(item: any) => <Spectrum {...props} asset={item} />}
/>
);
};
MultiSpectrum.defaultProps = {
asset: {
symbol: "btc",
highlights: ["eth", "zec", "zrx"],
bootstrapAsset: null,
bootstrapHighlights: null,
},
disableLinks: false,
assets: [],
mode: "light",
fontFamily: "inherit",
showHeader: true,
relatedMarkers: {
enabled: true,
bucketDistance: 35,
lineDistance: 25,
fontFamily: "inherit",
},
name: { enabled: true },
spectrum: { enabled: true },
icon: { enabled: true },
rank: { enabled: true },
trend: { enabled: true },
linkBootstrap: null,
} as Props;
export default MultiSpectrum;

342
src/spectrum/plot/index.tsx Normal file
View File

@ -0,0 +1,342 @@
import { h, Component } from "preact";
import { sortObjectArray } from "../../utils";
import classNames from "classnames";
import * as css from "./style.css";
import { score } from "../../score/style.css";
const PLOT_WIDTH = 240;
const PLOT_SCALE = PLOT_WIDTH / 1000;
const DEFAULT_BUCKET_DISTANCE = 35;
const DEFAULT_LINE_DISTANCE = 25;
// TODO: Port this component to TS
// type Props = {
// mode: "light" | "dark"
// }
type Props = {
mode: "light" | "dark";
symbol: string;
autoRefresh?: boolean;
} & any;
export default class Plot extends Component<Props, any> {
interval: any;
constructor() {
super();
this.state = {
loading: true,
distribution: null,
highlights: [],
highlightedSymbols: [],
};
}
async _getData() {
let data;
let success;
const fullDistribution = true
? this.props.asset.fullDistribution == true
: false;
if (!this.props.bootstrapFCASDistribution) {
let result = await this.props.api.fetchFCASDistribution(fullDistribution);
data = result.data;
success = result.success;
} else {
data = this.props.bootstrapFCASDistribution;
success = true;
}
if (!success || !data) {
setTimeout(() => {
return this._getData();
}, 2000);
return success;
}
if (data && data.length > 0) {
this.setState({
loading: false,
distribution: data,
});
}
return success;
}
async _getHighlights() {
const highlights: any[] = this.getHighlights();
let nextHighlightState = [];
let nextHighlightedSymbolState = [];
if (this.props.asset && this.props.asset.bootstrapHighlights) {
nextHighlightState = this.props.asset.bootstrapHighlights;
nextHighlightedSymbolState = this.props.asset.bootstrapHighlights.map(
(highlight: any) => highlight.symbol
);
} else {
await Promise.all(
highlights.map(async (asset) => {
const { data, success } = await this.props.api.fetchAssetMetric(
asset,
"fcas"
);
if (success === true && data) {
nextHighlightState.push(data);
nextHighlightedSymbolState.push(data.symbol);
}
})
);
}
if (nextHighlightState.length < this.state.highlights.length) {
return;
}
this.setState({
highlights: nextHighlightState,
highlightedSymbols: nextHighlightedSymbolState,
});
}
_update() {
if (this.props.autoRefresh !== true) {
return;
}
this.interval = setInterval(() => {
this._getData();
}, 300000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
async componentDidMount() {
this._getHighlights();
const success = await this._getData();
if (!success) {
this.setState({
loading: false,
distribution: [],
});
}
this._update();
}
getHighlights() {
let { asset } = this.props;
let { highlights } = asset;
let assetSymbol = this.props.symbol.toLowerCase();
if (highlights && highlights.length > 0) {
return highlights;
}
highlights = [];
if (assetSymbol == "eth" || assetSymbol == "btc") {
highlights = ["ZEC", "XRP"];
} else {
highlights = ["BTC"];
}
return highlights;
}
getBuckets(): any {
if (this.state.highlights.length == 0) {
return [];
}
let { bucketDistance } = this.props.relatedMarkers;
if (!bucketDistance) {
bucketDistance = DEFAULT_BUCKET_DISTANCE;
}
let buckets: any[] = [];
let currentBucketIndex = 0;
let anchorX = 0;
let scoresToBuckets: any = {};
let highlightLength = this.state.highlights.length;
let sortedHighLights = sortObjectArray(this.state.highlights, "value");
for (let i = 0; i < highlightLength; i++) {
let currentAsset = sortedHighLights[i];
// If first item
if (i === 0) {
buckets[currentBucketIndex] = [];
buckets[currentBucketIndex].push(currentAsset);
scoresToBuckets[currentAsset.value] = currentBucketIndex;
anchorX = currentAsset.value;
continue;
}
const nextAsset =
i !== highlightLength - 1 ? sortedHighLights[i + 1] : null;
const prevDist = Math.abs(anchorX - currentAsset.value);
if (prevDist <= bucketDistance) {
buckets[currentBucketIndex].push(currentAsset);
scoresToBuckets[currentAsset.value] = currentBucketIndex;
} else {
currentBucketIndex++;
buckets[currentBucketIndex] = [];
buckets[currentBucketIndex].push(currentAsset);
scoresToBuckets[currentAsset.value] = currentBucketIndex;
anchorX = currentAsset.value;
}
}
return { buckets, scoresToBuckets };
}
getYCoords(asset: any, buckets: any, scoresToBuckets: any) {
let { lineDistance } = this.props.relatedMarkers;
if (!lineDistance) {
lineDistance = DEFAULT_LINE_DISTANCE;
}
let bucketIndex = scoresToBuckets[asset.value];
if (bucketIndex == null || bucketIndex == undefined) return;
let bucket = buckets[bucketIndex];
let index = 0;
let toClose = false;
for (let i = 0; i < bucket.length; i++) {
let ba = bucket[i];
if (ba.symbol == asset.symbol) {
index = i;
if (i === 0) {
break;
}
const prevAsset = bucket[i - 1];
if (prevAsset && Math.abs(prevAsset.value - ba.value) <= lineDistance) {
toClose = true;
}
break;
}
}
return { y: 44 - 10 * index, toClose };
}
render(props: any, { loading, distribution }: any) {
if (loading) return null;
const highlightedSymbols = this.state.highlightedSymbols;
const highlights = this.state.highlights;
distribution = [...distribution, ...highlights];
const xPos = `${(props.metric.fcas / 1000) * 100}%`;
const dupes: any = [];
const highlightedAssets = distribution
.filter((i: any) => highlightedSymbols.indexOf(i.symbol) > -1)
.filter((i: any) => i.symbol != props.symbol.toUpperCase())
.filter((i: any) => {
let inList = dupes.indexOf(i.symbol) == -1;
dupes.push(i.symbol);
return inList;
});
const { buckets, scoresToBuckets } = this.getBuckets();
let relatedLabelStyle = {};
let relatedLineStyle = {};
if (props.relatedMarkers) {
relatedLabelStyle = {
fill: props.relatedMarkers.color,
fontFamily: props.relatedMarkers.fontFamily,
};
relatedLineStyle = {
stroke: props.relatedMarkers.color,
};
}
// @ts-ignore
const classes = classNames(css.wrapper, css[props.mode]);
return (
<svg width="100%" height="104" overflow="visible" class={classes}>
<defs>
<linearGradient id="gradient">
<stop stop-color="#ff2600" offset="0%" />
<stop stop-color="#ff7a18" offset="40%" />
<stop stop-color="#8fcb89" offset="68%" />
<stop stop-color="#30a92d" offset="92%" />
</linearGradient>
</defs>
<g fill={props.mode === "dark" ? "#fff" : "#000"}>
<circle cx="3" cy="44" r="2.5" />
<text x="9" y="47" font-size="8">
Coins
</text>
</g>
{/* Distribution Dots */}
<g
fill={
props.mode === "dark"
? "rgba(255, 255,255, 0.5)"
: "rgba(0, 0, 0, 0.4)"
}
>
{distribution.map((i: any) => (
<circle cx={`${(i.value / 1000) * 100}%`} cy="58" r="2.5" />
))}
</g>
{/* Gradient Line */}
<rect x="0" y="64" width="100%" height="6" fill="url(#gradient)" />
{/* Spectrum Legend */}
<text
y="85"
class={css.legend}
fill={props.mode === "dark" ? "#fff" : "#000"}
>
<tspan text-anchor="start" x="0">
0
</tspan>
<tspan text-anchor="middle" x="50%">
500
</tspan>
<tspan text-anchor="end" x="100%">
1000
</tspan>
</text>
{props.relatedMarkers.enabled &&
highlightedAssets.map((a: any) => {
const xPos = `${(a.value / 1000) * 100}%`;
let result = this.getYCoords(a, buckets, scoresToBuckets);
if (!result) return;
let { y, toClose } = result;
return (
<g class={css.related} style={relatedLabelStyle}>
<text x={xPos} y={y} text-anchor="middle" font-size="10">
{a.symbol}
</text>
{toClose === false && (
<line
x1={xPos}
y1={y + 3}
x2={xPos}
y2="60"
class={css.relatedLine}
style={relatedLineStyle}
/>
)}
</g>
);
})}
{/* Blue FCAS Marker */}
<text class={css.marker} text-anchor="middle" font-weight="bold">
<tspan x={xPos} y={26}>
{props.symbol.toUpperCase()}
</tspan>
</text>
{/* Blue FCAS Marker Line */}
<line x1={xPos} y1={28} x2={xPos} y2={60} class={css.line} />
</svg>
);
}
}

View File

@ -0,0 +1,47 @@
.light .marker {
fill: #2d57ed;
}
.light .line {
stroke: #2d57ed;
}
.light .related {
fill: #000;
}
.light .relatedLine {
stroke: #000;
}
.dark .marker {
fill: #20b7fc;
}
.dark .line {
stroke: #20b7fc;
}
.dark .related {
fill: #fff;
}
.dark .relatedLine {
stroke: #fff;
}
.wrapper text {
font-size: 11px;
font-weight: normal;
}
.marker {
font-size: 12px;
}
.line {
stroke-width: 1;
}
.relatedLine {
stroke-width: 0.5;
}
.legend tspan {
font-weight: normal;
font-size: 8px;
}

10
src/spectrum/plot/style.css.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
// This file is automatically generated.
// Please do not change this file!
export const dark: string;
export const legend: string;
export const light: string;
export const line: string;
export const marker: string;
export const related: string;
export const relatedLine: string;
export const wrapper: string;

61
src/spectrum/style.css Normal file
View File

@ -0,0 +1,61 @@
.light .meta,
.light a {
color: #2d57ed;
}
.light .name {
color: #000;
}
.dark .meta,
.dark a {
color: #20b7fc;
}
.dark .name {
color: #fff;
}
.header {
display: flex;
align-items: center;
margin-bottom: 24px;
}
.icon {
height: 28px;
width: 28px;
margin-right: 14px;
}
.name {
font-size: 24px;
font-weight: normal;
}
.meta {
display: flex;
align-items: center;
text-transform: uppercase;
margin-bottom: 16px;
font-size: 16px;
font-weight: normal;
}
.symbol {
margin-right: auto;
}
.fcas {
margin: 0 6px;
}
.rank {
margin-left: 9px;
}
.trend {
font-size: 10px;
margin-left: 8px;
font-weight: 600;
}

12
src/spectrum/style.css.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
// This file is automatically generated.
// Please do not change this file!
export const dark: string;
export const fcas: string;
export const header: string;
export const icon: string;
export const light: string;
export const meta: string;
export const name: string;
export const rank: string;
export const symbol: string;
export const trend: string;

View File

@ -0,0 +1,62 @@
import { h, Component, render } from "preact";
import "./style.scss";
import API, { WidgetLinksLink } from "../../../api";
import find from "lodash/find";
type Props = {
widget: "spectrum" | "multi-table" | "table";
api: API;
};
type State = {
links: WidgetLinksLink[];
isLoading: boolean;
intervalId: any;
};
class CustomLinks extends Component<Props, State> {
state: State = {
links: [],
isLoading: true,
intervalId: null
};
async getData() {
const res = await this.props.api.fetchWidgetLinks(this.props.widget);
this.setState({ links: res.data, isLoading: false });
}
componentWillUnmount() {
clearInterval(this.state.intervalId);
}
componentDidMount() {
this.getData();
const intervalId = setInterval(() => {
this.getData();
}, 60000);
this.setState({ intervalId: intervalId });
}
render(_: Props, state: State) {
if (this.state.isLoading) return <div />;
const bottomLink: any = find(state.links, { name: "bottom_link" });
if (!bottomLink || (bottomLink && !bottomLink.link_html)) {
return (
<div class="fs-bottom-link">
<a href="https://flipsidecrypto.com">
Want to know more about these scores?
</a>
</div>
);
}
return (
<div
class="fs-bottom-link"
dangerouslySetInnerHTML={{ __html: bottomLink.link_html }}
/>
);
}
}
export default CustomLinks;

View File

@ -0,0 +1,11 @@
.fs-bottom-link {
line-height: 40px;
a {
color: inherit;
text-decoration: underline;
cursor: pointer;
&:hover {
cursor: pointer;
}
}
}

View File

@ -1,8 +1,10 @@
import { h, Component } from "preact";
import keyBy from "lodash/keyBy";
import CustomLinks from "./components/customLinks";
import "./styles.scss";
import API from "../api";
function getMetricTrend(change) {
function getMetricTrend(change: number) {
if (change < 0) {
return "down";
} else if (change == 0) {
@ -12,11 +14,23 @@ function getMetricTrend(change) {
}
}
function calculateDiff(value, percent) {
function calculateDiff(value: number, percent: number) {
return Math.round(Math.abs(value * (percent / 100)));
}
export default class Table extends Component {
type Props = {
api: API;
symbol: string;
dark: boolean;
borderColor?: string;
};
type State = {
loading: boolean;
metrics: any;
};
export default class Table extends Component<Props, State> {
constructor() {
super();
this.state = { loading: true, metrics: null };
@ -43,13 +57,11 @@ export default class Table extends Component {
}
onClickLearnMore() {
const learnMoreUrl = `https://platform-api.flipsidecrypto.com/track/table-widget/${
this.props.api.key
}`;
const learnMoreUrl = `https://api.flipsidecrypto.com/track/table-widget/${this.props.api.key}`;
window.location.assign(learnMoreUrl);
}
render({ dark }, { loading, metrics }) {
render({ dark }: Props, { loading, metrics }: State) {
if (loading) {
return null;
}
@ -77,7 +89,7 @@ export default class Table extends Component {
<div class={`fs-table fs-table--${dark ? "dark" : "light"}`}>
<table>
<tr class="fs-table-fcas">
<th style={tdStyle}>FCAS</th>
<th style={tdStyle}>Project Health</th>
<td style={tdStyle}>
<span class={`fs-table-rank fs-table-rank--${rank}`}>{rank}</span>
</td>
@ -93,7 +105,7 @@ export default class Table extends Component {
</tr>
<tr>
<th style={tdStyle} colspan="2">
<th style={tdStyle} colSpan={2}>
User Activity
</th>
<td style={tdStyle}>{utility.value}</td>
@ -106,7 +118,7 @@ export default class Table extends Component {
</tr>
<tr>
<th style={tdStyle} colspan="2">
<th style={tdStyle} colSpan={2}>
Developer Behavior
</th>
<td style={tdStyle}>{dev.value}</td>
@ -118,9 +130,7 @@ export default class Table extends Component {
</td>
</tr>
</table>
<a class="fs-table-link" onClick={this.onClickLearnMore.bind(this)}>
Want to know more about these scores?
</a>
<CustomLinks api={this.props.api} widget={"table"} />
</div>
);
}

View File

@ -112,13 +112,3 @@
color: #808080;
}
}
.fs-table-link {
line-height: 40px;
color: inherit;
text-decoration: underline;
cursor: pointer;
&:hover {
cursor: pointer;
}
}

View File

@ -1,17 +0,0 @@
export const compare = key => {
return (a, b) => {
const valueA = a[key];
const valueB = b[key];
let comparison = 0;
if (valueA > valueB) {
comparison = 1;
} else if (valueA < valueB) {
comparison = -1;
}
return comparison;
};
};
export const sortObjectArray = (array, key) => {
return array.sort(compare(key));
};

35
src/utils.ts Normal file
View File

@ -0,0 +1,35 @@
import isArray from "lodash/isArray";
import mergeWith from "lodash/mergeWith";
export const compare = (key: string) => {
return (a: any, b: any) => {
const valueA = a[key];
const valueB = b[key];
let comparison = 0;
if (valueA > valueB) {
comparison = 1;
} else if (valueA < valueB) {
comparison = -1;
}
return comparison;
};
};
export const sortObjectArray = (array: any[], key: string) => {
return array.sort(compare(key));
};
export function defaultsWithoutArrays(obj: object, src: object): object {
return mergeWith({}, obj, src, (objVal, srcVal) => {
return isArray(srcVal) ? srcVal : undefined;
});
}
export function defaultFlipsideLink(apiKey: string, widget: string) {
return `https://api.flipsidecrypto.com/track/${widget}/${apiKey}?redirect_url=https://flipsidecrypto.com/go-beyond-price?utm_medium=widget&utm_campaign=${widget}_widget&utm_content=letter_grade`;
}
export function countDecimals(value: number) {
if (Math.floor(value) === value) return 0;
return value.toString().split(".")[1].length || 0;
}

39971
stats.json Normal file

File diff suppressed because one or more lines are too long

15
tsconfig.json Normal file
View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"outDir": "./dist/",
"moduleResolution": "node",
"noImplicitAny": true,
"sourceMap": true,
"module": "esnext",
"target": "es5",
"lib": ["es2015", "dom"],
"jsx": "react",
"allowJs": true,
"jsxFactory": "h",
"esModuleInterop": true
}
}

View File

@ -1,18 +1,37 @@
const path = require("path");
let version = require("./package.json").version;
let filename = `flipside-v${version}.js`;
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
const version = require("./package.json").version;
const PUBLIC_PATH =
process.env.NODE_ENV === "development"
? "/"
: "https://d3sek7b10w79kp.cloudfront.net/";
module.exports = {
entry: "./src/index.js",
entry: "./src/index.tsx",
output: {
filename: filename,
path: path.resolve(__dirname, "dist")
filename: `flipside-v${version}.js`,
path: path.resolve(__dirname, "dist"),
publicPath: PUBLIC_PATH
},
mode: "development",
resolve: {
extensions: [".tsx", ".ts", ".js"]
},
// plugins: [new BundleAnalyzerPlugin()],
module: {
rules: [
{ test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" },
{ test: /\.tsx?$/, use: "ts-loader", exclude: /node_modules/ },
{ test: /\.js$/, loader: "babel-loader", exclude: /node_modules/ },
{ test: /\.scss$/, use: ["style-loader", "css-loader", "sass-loader"] },
{
test: /\.css$/,
use: [
"style-loader",
"css-modules-typescript-loader",
{ loader: "css-loader", options: { modules: true } }
]
},
{ test: /\.svg$/, use: { loader: "url-loader" } }
]
}

5318
yarn-error.log Normal file

File diff suppressed because it is too large Load Diff

1906
yarn.lock

File diff suppressed because it is too large Load Diff