Skip to content

[Performance] Using Echarts To Improve Raw Graph Performance #390

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: graph_performance
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
700 changes: 523 additions & 177 deletions gui_dev/bun.lock

Large diffs are not rendered by default.

44 changes: 23 additions & 21 deletions gui_dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,43 @@
"preview": "vite preview"
},
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.5.0",
"@atlaskit/pragmatic-drag-and-drop": "^1.6.0",
"@dnd-kit/core": "^6.3.1",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^6.4.5",
"@mui/material": "^6.4.5",
"@mui/icons-material": "^6.4.11",
"@mui/material": "^6.4.11",
"cbor-js": "^0.1.0",
"echarts": "^5.6.0",
"echarts-for-react": "^3.0.2",
"immer": "^10.1.1",
"plotly.js-basic-dist-min": "^2.35.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"react-plotly.js": "^2.6.0",
"react-router-dom": "^7.2.0",
"zustand": "^5.0.3"
"react-router-dom": "^7.5.3",
"zustand": "^5.0.4"
},
"devDependencies": {
"@babel/core": "^7.26.9",
"@babel/eslint-parser": "^7.26.8",
"@babel/preset-env": "^7.26.9",
"@babel/preset-react": "^7.26.3",
"@eslint/compat": "^1.2.7",
"@vitejs/plugin-react": "^4.3.4",
"@babel/core": "^7.27.1",
"@babel/eslint-parser": "^7.27.1",
"@babel/preset-env": "^7.27.1",
"@babel/preset-react": "^7.27.1",
"@eslint/compat": "^1.2.9",
"@vitejs/plugin-react": "^4.4.1",
"@welldone-software/why-did-you-render": "^10.0.1",
"babel-plugin-react-compiler": "latest",
"eslint": "^9.21.0",
"eslint": "^9.26.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jsdoc": "^50.6.3",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-jsdoc": "^50.6.11",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-compiler": "latest",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"prettier": "^3.5.2",
"react-scan": "^0.1.3",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"prettier": "^3.5.3",
"react-scan": "^0.1.4",
"tiny-invariant": "^1.3.3",
"vite": "^6.1.1"
"vite": "^6.3.5"
}
}
46 changes: 46 additions & 0 deletions gui_dev/src/components/DummyDataGraph.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, { useEffect, useRef, useState } from "react";
import ReactECharts from 'echarts-for-react';

