Compare commits

...

68 Commits

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-08 23:25:42 +00:00
Jim Myers
23a7c4953a
Merge pull request #27 from FlipsideCrypto/v1.16.1
v1.16.1
2020-09-30 19:24:04 -04:00
Jim Myers
4844f92206 Default to 100% width of parent element for frame widget 2020-09-30 19:20:36 -04:00
Jim Myers
730f24a797 v1.16.1 2020-09-30 17:25:50 -04:00
Jim Myers
b2bbc6b955
Merge pull request #23 from FlipsideCrypto/v1.16.0
Add frame widget.
2020-08-14 22:09:25 -04:00
Jim Myers
56cd6f206a Use URLSearchParams function. 2020-08-14 22:05:26 -04:00
Jim Myers
cde8ae754c Add dark/light modes to frame. 2020-08-14 13:22:16 -04:00
Jim Myers
1326e490a7 Encode url params 2020-08-14 12:17:50 -04:00
Jim Myers
1d980b3ce6 Add frame widget. 2020-08-13 21:40:35 -04:00
Jim Myers
4318267c78 Add parameter to dynamic widget and remove . 2020-08-13 10:58:58 -04:00
Jim Myers
b038c1c427
Merge pull request #19 from FlipsideCrypto/v1.14.1
Version 1.14.1
2020-06-05 10:42:33 -04:00
Jim Myers
651b19c8e6 Default to autorefresh. 2020-06-05 10:36:46 -04:00
Jim Myers
866f129b9a use break not continue. 2020-06-05 10:27:46 -04:00
Jim Myers
99b05e6c39 Add autoRefreshToggle, fix bug where highlights do not fully appear in the spectrum. 2020-06-05 10:23:30 -04:00
Jim Myers
92208fad3e
Merge pull request #18 from FlipsideCrypto/hide-spectrum-header
Add showHeader option to spectrum widget
2020-05-15 07:58:38 -04:00
Jim Myers
95e2d7c262 Update default flipside link to api.flipsidecrypto.com 2020-05-15 07:57:35 -04:00
Bryan Cinman
3e5494daf7 Add showHeader option to spectrum widget 2020-05-13 14:03:41 -04:00
Jim Myers
6d885476dc
Merge pull request #17 from FlipsideCrypto/v1.13.2
Update link rendering for letter grade.
2020-04-16 13:20:12 -04:00
Jim Myers
c8d2400ffd Enable link bootstrap. 2020-04-16 13:16:47 -04:00
Jim Myers
f9e4d0a4f6 Update link rendering for letter grade. 2020-04-16 13:14:05 -04:00
Jim Myers
2b48937ac7 Merge branch 'master' of github.com:FlipsideCrypto/flipside-js 2020-04-14 21:43:27 -04:00
Jim Myers
e17d873d08 Bump version to 1.13.1 2020-04-14 21:42:41 -04:00
Jim Myers
c8f7055567
Merge pull request #16 from FlipsideCrypto/dynamic-dark
Add dark mode to dynamic widget
2020-04-14 17:18:47 -04:00
Jim Myers
3945d911c5 Update version to 1.13.0 2020-04-14 17:17:30 -04:00
Bryan Cinman
f283cc84a3 Add dark mode to dynamic widget 2020-04-14 13:42:44 -04:00
Jim Myers
2d51db81ec Update asset interface of dynamic widget. 2020-03-04 12:05:47 -05:00
Bryan Cinman
124d14c2a1 Use js_url properly, update. platform-api -> api 2020-02-27 14:03:03 -05:00
Bryan Cinman
c0268f4f40 Add dynamic widget 2020-02-26 13:29:21 -05:00
Jim Myers
2fb5d3da4e Reduce retries on failure. 2020-02-17 20:02:32 -05:00
Bryan Cinman
1fdb9926e0
Merge pull request #6 from FlipsideCrypto/bugfix/remove-dynamic-loading
remove dynamic highcharts loading
2019-05-01 15:09:55 -04:00
Bryan Cinman
210dc70234 remove dynamic highcharts loading 2019-05-01 13:00:08 -04:00
Jim Myers
a038c9bb4f Fix 0 point change issue in trend component. 2019-04-19 09:17:08 -04:00
Jim Myers
1460964b33 Utilize precomputed point change and grade. 2019-04-18 08:39:27 -04:00
Jim Myers
6f05f7fe4b Small issue fix. 2019-03-19 14:57:47 -04:00
Jim Myers
6d7b4d2c88 Update requests. 2019-03-15 14:40:53 -04:00
Jim Myers
eab5a73255 Merge branch 'bugfix/map-multitable-columns' into asset-ids 2019-03-13 23:22:15 -04:00
Bryan Cinman
3378e5625c add asset_ids to chart, add nodata messaging 2019-03-13 22:56:52 -04:00
Bryan Cinman
744681e3f8 add config column mapping 2019-03-13 16:27:17 -04:00
Bryan Cinman
a6d7a2d2a6 fix publicPath 2019-03-13 15:19:08 -04:00
Jim Myers
942fe5a7f4 Fix dual highcharts import issue. 2019-03-13 15:18:16 -04:00
Bryan Cinman
953107687e add ability for spectrum and chart to use asset ids 2019-03-13 04:27:31 -04:00
Jim Myers
5010dfd2ef Update handling of divider color. 2019-03-12 11:25:56 -04:00
Jim Myers
e3ee0fd590 Fix example. 2019-03-12 11:14:47 -04:00
Jim Myers
f4e0da2d8d Add ability to customize row and header style attribute on multi-table widget. 2019-03-12 11:13:19 -04:00
Jim Myers
b8abd9203f Update price table. 2019-03-12 10:48:16 -04:00
Jim Myers
3c5d940da6 Fixes for broken side-effects :( 2019-03-12 10:42:02 -04:00
Jim Myers
e6c9840ecb CMC updates 2019-03-12 10:09:00 -04:00
Bryan Cinman
33e93fcb3d remove dividers in test page multiTable 2019-03-11 17:49:53 -04:00
Bryan Cinman
95c1c69912 fix coin column color, set maximum decimal digits 2019-03-11 17:49:12 -04:00
Bryan Cinman
e48a219547 fix customlinks styling, add commas to currencies, add $ prefix 2019-03-11 17:12:09 -04:00
Bryan Cinman
24543e4231 added volume, price, and market cap data and api polling to multi-table 2019-03-08 16:39:25 -05:00
Jim Myers
a0c8c1a503 Update readme.md. 2019-03-04 21:00:20 -05:00
Jim Myers
518a20289d Add public path. 2019-03-04 20:42:39 -05:00
Jim Myers
194a1822be Comment out bundle analyzer (hangs during build). 2019-03-04 08:18:04 -05:00
Bryan Cinman
9413808bdf add code splitting 2019-03-03 18:00:43 -05:00
Jim Myers
5ae203dde5 Merge branch 'master' into charts 2019-03-01 14:55:20 -05:00
Jim Myers
3c8fd0f326 Configure. 2019-03-01 14:52:23 -05:00
Jim Myers
68796ce9bb Update links. 2019-03-01 12:38:02 -05:00
Bryan Cinman
0411a6742d fix svg text sizing on chart 2019-03-01 02:04:45 -05:00
Jim Myers
d611b38729 Add emodule interop. 2019-02-28 18:12:59 -05:00
Jim Myers
b67904ad78 Merge branch 'master' into charts 2019-02-28 11:49:31 -05:00
Jim Myers
f4d11d76d3 Merge branch 'master' into charts 2019-02-22 21:45:52 -05:00
Bryan Cinman
15291d473c allow highcharts config in chart widget 2019-02-18 16:14:29 -05:00
Bryan Cinman
2b3d98520e export chart, range selector, naming series, customLinks on chart 2019-02-18 03:41:37 -05:00
Bryan Cinman
8dd894dbec first pass at a linechart widget 2019-02-16 05:53:29 -05:00
Bryan Cinman
6c351d1c28 allow lodash function import with es6 syntax, start chart widget 2019-02-14 04:28:47 -05:00
38 changed files with 8025 additions and 5064 deletions

3
.gitignore vendored
View File

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

View File

@ -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
```

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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
View File

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

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

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

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

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

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

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

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

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

View File

@ -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 }}
/>

View File

@ -2,6 +2,7 @@
display: flex;
flex-grow: 1;
flex-wrap: wrap;
font-weight: 400;
}
.link {

View File

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

View File

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

View File

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

View File

@ -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}`} />;
}
}

View File

@ -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 (

View File

@ -1,6 +1,7 @@
.wrapper {
display: flex;
align-items: center;
justify-content: flex-end;
}
.icon {

4
src/custom.d.ts vendored
View File

@ -2,3 +2,7 @@ declare module "*.css" {
const css: any;
export default css;
}
interface Window {
Highcharts: any;
}

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

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

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

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

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

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

View File

@ -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);

View File

@ -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>
))}

View File

@ -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
View File

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

View File

@ -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";

View File

@ -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;

View File

@ -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>

View File

@ -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";

View File

@ -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);
}

View File

@ -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

File diff suppressed because one or more lines are too long

View File

@ -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
}
}

View File

@ -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/ },

File diff suppressed because it is too large Load Diff

1866
yarn.lock

File diff suppressed because it is too large Load Diff