Skip to content

Commit 480e1af

Browse files
committed
data decimation and tooltips working!! + overall ui improvements
1 parent 824f2a4 commit 480e1af

File tree

7 files changed

+435
-254
lines changed

7 files changed

+435
-254
lines changed
Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
import { ReactNode } from "react";
22

33
interface ItemContainerProps {
4-
children: ReactNode;
4+
children: ReactNode | null; // null shows "Data not available"
5+
title: string; // Do we want a standardized title display?
56
}
67

7-
export default function ItemContainer({ children }: ItemContainerProps) {
8+
// Generic wrapper that can contain any visualization. Spares some boilerplate
9+
export default function ItemContainer({ children, title }: ItemContainerProps) {
810
return (
9-
<div className="bg-background-2 rounded-xl p-3">
10-
{children}
11+
<div className="bg-background-2 rounded-xl p-3 min-h-40">
12+
{title && <h6 className="text-lg pl-2 font-semibold">{title}</h6>}
13+
{children ?? (
14+
<>
15+
<br />
16+
<p className="text-center text-foreground-2">
17+
{title} is not currently available.
18+
</p>
19+
</>
20+
)}
1121
</div>
12-
)
13-
};
22+
);
23+
}

frontend/app/components/LineChart.tsx

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
import React, { RefObject, useEffect, useRef } from "react";
2+
import ZoomPlugin from "chartjs-plugin-zoom";
3+
import {
4+
Chart,
5+
ChartItem,
6+
ChartConfiguration,
7+
LineController,
8+
LineElement,
9+
PointElement,
10+
LinearScale,
11+
Title,
12+
CategoryScale,
13+
ChartType,
14+
Legend,
15+
Decimation,
16+
Tooltip,
17+
} from "chart.js";
18+
19+
Chart.register(
20+
LineController,
21+
LineElement,
22+
PointElement,
23+
LinearScale,
24+
CategoryScale,
25+
Title,
26+
Legend,
27+
Decimation,
28+
Tooltip,
29+
ZoomPlugin,
30+
);
31+
32+
// ArrayLike<any> is used for data because chartjs supports TypedArrays and we
33+
// might use those too down the line
34+
interface LineChartProps {
35+
title: string;
36+
numRows: number; // total number of rows of data. Used to rerender the chart on updates
37+
dataX: ArrayLike<any>; // eg. timestamps
38+
dataY: ArrayLike<any>[]; // 1 or more y-value sets
39+
datasetNames?: string[]; // optional list of names to give each y-value set
40+
dataXUnits: string;
41+
dataYUnits: string;
42+
}
43+
44+
const CHART_TYPE: ChartType = "line";
45+
46+
const unique_colors: (keyof DefaultColors)[] = [
47+
"red",
48+
"amber",
49+
"lime",
50+
"cyan",
51+
"blue",
52+
"violet",
53+
"fuchsia",
54+
"pink",
55+
];
56+
import colors from "tailwindcss/colors";
57+
import { DefaultColors } from "tailwindcss/types/generated/colors";
58+
59+
function initChart(
60+
chartRef: RefObject<ChartItem>,
61+
{ dataY, datasetNames, dataXUnits, dataYUnits }: LineChartProps,
62+
) {
63+
const config: ChartConfiguration<ChartType, (typeof dataY)[0], number> = {
64+
type: CHART_TYPE,
65+
data: {
66+
datasets: dataY.map((_, i) => ({
67+
data: [],
68+
label: datasetNames ? datasetNames[i] : `Dataset ${i}`,
69+
backgroundColor: `${colors[unique_colors[i]]["900"]}`,
70+
borderColor: `${colors[unique_colors[i]]["700"]}`,
71+
radius: 0,
72+
})),
73+
// labels: [],
74+
},
75+
options: {
76+
datasets: {
77+
line: {
78+
indexAxis: "x",
79+
parsing: false,
80+
normalized: true,
81+
pointRadius: 0, // too noisy
82+
borderWidth: 2,
83+
borderDash: [],
84+
spanGaps: true,
85+
tension: 0,
86+
stepped: false,
87+
},
88+
},
89+
maintainAspectRatio: false,
90+
responsive: true,
91+
animation: false,
92+
parsing: false,
93+
interaction: {
94+
mode: "index",
95+
// mode: "nearest",
96+
// axis: "x",
97+
intersect: true,
98+
},
99+
plugins: {
100+
decimation: {
101+
enabled: true,
102+
// algorithm: "min-max",
103+
algorithm: "lttb",
104+
// samples: 50,
105+
// threshold: 50,
106+
},
107+
legend: {
108+
display: true,
109+
position: "top",
110+
align: "center",
111+
labels: {
112+
font: { size: 12 },
113+
boxWidth: 8,
114+
boxHeight: 8,
115+
color: colors.neutral[400],
116+
},
117+
},
118+
zoom: {
119+
pan: {
120+
enabled: true,
121+
mode: "x",
122+
},
123+
limits: {
124+
y: { min: "original", max: "original" },
125+
},
126+
zoom: {
127+
mode: "x",
128+
wheel: {
129+
enabled: true,
130+
},
131+
drag: {
132+
enabled: true,
133+
maintainAspectRatio: false,
134+
modifierKey: "shift",
135+
},
136+
pinch: {
137+
enabled: true,
138+
},
139+
},
140+
},
141+
title: {
142+
// We'll use ItemContainer's title instead
143+
display: false,
144+
},
145+
tooltip: {
146+
enabled: true,
147+
animation: {
148+
duration: 100,
149+
},
150+
},
151+
},
152+
aspectRatio: 1.3,
153+
scales: {
154+
x: {
155+
title: {
156+
display: dataXUnits != null,
157+
text: dataXUnits,
158+
font: { size: 14 },
159+
color: colors["neutral"][500],
160+
},
161+
type: "linear",
162+
ticks: {
163+
color: "rgba(255,255,255,.7)",
164+
maxRotation: 0, // Disabled rotation for performance
165+
autoSkip: true,
166+
},
167+
grid: { display: false },
168+
},
169+
y: {
170+
title: {
171+
display: dataYUnits != null,
172+
text: dataYUnits,
173+
font: { size: 14 },
174+
color: colors["neutral"][500],
175+
},
176+
type: "linear",
177+
ticks: {
178+
color: "rgba(255,255,255,.7)",
179+
maxRotation: 0, // Disabled rotation for performance
180+
autoSkip: true,
181+
},
182+
grid: { color: "rgba(255, 255, 255, 0.15)" },
183+
},
184+
},
185+
},
186+
};
187+
188+
return new Chart(chartRef.current!, config);
189+
}
190+
191+
export default function LineChart(props: LineChartProps) {
192+
const { dataX, dataY, numRows, datasetNames: names } = props;
193+
194+
for (const ySet of dataY) {
195+
if (ySet.length != dataX.length) {
196+
throw Error("dataY lists must have the same length as dataX!");
197+
}
198+
}
199+
200+
if (names && dataY.length != names.length) {
201+
throw Error("names list must have the same length as dataY");
202+
}
203+
204+
const chartRef = useRef<ChartItem>(null);
205+
const chartInstanceRef = useRef<Chart<ChartType, (typeof dataY)[0], number> | null>(null);
206+
207+
// // The chart's own list of stored points, of length MAX_LENGTH.
208+
// // Keeping our chart points in a useState requires reassigning a
209+
// // new copy of the list to it on each render. Works for now
210+
// const [dataPoints, setDataPoints] = useState<[number, number][]>([]);
211+
212+
useEffect(() => {
213+
chartInstanceRef.current = initChart(chartRef, props);
214+
215+
return () => {
216+
chartInstanceRef.current?.destroy();
217+
chartInstanceRef.current = null;
218+
};
219+
}, []);
220+
221+
useEffect(() => {
222+
if (chartInstanceRef.current && dataX && dataX.length > 0) {
223+
// assemble data
224+
for (const [idx, ySet] of dataY.filter((d) => d.length > 0).entries()) {
225+
chartInstanceRef.current.data.datasets[idx].data = [
226+
...Array.from(dataX).map((v, i) => ({
227+
x: v,
228+
y: ySet[i],
229+
})),
230+
];
231+
}
232+
//todo: make 10s the min width somehow?
233+
// let data = chartInstanceRef.current.data.datasets[0].data;
234+
// if (data[0][0] - data[0][data.length - 1] < 10) {
235+
// }
236+
237+
// console.log(chartInstanceRef.current.config.options?.indexAxis)
238+
// console.log(chartInstanceRef.current.data.datasets[0].parsing)
239+
240+
// TODO: do we want to set scales manually? It should do this for us
241+
// just fine
242+
// // @ts-ignore-next // We know this exists already from the config above. smh
243+
// chartInstanceRef.current.config.options.scales.y = {
244+
// TODO: setting the min/max on each update causes a sort of
245+
// visual flickering and is probably very laggy.
246+
// min: dataY[0][0],
247+
// max: dataY[0][dataX.length - 1],
248+
// min: Math.floor(dataPoints[0][0]),
249+
// max: Math.ceil(dataPoints[dataPoints.length - 1][0]),
250+
// };
251+
252+
chartInstanceRef.current.update('none');
253+
}
254+
}, [numRows]);
255+
256+
return (
257+
<div className="break-words">
258+
<div className="relative">
259+
{/* for some reason tsserver gets confused here... not idea why */}
260+
<canvas ref={chartRef as any}></canvas>
261+
</div>
262+
</div>
263+
);
264+
}

0 commit comments

Comments
 (0)