Compare commits
119 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
837ff75f7e | ||
|
|
135f6f9a6b | ||
|
|
40a16998b2 | ||
|
|
23a7c4953a | ||
|
|
4844f92206 | ||
|
|
730f24a797 | ||
|
|
b2bbc6b955 | ||
|
|
56cd6f206a | ||
|
|
cde8ae754c | ||
|
|
1326e490a7 | ||
|
|
1d980b3ce6 | ||
|
|
4318267c78 | ||
|
|
b038c1c427 | ||
|
|
651b19c8e6 | ||
|
|
866f129b9a | ||
|
|
99b05e6c39 | ||
|
|
92208fad3e | ||
|
|
95e2d7c262 | ||
|
|
3e5494daf7 | ||
|
|
6d885476dc | ||
|
|
c8d2400ffd | ||
|
|
f9e4d0a4f6 | ||
|
|
2b48937ac7 | ||
|
|
e17d873d08 | ||
|
|
c8f7055567 | ||
|
|
3945d911c5 | ||
|
|
f283cc84a3 | ||
|
|
2d51db81ec | ||
|
|
124d14c2a1 | ||
|
|
c0268f4f40 | ||
|
|
2fb5d3da4e | ||
|
|
1fdb9926e0 | ||
|
|
210dc70234 | ||
|
|
a038c9bb4f | ||
|
|
1460964b33 | ||
|
|
6f05f7fe4b | ||
|
|
6d7b4d2c88 | ||
|
|
eab5a73255 | ||
|
|
3378e5625c | ||
|
|
744681e3f8 | ||
|
|
a6d7a2d2a6 | ||
|
|
942fe5a7f4 | ||
|
|
953107687e | ||
|
|
5010dfd2ef | ||
|
|
e3ee0fd590 | ||
|
|
f4e0da2d8d | ||
|
|
b8abd9203f | ||
|
|
3c5d940da6 | ||
|
|
e6c9840ecb | ||
|
|
33e93fcb3d | ||
|
|
95c1c69912 | ||
|
|
e48a219547 | ||
|
|
24543e4231 | ||
|
|
a0c8c1a503 | ||
|
|
518a20289d | ||
|
|
194a1822be | ||
|
|
9413808bdf | ||
|
|
5ae203dde5 | ||
|
|
3c8fd0f326 | ||
|
|
68796ce9bb | ||
|
|
0411a6742d | ||
|
|
cbb8040d4c | ||
|
|
7f979af89f | ||
|
|
d611b38729 | ||
|
|
b67904ad78 | ||
|
|
e0f0bfc748 | ||
|
|
b939b5d704 | ||
|
|
f4d11d76d3 | ||
|
|
f60139c50c | ||
|
|
c949b0bda5 | ||
|
|
88d736fda3 | ||
|
|
e3b3e5985e | ||
|
|
8120e14533 | ||
|
|
15291d473c | ||
|
|
2b3d98520e | ||
|
|
8dd894dbec | ||
|
|
6c351d1c28 | ||
|
|
256a9fe375 | ||
|
|
d5b5b1fbdc | ||
|
|
5af454cafa | ||
|
|
429fa530aa | ||
|
|
22fdaa0bb2 | ||
|
|
90131a8b83 | ||
|
|
102e230380 | ||
|
|
7b43d6654d | ||
|
|
cf0a48879b | ||
|
|
6d48df3f62 | ||
|
|
8144c9fdb3 | ||
|
|
731aafae7b | ||
|
|
9a858c1a7f | ||
|
|
6795823cd8 | ||
|
|
7d6db5c8e1 | ||
|
|
6c41d15d66 | ||
|
|
8db0a87d79 | ||
|
|
3c895b6e51 | ||
|
|
ea8acb6e1e | ||
|
|
cf5f308ef1 | ||
|
|
3f553a851d | ||
|
|
5d84f1d2d4 | ||
|
|
8281686462 | ||
|
|
eac27d5d4f | ||
|
|
ac037b5939 | ||
|
|
eaf4f49b1c | ||
|
|
8632519e8d | ||
|
|
c7aaf54f3e | ||
|
|
56987cf57f | ||
|
|
6930f317be | ||
|
|
f0b2c4b223 | ||
|
|
4964a05242 | ||
|
|
ad4e464ced | ||
|
|
dc6f9a687a | ||
|
|
7d422574f5 | ||
|
|
95fe9498b2 | ||
|
|
7dff56444c | ||
|
|
ed5fef708b | ||
|
|
3e6f2f3ea2 | ||
|
|
112a078ec4 | ||
|
|
63b6602f49 | ||
|
|
7ec67a14d5 |
3
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
.vscode/
|
||||
.DS_STORE
|
||||
node_modules/
|
||||
dist/
|
||||
dist/
|
||||
.log
|
||||
|
||||
@ -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
|
||||
|
||||
```
|
||||
|
||||
259
index.html
@ -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
28
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
47
src/api.js
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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;
|
||||
152
src/components/customLinks/index.tsx
Normal 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;
|
||||
24
src/components/customLinks/style.css
Normal 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;
|
||||
}
|
||||
4
src/components/customLinks/style.css.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
export const link: string;
|
||||
export const wrapper: string;
|
||||
6
src/components/noDataMessage/index.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import { h } from "preact";
|
||||
import * as css from "./style.css";
|
||||
|
||||
const NoDataMessage = () => <div></div>;
|
||||
|
||||
export default NoDataMessage;
|
||||
5
src/components/noDataMessage/style.css
Normal file
@ -0,0 +1,5 @@
|
||||
.wrapper {
|
||||
font-size: 14px !important;
|
||||
font-weight: normal;
|
||||
white-space: normal;
|
||||
}
|
||||
3
src/components/noDataMessage/style.css.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
export const wrapper: string;
|
||||
15
src/components/rank/images/A.svg
Normal 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 |
15
src/components/rank/images/B.svg
Normal 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 |
15
src/components/rank/images/C.svg
Normal 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 |
15
src/components/rank/images/F.svg
Normal 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 |
15
src/components/rank/images/S.svg
Normal 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 |
62
src/components/rank/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
52
src/components/rank/style.css
Normal 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
@ -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;
|
||||
9
src/components/trend/images/down.svg
Normal 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 |
9
src/components/trend/images/up.svg
Normal 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 |
48
src/components/trend/index.tsx
Normal 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;
|
||||
18
src/components/trend/style.css
Normal 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
@ -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;
|
||||
57
src/components/withFcas/index.tsx
Normal 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
@ -0,0 +1,8 @@
|
||||
declare module "*.css" {
|
||||
const css: any;
|
||||
export default css;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
Highcharts: any;
|
||||
}
|
||||
64
src/dynamic/index.tsx
Normal 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
@ -0,0 +1,4 @@
|
||||
declare module 'load-js' {
|
||||
const fn: any;
|
||||
export default fn;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
.fs-plot__blue {
|
||||
fill: #2d57ed;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.fs-plot__x {
|
||||
font-size: 7px;
|
||||
}
|
||||
@ -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 Flipside’s 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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 |
@ -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 |
@ -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 |
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
41
src/index.js
@ -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
@ -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;
|
||||
13
src/multiTable/images/caret.svg
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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;
|
||||
51
src/spectrum/components/carousel/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
46
src/spectrum/components/carousel/style.css
Normal 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 {
|
||||
}
|
||||
10
src/spectrum/components/carousel/style.css.d.ts
vendored
Normal 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
@ -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
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
47
src/spectrum/plot/style.css
Normal 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
@ -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
@ -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
@ -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;
|
||||
62
src/table/components/customLinks/index.tsx
Normal 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;
|
||||
11
src/table/components/customLinks/style.scss
Normal file
@ -0,0 +1,11 @@
|
||||
.fs-bottom-link {
|
||||
line-height: 40px;
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -112,13 +112,3 @@
|
||||
color: #808080;
|
||||
}
|
||||
}
|
||||
|
||||
.fs-table-link {
|
||||
line-height: 40px;
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
17
src/utils.js
@ -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
@ -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
15
tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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" } }
|
||||
]
|
||||
}
|
||||
|
||||