Skip to content

Commit 43c6f69

Browse files
committed
feat: swapped chart.js for the trading view charts
1 parent 3c9aaee commit 43c6f69

File tree

7 files changed

+152
-192
lines changed

7 files changed

+152
-192
lines changed

apps/insights/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
"async-cache-dedupe": "catalog:",
3333
"bs58": "catalog:",
3434
"change-case": "catalog:",
35-
"chart.js": "catalog:",
3635
"clsx": "catalog:",
3736
"date-fns": "catalog:",
3837
"csv-stringify": "catalog:",

apps/insights/src/components/PythProDemoPriceChart/index.tsx

Lines changed: 94 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,185 +1,140 @@
1-
import type { Nullish } from "@pythnetwork/shared-lib/types";
2-
import { isNullOrUndefined } from "@pythnetwork/shared-lib/util";
31
import { capitalCase } from "change-case";
4-
import {
5-
Chart,
6-
CategoryScale,
7-
LinearScale,
8-
LineController,
9-
LineElement,
10-
PointElement,
11-
Tooltip,
12-
Legend,
13-
TimeScale,
14-
} from "chart.js";
15-
import { formatDate } from "date-fns";
16-
import { useEffect, useLayoutEffect, useRef, useState } from "react";
17-
18-
import classes from "./index.module.scss";
2+
import type {
3+
IChartApi,
4+
ISeriesApi,
5+
LineData,
6+
UTCTimestamp,
7+
} from "lightweight-charts";
8+
import { createChart, LineSeries } from "lightweight-charts";
9+
import { useEffect, useLayoutEffect, useRef } from "react";
10+
1911
import type { AppStateContextVal } from "../../context/pyth-pro-demo";
2012
import { usePythProAppStateContext } from "../../context/pyth-pro-demo";
21-
import { getColorForSymbol, isAllowedSymbol } from "../../util/pyth-pro-demo";
22-
23-
Chart.register(
24-
CategoryScale,
25-
LinearScale,
26-
LineController,
27-
LineElement,
28-
PointElement,
29-
Tooltip,
30-
Legend,
31-
TimeScale,
32-
);
33-
34-
type ChartJSPoint = { x: number; y: number };
13+
import {
14+
getColorForSymbol,
15+
getThemeCssVar,
16+
isAllowedSymbol,
17+
} from "../../util/pyth-pro-demo";
3518

3619
type PythProDemoPriceChartImplProps = Pick<
3720
AppStateContextVal,
3821
"dataSourcesInUse" | "metrics" | "selectedSource"
3922
>;
4023

41-
const MAX_DATA_AGE = 1000 * 60; // hold no more than one minute's worth of data in the chart
42-
const MAX_DATA_POINTS = 3000; // don't keep more than 3K points in memory
24+
const MAX_DATA_AGE = 1000 * 60; // 1 minute
25+
const MAX_DATA_POINTS = 3000;
4326

