Merge branch 'bugfix/map-multitable-columns' into asset-ids

This commit is contained in:
Jim Myers 2019-03-13 23:22:15 -04:00
commit eab5a73255
13 changed files with 279 additions and 99 deletions

View File

@ -23,7 +23,7 @@
.wrapper {
border: 1px solid #ccc;
padding: 20px;
width: 500px;
width: 600px;
font-weight: 700;
font-size: 20px;
}
@ -61,21 +61,25 @@
</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.chart("chart", {
title: "Bitcoin Health",
title: "Flipside 25 - Overall Industry Health",
series: [
{
name: "BTC FCAS",
asset_id: 00303043,
name: "FLIP25",
symbol: "FLIP25",
metric: "fcas",
type: "line",
yAxis: 1
}
],
colors: ["#81A0F1"],
rangeSelector: {
enabled: true
}
@ -136,23 +140,65 @@
borderColor: "#737e8d"
});
// Price table
flipside.multiTable("multiTable", {
assets: ["btc", "etc"],
columns: ["rank", "trend"],
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: 7,
style: {}
}
});

View File

@ -4,7 +4,7 @@
"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"
},

View File

@ -59,14 +59,14 @@ export default class API {
}
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);
}
@ -97,7 +97,8 @@ export type WidgetLinksSlug =
| "multi-table"
| "table"
| "score"
| "chart";
| "chart"
| "price-multi-table";
export type WidgetLinksLink = {
widget_id: string;
name: string;

View File

@ -1,5 +1,4 @@
import { h, Component } from "preact";
import Highcharts from "highcharts/highstock";
import merge from "lodash/merge";
import API from "../api";
import { createApiSeries, createSeries } from "./helpers";
@ -9,7 +8,13 @@ import CustomLinks from "../components/customLinks";
import * as css from "./style.css";
import NoDataMessage from "../components/noDataMessage";
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";

View File

@ -5,7 +5,13 @@ import classNames from "classnames";
import * as css from "./style.css";
type Props = {
widget: "spectrum" | "multi-table" | "table" | "score" | "chart";
widget:
| "spectrum"
| "multi-table"
| "table"
| "score"
| "chart"
| "price-multi-table";
api: API;
style?: any;
linkClass?: string;

View File

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

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

View File

@ -4,75 +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 sortBy from "lodash/sortBy";
import reverse from "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[];
@ -93,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
};
}
@ -132,6 +178,7 @@ export default class MultiTable extends Component<Props, State> {
page: 1,
fontFamily: "inherit",
columns: [
"fcas",
"trend",
"userActivity",
"developerBehavior",
@ -144,37 +191,43 @@ 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() {
try {
const res = await this.props.api.fetchMetrics({
assets: this.props.assets,
exclusions: this.props.exclusions,
page: this.props.page,
size: this.props.limit,
sort_by: COLUMNS[this.state.sortColumn].sortKey,
sort_desc: true,
metrics: ["fcas", "utility", "dev", "market-maturity"],
change_over: this.props.trend.changeOver
});
this.setState({
loading: false,
rows: res.data
});
} catch (e) {}
componentWillUnmount() {
clearInterval(this.updateInterval);
}
handleSort(col: string) {
_getData = async () => {
const res = await this.props.api.fetchMetrics({
assets: this.props.assets,
exclusions: this.props.exclusions,
page: this.props.page,
size: this.props.limit,
sort_by: COLUMNS[this.state.sortColumn].sortKey,
sort_desc: true,
metrics: mapConfigColumnsNames(this.state.filteredColumns),
change_over: this.props.trend.changeOver
});
this.setState({
loading: false,
rows: res.data
});
};
handleClickSort(col: string) {
if (COLUMNS[col].sortKey) {
const sortColumn = col;
const sortOrder = this.state.sortOrder === "asc" ? "desc" : "asc";
@ -184,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
@ -197,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 }}>
@ -207,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>
@ -219,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
@ -244,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

@ -28,3 +28,8 @@ 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`;
}
export function countDecimals(value: number) {
if (Math.floor(value) === value) return 0;
return value.toString().split(".")[1].length || 0;
}

View File

@ -3,13 +3,18 @@ const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
const version = require("./package.json").version;
const filename = `flipside-v${version}.js`;
const PUBLIC_PATH =
process.env.NODE_ENV === "development"
? "/"
: "https://d3sek7b10w79kp.cloudfront.net/";
module.exports = {
entry: "./src/index.tsx",
output: {
filename: filename,
chunkFilename: `flipside-[name]-v${version}.js`,
path: path.resolve(__dirname, "dist"),
publicPath: "https://d3sek7b10w79kp.cloudfront.net/"
publicPath: PUBLIC_PATH
},
mode: "development",
devtool: "inline-source-map",