export const DemoChart = () => {
const [data, setData] = useState([[Date.now(), Math.random() * 100]]);
const chartRef = useRef(null);

useEffect(() => {
const timer = setInterval(() => {
setData(prev => [...prev.slice(-50), [Date.now(), Math.random() * 100]]);
}, 1000);

return () => clearInterval(timer);
}, []);

const option = {
xAxis: { type: 'time' },
yAxis: { type: 'value' },
series: [
{
name: '1',
type: 'line',
data: data,
showSymbol: false,
stack: 'Total',
},
{
name: '2',
type: 'line',
data: data,
showSymbol: false,
stack: 'Total',
},
{
name: '3',
type: 'line',
data: data,
showSymbol: false,
stack: 'Total',
},

]
};

// return <ReactECharts ref={chartRef} option={option} style={{ height: 400 }} />;
};
122 changes: 36 additions & 86 deletions gui_dev/src/components/RawDataGraph.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState, useMemo } from "react";
import { useSocketStore } from "@/stores";
import { useSessionStore } from "@/stores/sessionStore";
import Plotly from "plotly.js-basic-dist-min";
import ReactECharts from 'echarts-for-react';
import {
Box,
Typography,
Expand Down Expand Up @@ -161,91 +161,9 @@ export const RawDataGraph = ({
}, [graphRawData, availableChannels, maxDataPoints]);

useEffect(() => {
if (!graphRef.current) return;



if (selectedChannels.length === 0) {
Plotly.purge(graphRef.current);
return;
}

const colors = generateColors(selectedChannels.length);

const totalChannels = selectedChannels.length;
const domainHeight = 1 / totalChannels;

const yAxes = {};
const maxVal = yAxisMaxValue !== "Auto" ? Number(yAxisMaxValue) : null;

selectedChannels.forEach((channelName, idx) => {
const start = 1 - (idx + 1) * domainHeight;
const end = 1 - idx * domainHeight;

const yAxisKey = `yaxis${idx === 0 ? "" : idx + 1}`;

yAxes[yAxisKey] = {
domain: [start, end],
nticks: 5,
tickfont: {
size: 10,
color: "#cccccc",
},
// Titles necessary? Legend works but what if people are color blind? Rotate not supported! Annotations are a possibility though
// title: {
// text: channelName,
// font: { color: "#f4f4f4", size: 12 },
// standoff: 30,
// textangle: -90,
// },
color: "#cccccc",
automargin: true,
};

if (maxVal !== null) {
yAxes[yAxisKey].range = [-maxVal, maxVal];
}
});

const traces = selectedChannels.map((channelName, idx) => {
const yData = rawData[channelName] || [];
const y = yData.slice().reverse();
const x = Array.from({ length: y.length }, (_, i) => i / samplingRate); // Convert samples to negative seconds

return {
x,
y,
type: "scattergl",
mode: "lines",
name: channelName,
hoverinfo: 'skip',
line: { simplify: false, color: colors[idx] },
yaxis: idx === 0 ? "y" : `y${idx + 1}`,
};
});

const layout = {
...layoutRef.current,
xaxis: {
...layoutRef.current.xaxis,
autorange: "reversed",
range: [maxDataPoints / samplingRate, 0], // Adjust range to negative seconds
domain: [0, 1],
anchor: totalChannels === 1 ? "y" : `y${totalChannels}`,
},
...yAxes,
height: 350, // TODO height autoadjust to screen
hovermode: false, // Add this line to disable hovermode in the trace
};

Plotly.react(graphRef.current, traces, layout, {
responsive: true,
displayModeBar: false,
})
.then((gd) => {
plotlyRef.current = gd;
})
.catch((error) => {
console.error("Plotly error:", error);
});
}, [rawData, selectedChannels, yAxisMaxValue, maxDataPoints]);

return (
Expand Down Expand Up @@ -318,7 +236,39 @@ export const RawDataGraph = ({
</Box>
</Box>

<div ref={graphRef} style={{ width: "100%" }}></div>
<ReactECharts
option={{
animation: false,
grid: { top: 40, right: 40, bottom: 40, left: 60 },
xAxis: {
type: 'value',
inverse: true, // Reversed time axis
min: 0,
max: maxDataPoints / samplingRate,
axisLabel: { formatter: '{value}s' }
},
yAxis: {
type: 'value',
min: yAxisMaxValue === "Auto" ? null : -Number(yAxisMaxValue),
max: yAxisMaxValue === "Auto" ? null : Number(yAxisMaxValue)
},
series: selectedChannels.map((channelName, idx) => ({
name: channelName,
type: 'line',
showSymbol: false,
data: (rawData[channelName] || [])
.slice()
.reverse()
.map((value, i) => [i / samplingRate, value]),
lineStyle: {
width: 1,
color: generateColors(selectedChannels.length)[idx]
}
}))
}}
style={{ height: 400, width: '100%' }}
opts={{ renderer: 'canvas' }}
/>
</Box>
);
};
6 changes: 6 additions & 0 deletions gui_dev/src/pages/Dashboard.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { RawDataGraph } from '@/components/RawDataGraph';
import { DemoChart } from '@/components/DummyDataGraph';
import { PSDGraph } from '@/components/PSDGraph';
import { DecodingGraph } from '@/components/DecodingGraph';
import { HeatmapGraph } from '@/components/HeatmapGraph';
Expand All @@ -18,6 +19,7 @@ export const Dashboard = () => {
const stopStream = useSessionStore((state) => state.stopStream);

const [enabledGraphs, setEnabledGraphs] = useState({
dummyData: true,
rawData: true,
psdPlot: true,
heatmap: true,
Expand All @@ -33,6 +35,7 @@ export const Dashboard = () => {
};

const graphComponents = {
dummyData: DemoChart,
rawData: RawDataGraph,
psdPlot: PSDGraph,
heatmap: HeatmapGraph,
Expand Down Expand Up @@ -63,6 +66,9 @@ export const Dashboard = () => {
onChange={handleGraphToggle}
aria-label="graph toggle"
>
<ToggleButton value="dummyData" aria-label="dummy data">
Dummy Data
</ToggleButton>
<ToggleButton value="rawData" aria-label="raw data">
Raw Data
</ToggleButton>
Expand Down
36 changes: 36 additions & 0 deletions gui_dev/src/stores/socketStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,16 @@ export const useSocketStore = createStore("socket", (set, get) => ({
error: null,
graphData: [],
graphRawData: [],
accuRawData: [],
graphDecodingData: [],
availableDecodingOutputs: [],
infoMessages: [],
reconnectTimer: null,
intentionalDisconnect: false,
messageCount: 0,
maxDataPoints: 10000,

setMaxDataPoints: (maxDataPoints) => set({ maxDataPoints }),

setSocket: (socket) => set({ socket }),

Expand Down Expand Up @@ -84,6 +88,7 @@ export const useSocketStore = createStore("socket", (set, get) => ({
const decodedData = CBOR.decode(event.data);
if (Object.keys(decodedData)[0] == "raw_data") {
set({ graphRawData: decodedData.raw_data });
get().setAccuRawData();
} else {
// check here if there are values in decodedData that start with "decoding"
// if so, set graphDecodingData to the value of those keys
Expand Down Expand Up @@ -217,4 +222,35 @@ export const useSocketStore = createStore("socket", (set, get) => ({
};
}
},

setAccuRawData: () => {
const { graphRawData, maxDataPoints } = get();
const updatedAccuRawData = { ...get().accuRawData };

Object.entries(graphRawData).forEach(([key, value]) => {

if (!updatedAccuRawData[key]) {
updatedAccuRawData[key] = [];
}

updatedAccuRawData[key].push(...value);

// Trim data to `maxDataPoints`
if (updatedAccuRawData[key].length > maxDataPoints) {
updatedAccuRawData[key] = updatedAccuRawData[key].slice(-maxDataPoints);
}
});

set({ accuRawData: updatedAccuRawData });
},

getAccuRawData: (selectedChannels) => {
let rawData = get().accuRawData;

let filteredRawData = Object.fromEntries(
Object.entries(rawData).filter( ([key,valye]) => selectedChannels.includes(key) )
);

return filteredRawData;
}
}));
Loading