mirror of
https://github.com/FlipsideCrypto/flipside-js.git
synced 2026-02-06 18:56:43 +00:00
Compare commits
68 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 | ||
|
|
d611b38729 | ||
|
|
b67904ad78 | ||
|
|
f4d11d76d3 | ||
|
|
15291d473c | ||
|
|
2b3d98520e | ||
|
|
8dd894dbec | ||
|
|
6c351d1c28 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
.vscode/
|
||||
.DS_STORE
|
||||
node_modules/
|
||||
dist/
|
||||
dist/
|
||||
.log
|
||||
|
||||
@ -8,14 +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 Spectrum Widget Example](https://jsfiddle.net/flipsidejim/edk823fb/)
|
||||
<br>
|
||||
[View Live Multi Table Widget Example](https://jsfiddle.net/flipsidejim/hafj7drm/)
|
||||
<br>
|
||||
[View Live Table Widget Example](https://jsfiddle.net/flipsidejim/vsh5dq9y/)
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
|
||||
197
index.html
197
index.html
@ -5,7 +5,7 @@
|
||||
<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.9.0.js"></script>
|
||||
<script src="flipside-v1.16.1.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
@ -13,6 +13,7 @@
|
||||
"Segoe UI Symbol";
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: #fff !important;
|
||||
}
|
||||
|
||||
text {
|
||||
@ -21,14 +22,15 @@
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
border: 1px solid #ccc;
|
||||
padding: 20px;
|
||||
width: 500px;
|
||||
width: 1080px;
|
||||
height: 800px;
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
}
|
||||
.frameWidget {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.wrapper p {
|
||||
font-size: 40px;
|
||||
@ -44,11 +46,29 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<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"><div id="widget-0" style="width: 289px"></div></div>
|
||||
|
||||
<div class="wrapper" style="background-color:#000000">
|
||||
<div class="wrapper" style="background-color: #000000">
|
||||
<div id="widget-1" style="width: 289px"></div>
|
||||
</div>
|
||||
|
||||
@ -57,13 +77,88 @@
|
||||
</div>
|
||||
|
||||
<div class="wrapper"><div id="multiTable"></div></div>
|
||||
<div class="wrapper">
|
||||
<div id="multiTableFcas"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
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.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: {
|
||||
symbol: "nmr",
|
||||
highlights: ["BTC", "ETH", "QTUM"]
|
||||
asset_id: 1,
|
||||
highlights: ["BTC", "ETH", "QTUM"],
|
||||
},
|
||||
mode: "light",
|
||||
fontFamily: "inherit",
|
||||
@ -71,61 +166,101 @@
|
||||
enabled: true,
|
||||
bucketDistance: 35,
|
||||
lineDistance: 25,
|
||||
fontFamily: "inherit"
|
||||
fontFamily: "inherit",
|
||||
},
|
||||
showHeader: false,
|
||||
name: { enabled: true, style: {} },
|
||||
spectrum: { enabled: true },
|
||||
icon: { enabled: true },
|
||||
rank: { enabled: true },
|
||||
trend: { enabled: true }
|
||||
trend: { enabled: true },
|
||||
});
|
||||
|
||||
flipside.spectrum("widget-1", {
|
||||
assets: [
|
||||
{
|
||||
symbol: "eth",
|
||||
highlights: ["BTC", "NMR", "QTUM"]
|
||||
asset_id: 1,
|
||||
highlights: ["BTC", "NMR", "QTUM"],
|
||||
},
|
||||
{
|
||||
symbol: "xrp",
|
||||
highlights: ["BTC", "NMR", "QTUM"]
|
||||
asset_id: 2,
|
||||
highlights: ["BTC", "NMR", "QTUM"],
|
||||
},
|
||||
{
|
||||
symbol: "btc",
|
||||
highlights: ["eos", "NMR", "QTUM"]
|
||||
}
|
||||
],
|
||||
mode: "dark",
|
||||
bucketDistance: 100,
|
||||
showHeader: false,
|
||||
relatedMarkers: {
|
||||
bucketDistance: 55,
|
||||
lineDistance: 35
|
||||
}
|
||||
lineDistance: 35,
|
||||
},
|
||||
});
|
||||
|
||||
flipside.createTable("widget-2", "btc", {
|
||||
dark: true,
|
||||
borderColor: "#737e8d"
|
||||
borderColor: "#737e8d",
|
||||
});
|
||||
|
||||
// Price table
|
||||
flipside.multiTable("multiTable", {
|
||||
columns: ["rank", "trend"],
|
||||
exclusions: ["gas", "TRX"],
|
||||
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: 15,
|
||||
page: 1,
|
||||
limit: 10,
|
||||
mode: "light",
|
||||
rows: {
|
||||
dividers: true
|
||||
alternating: true,
|
||||
alternatingColors: "#eeeeee",
|
||||
dividers: false,
|
||||
},
|
||||
title: {
|
||||
text: "Top Coins",
|
||||
style: { fontSize: "24px", fontWeight: 400 }
|
||||
style: {},
|
||||
},
|
||||
trend: {
|
||||
changeOver: 7
|
||||
}
|
||||
enabled: true,
|
||||
changeOver: 14,
|
||||
style: {},
|
||||
},
|
||||
});
|
||||
|
||||
flipside.score("score", { symbol: "eth" });
|
||||
|
||||
4333
package-lock.json
generated
4333
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "flipside-js",
|
||||
"version": "1.9.0",
|
||||
"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": {
|
||||
"start": "webpack-dev-server",
|
||||
"start": "NODE_ENV=development webpack-dev-server",
|
||||
"build": "webpack -p --env production",
|
||||
"build:stats": "webpack -p --env production --json > stats.json"
|
||||
},
|
||||
@ -13,28 +13,32 @@
|
||||
"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/lodash": "^4.14.120",
|
||||
"@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",
|
||||
"css-modules-typescript-loader": "^1.1.1",
|
||||
"node-sass": "^4.10.0",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
60
src/api.ts
60
src/api.ts
@ -7,8 +7,8 @@ export default class API {
|
||||
constructor(apiKey: string) {
|
||||
this.key = apiKey;
|
||||
this.client = axios.create({
|
||||
baseURL: "https://platform-api.flipsidecrypto.com/api/v1",
|
||||
params: { api_key: apiKey }
|
||||
baseURL: "https://api.flipsidecrypto.com/api/v1",
|
||||
params: { api_key: apiKey },
|
||||
});
|
||||
}
|
||||
|
||||
@ -17,14 +17,14 @@ export default class API {
|
||||
url: string,
|
||||
params = {},
|
||||
retryCount = 0,
|
||||
retryMax = 15
|
||||
retryMax = 1
|
||||
): Promise<any> {
|
||||
let res;
|
||||
try {
|
||||
res = await this.client.request({
|
||||
url,
|
||||
method,
|
||||
params: params
|
||||
params: params,
|
||||
});
|
||||
if (res.status >= 200 && res.status < 300) {
|
||||
return { data: res.data, success: true };
|
||||
@ -40,33 +40,37 @@ export default class API {
|
||||
return { data: null, success: false };
|
||||
}
|
||||
|
||||
async fetchAssetMetric(symbol: string, metric: string, days = 7) {
|
||||
const sym = `${symbol}`.toUpperCase();
|
||||
async fetchAssetMetric(id: string, metric: string, days = 7) {
|
||||
const sym = `${id}`.toUpperCase();
|
||||
return await this._fetch("GET", `/assets/${sym}/metrics/${metric}`, {
|
||||
change_over: days
|
||||
change_over: days,
|
||||
});
|
||||
}
|
||||
|
||||
async fetchAssetMetrics(symbol: string) {
|
||||
const sym = `${symbol}`.toUpperCase();
|
||||
async fetchAssetMetrics(id: string) {
|
||||
const sym = `${id}`.toUpperCase();
|
||||
return await this._fetch("GET", `/assets/${sym}/metrics`);
|
||||
}
|
||||
|
||||
async fetchFCASDistribution() {
|
||||
async fetchFCASDistribution(fullDistribution: boolean = false) {
|
||||
return await this._fetch("GET", `/metrics/FCAS/assets`, {
|
||||
visual_distribution: true
|
||||
visual_distribution: !fullDistribution,
|
||||
});
|
||||
}
|
||||
|
||||
async fetchDynamic(id: string) {
|
||||
return this._fetch("GET", `/widgets/dynamic/${id}`);
|
||||
}
|
||||
|
||||
async fetchMetrics(payload: {
|
||||
assets: string[];
|
||||
exclusions: string[];
|
||||
assets?: string[];
|
||||
exclusions?: string[];
|
||||
sort_by?: string;
|
||||
sort_desc?: boolean;
|
||||
page: number;
|
||||
page?: number;
|
||||
size?: number;
|
||||
metrics: string[];
|
||||
change_over: number;
|
||||
metrics?: string[];
|
||||
change_over?: number;
|
||||
}) {
|
||||
return await this.client.post(`/assets/metrics`, payload);
|
||||
}
|
||||
@ -74,9 +78,31 @@ export default class API {
|
||||
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 WidgetLinksSlug = "spectrum" | "multi-table" | "table" | "score";
|
||||
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;
|
||||
|
||||
84
src/chart/defaults.ts
Normal file
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
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
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
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
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;
|
||||
@ -1,11 +1,17 @@
|
||||
import { h, Component, render } from "preact";
|
||||
import API, { WidgetLinksLink } from "../../api";
|
||||
import find = require("lodash/find");
|
||||
import find from "lodash/find";
|
||||
import classNames from "classnames";
|
||||
import * as css from "./style.css";
|
||||
|
||||
type Props = {
|
||||
widget: "spectrum" | "multi-table" | "table" | "score";
|
||||
widget:
|
||||
| "spectrum"
|
||||
| "multi-table"
|
||||
| "table"
|
||||
| "score"
|
||||
| "chart"
|
||||
| "price-multi-table";
|
||||
api: API;
|
||||
style?: any;
|
||||
linkClass?: string;
|
||||
@ -18,7 +24,58 @@ type State = {
|
||||
|
||||
class CustomLinks extends Component<Props, State> {
|
||||
state: State = {
|
||||
links: []
|
||||
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() {
|
||||
@ -28,6 +85,14 @@ class CustomLinks extends Component<Props, State> {
|
||||
}
|
||||
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) {
|
||||
@ -36,7 +101,17 @@ class CustomLinks extends Component<Props, State> {
|
||||
return (
|
||||
<div class={css.wrapper} style={props.style}>
|
||||
<span class={linkClass}>
|
||||
<a href="https://flipsidecrypto.com/fcas">What's this?</a>
|
||||
<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>
|
||||
);
|
||||
@ -45,22 +120,26 @@ class CustomLinks extends Component<Props, State> {
|
||||
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 }}
|
||||
/>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-wrap: wrap;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.link {
|
||||
|
||||
6
src/components/noDataMessage/index.tsx
Normal file
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
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
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;
|
||||
@ -4,6 +4,7 @@ import * as css from "./style.css";
|
||||
|
||||
type Props = {
|
||||
score: number;
|
||||
grade?: string;
|
||||
kind?: "slim" | "normal" | "large";
|
||||
class?: string;
|
||||
};
|
||||
@ -14,11 +15,11 @@ type State = {
|
||||
|
||||
export default class Rank extends Component<Props, State> {
|
||||
static defaultProps = {
|
||||
kind: "slim"
|
||||
kind: "slim",
|
||||
};
|
||||
|
||||
state: State = {
|
||||
showTooltip: false
|
||||
showTooltip: false,
|
||||
};
|
||||
|
||||
showTooltip = () => {
|
||||
@ -31,16 +32,21 @@ export default class Rank extends Component<Props, State> {
|
||||
|
||||
render(props: Props) {
|
||||
let rankClass;
|
||||
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;
|
||||
let css_: any = css;
|
||||
if (props.grade) {
|
||||
rankClass = css_[props.grade.toLowerCase()];
|
||||
} else {
|
||||
rankClass = css.s;
|
||||
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];
|
||||
@ -52,6 +58,5 @@ export default class Rank extends Component<Props, State> {
|
||||
<span class={classes} />
|
||||
</div>
|
||||
);
|
||||
// return <span class={`fs-rank fs-rank-${rank}`} />;
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,8 @@ import classNames from "classnames";
|
||||
import * as css from "./style.css";
|
||||
|
||||
type Props = {
|
||||
change: number;
|
||||
change?: number;
|
||||
pointChange?: number;
|
||||
value: number;
|
||||
class?: string;
|
||||
};
|
||||
@ -14,15 +15,26 @@ export function calculateTrendDiff(value: number, percent: number): number {
|
||||
|
||||
const Trend = (props: Props) => {
|
||||
let directionClass, icon;
|
||||
if (props.change < 0) {
|
||||
let changeDeterminate = props.pointChange ? props.pointChange : props.change;
|
||||
if (changeDeterminate < 0) {
|
||||
directionClass = css.down;
|
||||
icon = require("./images/down.svg");
|
||||
} else {
|
||||
} else if (changeDeterminate > 0) {
|
||||
directionClass = css.up;
|
||||
icon = require("./images/up.svg");
|
||||
}
|
||||
|
||||
const difference = calculateTrendDiff(props.value, props.change);
|
||||
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 (
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
||||
4
src/custom.d.ts
vendored
4
src/custom.d.ts
vendored
@ -2,3 +2,7 @@ declare module "*.css" {
|
||||
const css: any;
|
||||
export default css;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
Highcharts: any;
|
||||
}
|
||||
|
||||
64
src/dynamic/index.tsx
Normal file
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
4
src/dynamic/load-js.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
declare module 'load-js' {
|
||||
const fn: any;
|
||||
export default fn;
|
||||
}
|
||||
139
src/frame/index.tsx
Normal file
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,10 @@ 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;
|
||||
@ -17,6 +21,10 @@ export default class Flipside {
|
||||
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);
|
||||
@ -26,6 +34,7 @@ export default class Flipside {
|
||||
spectrum(el: string, opts: SpectrumProps): void {
|
||||
const element = document.getElementById(el);
|
||||
const props = defaultsWithoutArrays(Spectrum.defaultProps, opts);
|
||||
|
||||
render(<Spectrum {...props} api={this.api} />, element);
|
||||
}
|
||||
|
||||
@ -33,9 +42,20 @@ export default class Flipside {
|
||||
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
|
||||
dark: false,
|
||||
};
|
||||
const mergedOpts = Object.assign({}, defaults, opts);
|
||||
|
||||
|
||||
@ -4,76 +4,105 @@ 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";
|
||||
|
||||
import uniq = require("lodash/uniq");
|
||||
import sortBy = require("lodash/sortBy");
|
||||
import reverse = require("lodash/reverse");
|
||||
|
||||
// Define the columns, the content of their header, and how their data is rendered.
|
||||
type ColumnDefinition = {
|
||||
header: string;
|
||||
renderItem: (row: Row) => any;
|
||||
sortKey?: string;
|
||||
};
|
||||
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 } = {
|
||||
coin: {
|
||||
symbol: {
|
||||
header: "Coin",
|
||||
renderItem: (row: Row) => row.symbol
|
||||
renderItem: row => row.symbol
|
||||
},
|
||||
name: {
|
||||
header: "Coin",
|
||||
renderItem: row => row.asset_name || row.symbol
|
||||
},
|
||||
|
||||
fcas: {
|
||||
header: "FCAS",
|
||||
renderItem: (row: Row) => row.fcas,
|
||||
renderItem: row => row.fcas,
|
||||
sortKey: "fcas"
|
||||
},
|
||||
|
||||
trend: {
|
||||
header: "7D",
|
||||
renderItem: (row: Row) => (
|
||||
<Trend change={row.fcas_change} value={row.fcas} />
|
||||
),
|
||||
renderItem: row => <Trend change={row.fcas_change} value={row.fcas} />,
|
||||
sortKey: "fcas_change"
|
||||
},
|
||||
|
||||
userActivity: {
|
||||
header: "User Activity",
|
||||
renderItem: (row: Row) => row.utility,
|
||||
renderItem: row => row.utility,
|
||||
sortKey: "utility"
|
||||
},
|
||||
|
||||
developerBehavior: {
|
||||
header: "Developer Behavior",
|
||||
renderItem: (row: Row) => row.dev,
|
||||
renderItem: row => row.dev,
|
||||
sortKey: "dev"
|
||||
},
|
||||
|
||||
marketMaturity: {
|
||||
header: "Market Maturity",
|
||||
renderItem: (row: Row) => row.market_maturity,
|
||||
renderItem: row => row.market_maturity,
|
||||
sortKey: "market_maturity"
|
||||
},
|
||||
|
||||
rank: {
|
||||
header: "Rank",
|
||||
renderItem: (row: Row) => <Rank score={row.fcas} />,
|
||||
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"
|
||||
}
|
||||
};
|
||||
|
||||
type ColumnName =
|
||||
| "trend"
|
||||
| "developerBehavior"
|
||||
| "userActivity"
|
||||
| "marketMaturity"
|
||||
| "rank";
|
||||
|
||||
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[];
|
||||
@ -94,36 +123,52 @@ export type Props = {
|
||||
dividers?: boolean;
|
||||
dividersColor?: string;
|
||||
style?: object;
|
||||
padding?: string;
|
||||
headerBold?: boolean;
|
||||
};
|
||||
api?: API;
|
||||
};
|
||||
|
||||
type Row = {
|
||||
symbol: string;
|
||||
fcas: number;
|
||||
dev: number;
|
||||
utility: number;
|
||||
fcas_change: number;
|
||||
dev_change: number;
|
||||
utility_change: number;
|
||||
market_maturity: number;
|
||||
market_maturity_change: number;
|
||||
};
|
||||
|
||||
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> {
|
||||
constructor() {
|
||||
super();
|
||||
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,
|
||||
sortColumn: "fcas",
|
||||
sortOrder: "desc"
|
||||
pageSortBy: props.sortBy || props.columns[0],
|
||||
sortColumn: props.sortBy || "fcas",
|
||||
sortOrder: "desc",
|
||||
priceFilterRequired: priceFilterRequired,
|
||||
filteredColumns
|
||||
};
|
||||
}
|
||||
|
||||
@ -133,6 +178,7 @@ export default class MultiTable extends Component<Props, State> {
|
||||
page: 1,
|
||||
fontFamily: "inherit",
|
||||
columns: [
|
||||
"fcas",
|
||||
"trend",
|
||||
"userActivity",
|
||||
"developerBehavior",
|
||||
@ -145,18 +191,26 @@ export default class MultiTable extends Component<Props, State> {
|
||||
rows: {
|
||||
alternating: true,
|
||||
alternatingColors: [],
|
||||
dividers: false
|
||||
dividers: false,
|
||||
dividersColor: null,
|
||||
style: {}
|
||||
},
|
||||
trend: {
|
||||
changeOver: 7
|
||||
}
|
||||
},
|
||||
widgetType: "multi-table"
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
this._getData();
|
||||
await this._getData();
|
||||
this.updateInterval = setInterval(this._getData, 60000);
|
||||
}
|
||||
|
||||
async _getData() {
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.updateInterval);
|
||||
}
|
||||
|
||||
_getData = async () => {
|
||||
const res = await this.props.api.fetchMetrics({
|
||||
assets: this.props.assets,
|
||||
exclusions: this.props.exclusions,
|
||||
@ -164,16 +218,16 @@ export default class MultiTable extends Component<Props, State> {
|
||||
size: this.props.limit,
|
||||
sort_by: COLUMNS[this.state.sortColumn].sortKey,
|
||||
sort_desc: true,
|
||||
metrics: ["fcas", "utility", "dev", "market-maturity"],
|
||||
metrics: mapConfigColumnsNames(this.state.filteredColumns),
|
||||
change_over: this.props.trend.changeOver
|
||||
});
|
||||
this.setState({
|
||||
loading: false,
|
||||
rows: res.data
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleSort(col: string) {
|
||||
handleClickSort(col: string) {
|
||||
if (COLUMNS[col].sortKey) {
|
||||
const sortColumn = col;
|
||||
const sortOrder = this.state.sortOrder === "asc" ? "desc" : "asc";
|
||||
@ -183,8 +237,8 @@ export default class MultiTable extends Component<Props, State> {
|
||||
|
||||
render(props: Props, state: State) {
|
||||
if (state.loading) return null;
|
||||
|
||||
const columns = ["coin", "fcas", ...props.columns];
|
||||
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
|
||||
@ -196,7 +250,7 @@ export default class MultiTable extends Component<Props, State> {
|
||||
sortedRows = reverse(sortedRows);
|
||||
}
|
||||
|
||||
const { fontFamily } = props;
|
||||
const { fontFamily, widgetType } = props;
|
||||
|
||||
return (
|
||||
<div class={classes} style={{ fontFamily }}>
|
||||
@ -206,7 +260,7 @@ export default class MultiTable extends Component<Props, State> {
|
||||
{props.title.text}
|
||||
</h1>
|
||||
)}
|
||||
<CustomLinks widget="multi-table" api={this.props.api} />
|
||||
<CustomLinks widget={widgetType} api={this.props.api} />
|
||||
</header>
|
||||
|
||||
<table>
|
||||
@ -218,7 +272,11 @@ export default class MultiTable extends Component<Props, State> {
|
||||
"fs-multi-sortable": !!column.sortKey
|
||||
});
|
||||
return (
|
||||
<th class={classes} onClick={() => this.handleSort(col)}>
|
||||
<th
|
||||
class={classes}
|
||||
onClick={() => this.handleClickSort(col)}
|
||||
style={props.headers.style}
|
||||
>
|
||||
<div class="fs-multi-colhead" style={props.headers.style}>
|
||||
{column.sortKey && (
|
||||
<span
|
||||
@ -243,7 +301,15 @@ export default class MultiTable extends Component<Props, State> {
|
||||
{sortedRows.map(asset => (
|
||||
<tr>
|
||||
{columns.map(col => (
|
||||
<td class={`fs-multi-${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>
|
||||
))}
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
table {
|
||||
text-align: right;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th,
|
||||
@ -18,6 +19,11 @@
|
||||
|
||||
td {
|
||||
font-weight: 500;
|
||||
font-variant-numeric: tabular-nums;
|
||||
&:first-child {
|
||||
text-align: left;
|
||||
color: #2d57ed;
|
||||
}
|
||||
}
|
||||
|
||||
&-light {
|
||||
@ -39,9 +45,8 @@
|
||||
&-colhead {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
&-text {
|
||||
flex-basis: 0;
|
||||
// flex-basis: 0;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
36
src/multiTable/types.d.ts
vendored
Normal file
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";
|
||||
@ -1,4 +1,4 @@
|
||||
import { h, Component } from "preact";
|
||||
import { h } from "preact";
|
||||
import withFcas, { WithFcasProps } from "../components/withFcas";
|
||||
import Trend from "../components/trend";
|
||||
import Rank from "../components/rank";
|
||||
|
||||
@ -7,6 +7,8 @@ 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;
|
||||
@ -20,8 +22,10 @@ type BootstrapHighlightType = {
|
||||
};
|
||||
|
||||
export type AssetType = {
|
||||
symbol: string;
|
||||
asset_id?: string;
|
||||
symbol?: string;
|
||||
highlights?: string[];
|
||||
fullDistribution?: boolean;
|
||||
bootstrapAsset?: BootstrapAssetType;
|
||||
bootstrapHighlights?: BootstrapHighlightType[];
|
||||
};
|
||||
@ -32,6 +36,7 @@ export type Props = {
|
||||
mode?: "light" | "dark";
|
||||
fontFamily?: string;
|
||||
autoWidth?: boolean;
|
||||
showHeader?: boolean;
|
||||
relatedMarkers?: {
|
||||
bucketDistance?: number;
|
||||
lineDistance?: number;
|
||||
@ -48,38 +53,49 @@ export type Props = {
|
||||
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"
|
||||
mode: "light",
|
||||
autoRefresh: true,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = { loading: true, metric: null };
|
||||
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(
|
||||
this.props.asset.symbol,
|
||||
"FCAS"
|
||||
);
|
||||
let result = await this.props.api.fetchAssetMetric(assetId, "FCAS");
|
||||
data = result.data;
|
||||
success = result.success;
|
||||
} else {
|
||||
@ -87,28 +103,41 @@ class Spectrum extends Component<Props, State> {
|
||||
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();
|
||||
}, 2000);
|
||||
}, 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
|
||||
}
|
||||
name: data.asset_name,
|
||||
},
|
||||
widgetLinks: widgetLinks,
|
||||
});
|
||||
return success;
|
||||
}
|
||||
|
||||
_update() {
|
||||
if (this.props.autoRefresh !== true) {
|
||||
return;
|
||||
}
|
||||
this.interval = window.setInterval(async () => {
|
||||
await this._getData();
|
||||
}, 300000);
|
||||
}, 90000);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -123,8 +152,8 @@ class Spectrum extends Component<Props, State> {
|
||||
metric: {
|
||||
name: "NA",
|
||||
fcas: 0,
|
||||
change: 0
|
||||
}
|
||||
change: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
this._update();
|
||||
@ -132,44 +161,61 @@ class Spectrum extends Component<Props, State> {
|
||||
|
||||
render(props: Props, state: State) {
|
||||
if (state.loading) return null;
|
||||
if (!state.data) return <NoDataMessage />;
|
||||
|
||||
const { asset, mode, rank, trend, api } = props;
|
||||
const { metric } = state;
|
||||
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]}>
|
||||
<div class={css.header}>
|
||||
<img
|
||||
class={css.icon}
|
||||
src={`https://d301yvow08hyfu.cloudfront.net/svg/color/${asset.symbol.toLowerCase()}.svg`}
|
||||
/>
|
||||
<span class={css.name}>{metric.name}</span>
|
||||
</div>
|
||||
{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>
|
||||
)}
|
||||
|
||||
<div class={css.meta}>
|
||||
<span class={css.symbol}>{asset.symbol}</span>
|
||||
<span class={css.fcas}>Health {metric.fcas}</span>
|
||||
{trend.enabled && (
|
||||
<span class={css.trend}>
|
||||
<Trend change={metric.change} value={metric.fcas} />
|
||||
</span>
|
||||
)}
|
||||
{rank.enabled && (
|
||||
<a href={defaultFlipsideLink(api.key, "spectrum")}>
|
||||
<span class={css.rank}>
|
||||
<Rank score={metric.fcas} kind="normal" />
|
||||
{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>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{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} />}
|
||||
{props.spectrum.enabled && (
|
||||
<Plot metric={metric} {...props} symbol={data.symbol} />
|
||||
)}
|
||||
|
||||
{props.disableLinks === false && (
|
||||
<CustomLinks
|
||||
widget="spectrum"
|
||||
api={props.api}
|
||||
linkBootstrap={props.linkBootstrap}
|
||||
linkBootstrap={state.widgetLinks}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -197,24 +243,25 @@ MultiSpectrum.defaultProps = {
|
||||
symbol: "btc",
|
||||
highlights: ["eth", "zec", "zrx"],
|
||||
bootstrapAsset: null,
|
||||
bootstrapHighlights: null
|
||||
bootstrapHighlights: null,
|
||||
},
|
||||
disableLinks: false,
|
||||
assets: [],
|
||||
mode: "light",
|
||||
fontFamily: "inherit",
|
||||
showHeader: true,
|
||||
relatedMarkers: {
|
||||
enabled: true,
|
||||
bucketDistance: 35,
|
||||
lineDistance: 25,
|
||||
fontFamily: "inherit"
|
||||
fontFamily: "inherit",
|
||||
},
|
||||
name: { enabled: true },
|
||||
spectrum: { enabled: true },
|
||||
icon: { enabled: true },
|
||||
rank: { enabled: true },
|
||||
trend: { enabled: true },
|
||||
linkBootstrap: null
|
||||
linkBootstrap: null,
|
||||
} as Props;
|
||||
|
||||
export default MultiSpectrum;
|
||||
|
||||
@ -2,6 +2,7 @@ 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;
|
||||
@ -13,7 +14,13 @@ const DEFAULT_LINE_DISTANCE = 25;
|
||||
// mode: "light" | "dark"
|
||||
// }
|
||||
|
||||
export default class Plot extends Component<any, any> {
|
||||
type Props = {
|
||||
mode: "light" | "dark";
|
||||
symbol: string;
|
||||
autoRefresh?: boolean;
|
||||
} & any;
|
||||
|
||||
export default class Plot extends Component<Props, any> {
|
||||
interval: any;
|
||||
|
||||
constructor() {
|
||||
@ -22,16 +29,18 @@ export default class Plot extends Component<any, any> {
|
||||
loading: true,
|
||||
distribution: null,
|
||||
highlights: [],
|
||||
highlightedSymbols: []
|
||||
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();
|
||||
let result = await this.props.api.fetchFCASDistribution(fullDistribution);
|
||||
data = result.data;
|
||||
success = result.success;
|
||||
} else {
|
||||
@ -49,7 +58,7 @@ export default class Plot extends Component<any, any> {
|
||||
if (data && data.length > 0) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
distribution: data
|
||||
distribution: data,
|
||||
});
|
||||
}
|
||||
return success;
|
||||
@ -66,7 +75,7 @@ export default class Plot extends Component<any, any> {
|
||||
);
|
||||
} else {
|
||||
await Promise.all(
|
||||
highlights.map(async asset => {
|
||||
highlights.map(async (asset) => {
|
||||
const { data, success } = await this.props.api.fetchAssetMetric(
|
||||
asset,
|
||||
"fcas"
|
||||
@ -83,11 +92,14 @@ export default class Plot extends Component<any, any> {
|
||||
}
|
||||
this.setState({
|
||||
highlights: nextHighlightState,
|
||||
highlightedSymbols: nextHighlightedSymbolState
|
||||
highlightedSymbols: nextHighlightedSymbolState,
|
||||
});
|
||||
}
|
||||
|
||||
_update() {
|
||||
if (this.props.autoRefresh !== true) {
|
||||
return;
|
||||
}
|
||||
this.interval = setInterval(() => {
|
||||
this._getData();
|
||||
}, 300000);
|
||||
@ -103,7 +115,7 @@ export default class Plot extends Component<any, any> {
|
||||
if (!success) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
distribution: []
|
||||
distribution: [],
|
||||
});
|
||||
}
|
||||
this._update();
|
||||
@ -111,8 +123,8 @@ export default class Plot extends Component<any, any> {
|
||||
|
||||
getHighlights() {
|
||||
let { asset } = this.props;
|
||||
let { highlights, symbol } = asset;
|
||||
let assetSymbol = symbol.toLowerCase();
|
||||
let { highlights } = asset;
|
||||
let assetSymbol = this.props.symbol.toLowerCase();
|
||||
|
||||
if (highlights && highlights.length > 0) {
|
||||
return highlights;
|
||||
@ -179,7 +191,10 @@ export default class Plot extends Component<any, any> {
|
||||
}
|
||||
|
||||
let bucketIndex = scoresToBuckets[asset.value];
|
||||
|
||||
if (bucketIndex == null || bucketIndex == undefined) return;
|
||||
let bucket = buckets[bucketIndex];
|
||||
|
||||
let index = 0;
|
||||
|
||||
let toClose = false;
|
||||
@ -197,6 +212,7 @@ export default class Plot extends Component<any, any> {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { y: 44 - 10 * index, toClose };
|
||||
}
|
||||
|
||||
@ -207,11 +223,16 @@ export default class Plot extends Component<any, any> {
|
||||
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.asset.symbol.toUpperCase());
|
||||
.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();
|
||||
|
||||
@ -220,10 +241,10 @@ export default class Plot extends Component<any, any> {
|
||||
if (props.relatedMarkers) {
|
||||
relatedLabelStyle = {
|
||||
fill: props.relatedMarkers.color,
|
||||
fontFamily: props.relatedMarkers.fontFamily
|
||||
fontFamily: props.relatedMarkers.fontFamily,
|
||||
};
|
||||
relatedLineStyle = {
|
||||
stroke: props.relatedMarkers.color
|
||||
stroke: props.relatedMarkers.color,
|
||||
};
|
||||
}
|
||||
|
||||
@ -284,7 +305,9 @@ export default class Plot extends Component<any, any> {
|
||||
{props.relatedMarkers.enabled &&
|
||||
highlightedAssets.map((a: any) => {
|
||||
const xPos = `${(a.value / 1000) * 100}%`;
|
||||
let { y, toClose } = this.getYCoords(a, buckets, scoresToBuckets);
|
||||
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">
|
||||
@ -307,7 +330,7 @@ export default class Plot extends Component<any, any> {
|
||||
{/* Blue FCAS Marker */}
|
||||
<text class={css.marker} text-anchor="middle" font-weight="bold">
|
||||
<tspan x={xPos} y={26}>
|
||||
{props.asset.symbol.toUpperCase()}
|
||||
{props.symbol.toUpperCase()}
|
||||
</tspan>
|
||||
</text>
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { h, Component, render } from "preact";
|
||||
import "./style.scss";
|
||||
import API, { WidgetLinksLink } from "../../../api";
|
||||
import find = require("lodash/find");
|
||||
import find from "lodash/find";
|
||||
|
||||
type Props = {
|
||||
widget: "spectrum" | "multi-table" | "table";
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { h, Component } from "preact";
|
||||
import keyBy = require("lodash/keyBy");
|
||||
import keyBy from "lodash/keyBy";
|
||||
import CustomLinks from "./components/customLinks";
|
||||
import "./styles.scss";
|
||||
import API from "../api";
|
||||
@ -57,9 +57,7 @@ export default class Table extends Component<Props, State> {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
11
src/utils.ts
11
src/utils.ts
@ -1,5 +1,5 @@
|
||||
import isArray = require("lodash/isArray");
|
||||
import mergeWith = require("lodash/mergeWith");
|
||||
import isArray from "lodash/isArray";
|
||||
import mergeWith from "lodash/mergeWith";
|
||||
|
||||
export const compare = (key: string) => {
|
||||
return (a: any, b: any) => {
|
||||
@ -26,5 +26,10 @@ export function defaultsWithoutArrays(obj: object, src: object): object {
|
||||
}
|
||||
|
||||
export function defaultFlipsideLink(apiKey: string, widget: string) {
|
||||
return `https://platform-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`;
|
||||
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;
|
||||
}
|
||||
|
||||
3562
stats.json
3562
stats.json
File diff suppressed because one or more lines are too long
@ -1,13 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/",
|
||||
"moduleResolution": "node",
|
||||
"noImplicitAny": true,
|
||||
"sourceMap": true,
|
||||
"module": "commonjs",
|
||||
"module": "esnext",
|
||||
"target": "es5",
|
||||
"lib": ["es2015", "dom"],
|
||||
"jsx": "react",
|
||||
"allowJs": true,
|
||||
"jsxFactory": "h"
|
||||
"jsxFactory": "h",
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,18 +1,24 @@
|
||||
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.tsx",
|
||||
output: {
|
||||
filename: filename,
|
||||
path: path.resolve(__dirname, "dist")
|
||||
filename: `flipside-v${version}.js`,
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
publicPath: PUBLIC_PATH
|
||||
},
|
||||
mode: "development",
|
||||
devtool: "inline-source-map",
|
||||
resolve: {
|
||||
extensions: [".tsx", ".ts", ".js"]
|
||||
},
|
||||
// plugins: [new BundleAnalyzerPlugin()],
|
||||
module: {
|
||||
rules: [
|
||||
{ test: /\.tsx?$/, use: "ts-loader", exclude: /node_modules/ },
|
||||
|
||||
1796
yarn-error.log
1796
yarn-error.log
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user