44-
function PythProDemoPriceChartImpl({
27+
export function PythProDemoPriceChartImpl({
4528
dataSourcesInUse,
4629
metrics,
4730
selectedSource,
4831
}: PythProDemoPriceChartImplProps) {
49-
/** state */
50-
const [canvasRef, setCanvasRef] =
51-
useState<Nullish<HTMLCanvasElement>>(undefined);
32+
const containerRef = useRef<HTMLDivElement | null>(null);
33+
const chartRef = useRef<IChartApi>(undefined);
34+
const seriesMapRef = useRef<Record<string, ISeriesApi<"Line">>>({});
5235

53-
/** refs */
54-
const chartHandlerRef = useRef<Nullish<Chart>>(undefined);
36+
useLayoutEffect(() => {
37+
if (!containerRef.current) return;
5538

56-
/** effects */
57-
useEffect(() => {
58-
if (!canvasRef) return;
59-
const c = new Chart(canvasRef, {
60-
type: "line",
61-
data: { datasets: [] },
62-
options: {
63-
animation: false,
64-
elements: {
65-
point: { radius: 0 },
66-
},
67-
responsive: true,
68-
maintainAspectRatio: false,
69-
scales: {
70-
x: {
71-
beginAtZero: false,
72-
type: "linear", // push numeric timestamps or indices
73-
grid: { display: true },
74-
ticks: {
75-
callback(val) {
76-
const num = Number(val);
77-
const d = new Date();
78-
d.setTime(num);
79-
80-
return formatDate(d, "pp");
81-
},
82-
display: true,
83-
},
84-
},
85-
y: { type: "linear", beginAtZero: false, grid: { display: true } },
86-
},
87-
plugins: {
88-
legend: {
89-
display: true,
90-
labels: {
91-
generateLabels: (chart) => {
92-
// Start with the default labels
93-
const original =
94-
Chart.defaults.plugins.legend.labels.generateLabels(chart);
95-
96-
// Map them to whatever text you want
97-
return original.map((label) => ({
98-
...label,
99-
text: capitalCase(label.text),
100-
}));
101-
},
102-
usePointStyle: true,
103-
},
104-
},
105-
tooltip: { enabled: false },
106-
},
39+
const grayColor = getThemeCssVar("--theme-palette-gray-800") ?? "#ccc";
40+
const grayText = getThemeCssVar("--theme-palette-gray-300") ?? "#f1f1f3";
41+
42+
const chart = createChart(containerRef.current, {
43+
layout: {
44+
attributionLogo: false, // hide TradingView logo
45+
background: { color: "transparent" },
46+
textColor: grayText,
47+
},
48+
grid: {
49+
horzLines: { color: grayColor },
50+
vertLines: { color: grayColor },
51+
},
52+
rightPriceScale: {
53+
borderColor: grayColor,
54+
},
55+
timeScale: {
56+
barSpacing: 3,
57+
borderColor: grayColor,
58+
rightOffset: 0,
59+
secondsVisible: true,
60+
timeVisible: true,
10761
},
10862
});
10963

110-
chartHandlerRef.current = c;
111-
}, [canvasRef]);
64+
chartRef.current = chart;
65+
66+
return () => {
67+
chart.remove();
68+
chartRef.current = undefined;
69+
seriesMapRef.current = {};
70+
};
71+
}, []);
11272

11373
useEffect(() => {
114-
if (!chartHandlerRef.current || !isAllowedSymbol(selectedSource)) return;
115-
const { current: c } = chartHandlerRef;
74+
if (!chartRef.current || !isAllowedSymbol(selectedSource)) return;
11675

11776
for (const dataSource of dataSourcesInUse) {
11877
const latest = metrics[dataSource]?.latest;
11978
const symbolMetrics = latest?.[selectedSource];
120-
if (
121-
isNullOrUndefined(symbolMetrics) ||
122-
isNullOrUndefined(symbolMetrics.price)
123-
) {
124-
continue;
125-
}
126-
127-
let ds = c.data.datasets.find((d) => d.label === dataSource);
128-
if (!ds) {
129-
ds = {
130-
data: [],
131-
borderColor: getColorForSymbol(dataSource),
132-
label: dataSource,
133-
pointBorderWidth: 1,
134-
pointRadius: 0,
135-
pointHoverRadius: 0,
136-
tension: 0.2,
137-
};
138-
c.data.datasets.push(ds);
79+
if (!symbolMetrics?.price) continue;
80+
81+
let series = seriesMapRef.current[dataSource];
82+
if (!series) {
83+
series = chartRef.current.addSeries(LineSeries, {
84+
priceScaleId: "right",
85+
title: capitalCase(dataSource),
86+
});
87+
series.applyOptions({
88+
color: getColorForSymbol(dataSource),
89+
lineWidth: 2,
90+
lineStyle: 0, // solid
91+
});
92+
seriesMapRef.current[dataSource] = series;
13993
}
14094

141-
const lastDataPoint = ds.data.at(-1) as Nullish<ChartJSPoint>;
95+
const [lastPoint] = series.data().slice(-1);
14296
const latestMetricIsFresh =
143-
!lastDataPoint || lastDataPoint.x !== symbolMetrics.timestamp;
97+
!lastPoint ||
98+
lastPoint.time !== Math.floor(symbolMetrics.timestamp / 1000);
14499

145-
if (!latestMetricIsFresh) return;
100+
if (!latestMetricIsFresh) continue;
146101

147-
ds.data.push({ x: symbolMetrics.timestamp, y: symbolMetrics.price });
102+
const newPoint: LineData = {
103+
time: Math.floor(symbolMetrics.timestamp / 1000) as UTCTimestamp,
104+
value: symbolMetrics.price,
105+
};
148106

107+
series.update(newPoint);
108+
109+
// Trim old points
149110
const end = symbolMetrics.timestamp;
150111
const start = end - MAX_DATA_AGE;
151112

152-
ds.data = (ds.data as ChartJSPoint[])
153-
.filter((d) => d.x >= start && d.x <= end)
113+
const allData = series.data();
114+
const trimmed = allData
115+
.filter(
116+
(d) =>
117+
(d.time as UTCTimestamp) * 1000 >= start &&
118+
(d.time as UTCTimestamp) * 1000 <= end,
119+
)
154120
.slice(-MAX_DATA_POINTS);
155121

156-
// .sort() mutates the original array
157-
c.data.datasets.sort(
158-
(a, b) => a.label?.localeCompare(b.label ?? "") ?? 0,
159-
);
160-
}
122+
series.setData(trimmed);
161123

162-
c.update();
124+
// Update visible range so chart fills left-to-right
125+
chartRef.current.timeScale().setVisibleRange({
126+
from: Math.floor(start / 1000) as UTCTimestamp,
127+
to: Math.floor(end / 1000) as UTCTimestamp,
128+
});
129+
}
163130
});
164131

165-
useLayoutEffect(() => {
166-
return () => {
167-
chartHandlerRef.current?.destroy();
168-
};
169-
}, []);
132+
if (!isAllowedSymbol(selectedSource)) return;
170133

171-
if (!isAllowedSymbol(selectedSource)) {
172-
return;
173-
}
174-
return (
175-
<div className={classes.root}>
176-
<canvas ref={setCanvasRef} />
177-
</div>
178-
);
134+
return <div ref={containerRef} style={{ width: "100%", height: "400px" }} />;
179135
}
180136

181137
export function PythProDemoPriceChart() {
182-
/** context */
183138
const { dataSourcesInUse, metrics, selectedSource } =
184139
usePythProAppStateContext();
185140

apps/insights/src/util/pyth-pro-demo/get-color-for-symbol.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Nullish } from "@pythnetwork/shared-lib/types";
22
import { isNullOrUndefined } from "@pythnetwork/shared-lib/util";
33

4+
import { getThemeCssVar } from "./get-theme-css-var";
45
import type { AllDataSourcesType } from "../../schemas/pyth/pyth-pro-demo-schema";
56

67
let palette: Nullish<Partial<Record<AllDataSourcesType, string>>>;
@@ -19,18 +20,16 @@ function hydratePalette() {
1920
if (isNullOrUndefined(doc)) return palette;
2021

2122
if (isNullOrUndefined(palette)) {
22-
const computedStyle = getComputedStyle(document.documentElement);
23-
2423
palette = {
25-
binance: computedStyle.getPropertyValue("--theme-palette-yellow-400"),
26-
bybit: computedStyle.getPropertyValue("--theme-palette-orange-400"),
27-
coinbase: computedStyle.getPropertyValue("--theme-palette-blue-700"),
28-
infoway_io: computedStyle.getPropertyValue("--theme-palette-blue-300"),
29-
okx: computedStyle.getPropertyPriority("--theme-palette-gray-400"),
30-
prime_api: computedStyle.getPropertyValue("--theme-palette-red-600"),
31-
pyth: computedStyle.getPropertyValue("--theme-palette-purple-400"),
32-
pyth_pro: computedStyle.getPropertyValue("--theme-palette-purple-500"),
33-
twelve_data: computedStyle.getPropertyValue("--theme-palette-blue-500"),
24+
binance: getThemeCssVar("--theme-palette-yellow-400") ?? "",
25+
bybit: getThemeCssVar("--theme-palette-orange-400") ?? "",
26+
coinbase: getThemeCssVar("--theme-palette-blue-700") ?? "",
27+
infoway_io: getThemeCssVar("--theme-palette-blue-300") ?? "",
28+
okx: getThemeCssVar("--theme-palette-gray-400") ?? "",
29+
prime_api: getThemeCssVar("--theme-palette-red-600") ?? "",
30+
pyth: getThemeCssVar("--theme-palette-purple-300") ?? "",
31+
pyth_pro: getThemeCssVar("--theme-palette-purple-500") ?? "",
32+
twelve_data: getThemeCssVar("--theme-palette-blue-500") ?? "",
3433
};
3534
}
3635
return palette;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const varCache: Record<string, string> = {};
2+
3+
/**
4+
* attemps to read the value of a CSS variable from the
5+
* available css variables.
6+
* if the variable doesn't exist, and error is thrown
7+
*/
8+
export function getThemeCssVar(varName: string) {
9+
const doc = globalThis.document as typeof globalThis.document | undefined;
10+
if (!doc?.documentElement) return;
11+
12+
const val = varCache[varName];
13+
if (val) return val;
14+
const computed = getComputedStyle(doc.documentElement);
15+
const cssVarVal = computed.getPropertyValue(varName);
16+
17+
varCache[varName] = cssVarVal;
18+
19+
return cssVarVal;
20+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./get-color-for-symbol";
2+
export * from "./get-theme-css-var";
23
export * from "./is-allowed";

0 commit comments

Comments
 (0)