diff --git a/gui_dev/src/components/BandPowerGraph.jsx b/gui_dev/src/components/BandPowerGraph.jsx index 00059d70..26db7db1 100644 --- a/gui_dev/src/components/BandPowerGraph.jsx +++ b/gui_dev/src/components/BandPowerGraph.jsx @@ -121,6 +121,7 @@ export const BandPowerGraph = () => { height: 350, paper_bgcolor: "#333", plot_bgcolor: "#333", + hovermode: false, // Add this line to disable hovermode xaxis: { title: { text: "Frequency Band", font: { color: "#f4f4f4" } }, color: "#cccccc", @@ -141,6 +142,7 @@ export const BandPowerGraph = () => { x: data.features, y: data.values, type: "bar", + hoverinfo: 'skip', name: data.channelName, marker: { color: barColors }, }; diff --git a/gui_dev/src/components/DecodingGraph.jsx b/gui_dev/src/components/DecodingGraph.jsx index e757bacc..cbea6793 100644 --- a/gui_dev/src/components/DecodingGraph.jsx +++ b/gui_dev/src/components/DecodingGraph.jsx @@ -33,7 +33,7 @@ export const DecodingGraph = ({ }) => { //const graphData = useSocketStore((state) => state.graphData); const graphDecodingData = useSocketStore((state) => state.graphDecodingData); - + const samplingRate = useSessionStore((state) => state.streamParameters.samplingRateFeatures); //const channels = useSessionStore((state) => state.channels, shallow); //const usedChannels = useMemo( @@ -68,6 +68,7 @@ export const DecodingGraph = ({ height: 400, paper_bgcolor: "#333", plot_bgcolor: "#333", + hovermode: false, // Add this line to disable hovermode margin: { l: 50, r: 50, @@ -110,7 +111,7 @@ export const DecodingGraph = ({ }; const handleMaxDataPointsChangeDecoding = (event, newValue) => { - setMaxDataPointsDecoding(newValue); + setMaxDataPointsDecoding(newValue * samplingRate); // Convert seconds to number of samples }; //useEffect(() => { @@ -218,13 +219,14 @@ export const DecodingGraph = ({ const traces = selectedDecodingOutputs.map((decodingOutput, idx) => { const yData = decodingData[decodingOutput] || []; const y = yData.slice().reverse(); - const x = Array.from({ length: y.length }, (_, i) => i); + const x = Array.from({ length: y.length }, (_, i) => i / samplingRate); // Convert to time in seconds return { x, y, type: "scattergl", mode: "lines", + hoverinfo: 'skip', name: decodingOutput, line: { simplify: false, color: colors[idx] }, yaxis: idx === 0 ? "y" : `y${idx + 1}`, @@ -236,9 +238,13 @@ export const DecodingGraph = ({ xaxis: { ...layoutRef.current.xaxis, autorange: "reversed", - range: [0, maxDataPointsDecoding], + range: [0, maxDataPointsDecoding / samplingRate], // Adjust range to time in seconds domain: [0, 1], anchor: totalDecodingOutputs === 1 ? "y" : `y${totalDecodingOutputs}`, + title: { + text: "Time [s]", // Update x-axis title + font: { color: "#f4f4f4" }, + }, }, ...yAxes, height: 350, // TODO height autoadjust to screen @@ -254,7 +260,7 @@ export const DecodingGraph = ({ .catch((error) => { console.error("Plotly error:", error); }); - }, [decodingData, selectedDecodingOutputs, yAxisMaxValue, maxDataPointsDecoding]); + }, [decodingData, selectedDecodingOutputs, yAxisMaxValue, maxDataPointsDecoding, samplingRate]); return ( @@ -310,20 +316,20 @@ export const DecodingGraph = ({ + title="Window Size (s)" defaultExpanded={true} id="BoxDecoding"> - Current Value: {maxDataPointsDecoding} + Current Value: {(maxDataPointsDecoding / samplingRate).toFixed(1)} s diff --git a/gui_dev/src/components/HeatmapGraph.jsx b/gui_dev/src/components/HeatmapGraph.jsx index 8cc80e71..504ce317 100644 --- a/gui_dev/src/components/HeatmapGraph.jsx +++ b/gui_dev/src/components/HeatmapGraph.jsx @@ -9,15 +9,13 @@ import { FormControlLabel, Radio, Checkbox, + Slider, } from '@mui/material'; import { CollapsibleBox } from './CollapsibleBox'; import { getChannelAndFeature } from './utils'; import { shallow } from 'zustand/shallow'; -const maxTimeWindow = 10; - export const HeatmapGraph = () => { - const channels = useSessionStore((state) => state.channels, shallow); const usedChannels = useMemo( @@ -30,35 +28,41 @@ export const HeatmapGraph = () => { [usedChannels] ); - const [selectedChannel, setSelectedChannel] = useState(''); // TODO: Switch this maybe multiple? + const [selectedChannel, setSelectedChannel] = useState(''); const [features, setFeatures] = useState([]); - const [fftFeatures, setFftFeatures] = useState([]); + const [fftFeatures, setFftFeatures] = useState([]); const [otherFeatures, setOtherFeatures] = useState([]); const [selectedFeatures, setSelectedFeatures] = useState([]); const [heatmapData, setHeatmapData] = useState({ x: [], z: [] }); const [isDataStale, setIsDataStale] = useState(false); const [lastDataTime, setLastDataTime] = useState(null); - const [lastDataTimestamp, setLastDataTimestamp] = useState(null); - const hasInitialized = useRef(Date.now()); const graphData = useSocketStore((state) => state.graphData); + const [maxDataPoints, setMaxDataPoints] = useState(100); + + const handleMaxDataPointsChange = (event, newValue) => { + setMaxDataPoints(newValue); + }; + const handleChannelToggle = (event) => { setSelectedChannel(event.target.value); }; - // Added handler for fft_psd_xyz features toggle TODO: also welch/ other features? const handleFftFeaturesToggle = () => { - const allFftFeaturesSelected = fftFeatures.every((feature) => selectedFeatures.includes(feature)); + const allFftFeaturesSelected = fftFeatures.every((feature) => + selectedFeatures.includes(feature) + ); if (allFftFeaturesSelected) { setSelectedFeatures((prevSelected) => prevSelected.filter((feature) => !fftFeatures.includes(feature)) ); } else { - setSelectedFeatures((prevSelected) => - [...prevSelected, ...fftFeatures.filter((feature) => !prevSelected.includes(feature))] - ); + setSelectedFeatures((prevSelected) => [ + ...prevSelected, + ...fftFeatures.filter((feature) => !prevSelected.includes(feature)), + ]); } }; @@ -86,47 +90,45 @@ export const HeatmapGraph = () => { const featureKeys = dataKeys.filter( (key) => key.startsWith(channelPrefix) && key !== 'time' ); - const newFeatures = featureKeys.map((key) => key.substring(channelPrefix.length)); + const newFeatures = featureKeys.map((key) => + key.substring(channelPrefix.length) + ); if (JSON.stringify(newFeatures) !== JSON.stringify(features)) { console.log('Updating features:', newFeatures); setFeatures(newFeatures); - // TODO: currently all psd selectable together. should we also do this for welch and other features? - const fftFeatures = newFeatures.filter((feature) => feature.startsWith('fft_psd_')); - const otherFeatures = newFeatures.filter((feature) => !feature.startsWith('fft_psd_')); + const fftFeatures = newFeatures.filter((feature) => + feature.startsWith('fft_psd_') + ); + const otherFeatures = newFeatures.filter( + (feature) => !feature.startsWith('fft_psd_') + ); setFftFeatures(fftFeatures); setOtherFeatures(otherFeatures); setSelectedFeatures(newFeatures); - setHeatmapData({ x: [], z: [] }); // Reset heatmap data when features change + setHeatmapData({ x: [], z: [] }); setIsDataStale(false); setLastDataTime(null); - setLastDataTimestamp(null); } }, [graphData, selectedChannel, features]); useEffect(() => { - if (!graphData || !selectedChannel || features.length === 0 || selectedFeatures.length === 0) return; - - // TODO: Always data in ms? (Time conversion here always necessary?) - // Timon: yes, let's define that the stream's time is always in ms - let timestamp = graphData.time; - if (timestamp === undefined) { - timestamp = (Date.now() - hasInitialized.current) / 1000; - } else { - timestamp = timestamp / 1000; - } + if ( + !graphData || + !selectedChannel || + features.length === 0 || + selectedFeatures.length === 0 + ) + return; setLastDataTime(Date.now()); setIsDataStale(false); - let x = [...heatmapData.x, timestamp]; - let z; - // Initialize 'z' for selected features if (heatmapData.z && heatmapData.z.length === selectedFeatures.length) { z = heatmapData.z.map((row) => [...row]); } else { @@ -137,27 +139,30 @@ export const HeatmapGraph = () => { const key = `${selectedChannel}_${featureName}`; const value = graphData[key]; const numericValue = typeof value === 'number' && !isNaN(value) ? value : 0; - z[idx].push(numericValue); - }); - - const currentTime = timestamp; - const minTime = currentTime - maxTimeWindow; // TODO: What should be the visible window frame? adjustable? 10s? - // Timon: Would be amazing if it's adjustable - const validIndices = x.reduce((indices, time, index) => { - if (time >= minTime) { - indices.push(index); + // Shift existing data to the left if necessary + if (z[idx].length >= maxDataPoints) { + z[idx].shift(); } - return indices; - }, []); - x = validIndices.map((index) => x[index]); - z = z.map((row) => validIndices.map((index) => row[index])); + // Append the new data + z[idx].push(numericValue); + }); + + // Update x based on the length of z[0] (assuming all rows are the same length) + const dataLength = z[0]?.length || 0; + const x = Array.from({ length: dataLength }, (_, i) => i); setHeatmapData({ x, z }); - }, [graphData, selectedChannel, features, selectedFeatures]); - - // Check if data is stale (no new data in the last second) -> TODO: Find better solution debug this + }, [ + graphData, + selectedChannel, + features, + selectedFeatures, + maxDataPoints, + ]); + + // Check if data is stale (no new data in the last second) useEffect(() => { if (!lastDataTime) return; @@ -172,27 +177,20 @@ export const HeatmapGraph = () => { return () => clearInterval(interval); }, [lastDataTime]); - // TODO: Adjustment of x-axis -> this currently is a bit buggy - const xRange = isDataStale && heatmapData.x.length > 0 - ? [heatmapData.x[0], heatmapData.x[heatmapData.x.length - 1]] - : undefined; - const layout = { - // title: { text: 'Heatmap', font: { color: '#f4f4f4' } }, - height: 600, + height: 350, paper_bgcolor: '#333', plot_bgcolor: '#333', autosize: true, xaxis: { - tickformat: '.2f', - title: { text: 'Time (s)', font: { color: '#f4f4f4' } }, + title: { text: 'Nr. of Samples', font: { color: '#f4f4f4' } }, color: '#cccccc', tickfont: { color: '#cccccc', }, automargin: false, - autorange: !isDataStale, - range: xRange, + hovermode: false + // autorange: 'reversed' }, yaxis: { title: { text: 'Features', font: { color: '#f4f4f4' } }, @@ -213,18 +211,14 @@ export const HeatmapGraph = () => { Heatmap - + - + {usedChannels.map((channel, index) => ( } // TODO: Should we make multiple selectable? // Timon: No, let's keep with one + control={} label={channel.name} /> ))} @@ -232,7 +226,7 @@ export const HeatmapGraph = () => { - + @@ -240,10 +234,16 @@ export const HeatmapGraph = () => { selectedFeatures.includes(feature))} + checked={fftFeatures.every((feature) => + selectedFeatures.includes(feature) + )} indeterminate={ - fftFeatures.some((feature) => selectedFeatures.includes(feature)) && - !fftFeatures.every((feature) => selectedFeatures.includes(feature)) + fftFeatures.some((feature) => + selectedFeatures.includes(feature) + ) && + !fftFeatures.every((feature) => + selectedFeatures.includes(feature) + ) } onChange={handleFftFeaturesToggle} color="primary" @@ -269,25 +269,43 @@ export const HeatmapGraph = () => { + + + Current Value: {maxDataPoints} + + + - {heatmapData.x.length > 0 && selectedFeatures.length > 0 && heatmapData.z.length > 0 && ( - - )} + {heatmapData.x.length > 0 && + selectedFeatures.length > 0 && + heatmapData.z.length > 0 && ( + + )} ); -}; +}; \ No newline at end of file diff --git a/gui_dev/src/components/PSDGraph.jsx b/gui_dev/src/components/PSDGraph.jsx index 91438f29..d365ff9d 100644 --- a/gui_dev/src/components/PSDGraph.jsx +++ b/gui_dev/src/components/PSDGraph.jsx @@ -135,6 +135,7 @@ export const PSDGraph = () => { height: 350, paper_bgcolor: "#333", plot_bgcolor: "#333", + hovermode: false, // Add this line to disable hovermode xaxis: { title: { text: "Feature Index", font: { color: "#f4f4f4" } }, color: "#cccccc", @@ -156,6 +157,7 @@ export const PSDGraph = () => { y: data.values, type: "scattergl", mode: "lines", + hoverinfo: "skip", name: data.channelName, line: { simplify: false, color: colors[idx] }, })); diff --git a/gui_dev/src/components/RawDataGraph.jsx b/gui_dev/src/components/RawDataGraph.jsx index 62087e4f..32f3dde0 100644 --- a/gui_dev/src/components/RawDataGraph.jsx +++ b/gui_dev/src/components/RawDataGraph.jsx @@ -28,13 +28,14 @@ const generateColors = (numColors) => { export const RawDataGraph = ({ title = "Raw Data", - xAxisTitle = "Nr. of Samples", + xAxisTitle = "Time [s]", yAxisTitle = "Value", }) => { //const graphData = useSocketStore((state) => state.graphData); const graphRawData = useSocketStore((state) => state.graphRawData); const channels = useSessionStore((state) => state.channels, shallow); + const samplingRate = useSessionStore((state) => state.streamParameters.samplingRate); const usedChannels = useMemo( () => channels.filter((channel) => channel.used === 1), @@ -63,6 +64,7 @@ export const RawDataGraph = ({ height: 400, paper_bgcolor: "#333", plot_bgcolor: "#333", + hovermode: false, // Add this line to disable hovermode margin: { l: 50, r: 50, @@ -100,12 +102,12 @@ export const RawDataGraph = ({ }); }; - const handleYAxisMaxValueChange = (event) => { - setYAxisMaxValue(event.target.value); + const handleYAxisMaxValueChange = (event, newValue) => { + setYAxisMaxValue(newValue); }; const handleMaxDataPointsChange = (event, newValue) => { - setMaxDataPoints(newValue); + setMaxDataPoints(newValue * samplingRate); // Convert seconds to samples }; useEffect(() => { @@ -206,7 +208,7 @@ export const RawDataGraph = ({ const traces = selectedChannels.map((channelName, idx) => { const yData = rawData[channelName] || []; const y = yData.slice().reverse(); - const x = Array.from({ length: y.length }, (_, i) => i); + const x = Array.from({ length: y.length }, (_, i) => i / samplingRate); // Convert samples to negative seconds return { x, @@ -214,6 +216,7 @@ export const RawDataGraph = ({ type: "scattergl", mode: "lines", name: channelName, + hoverinfo: 'skip', line: { simplify: false, color: colors[idx] }, yaxis: idx === 0 ? "y" : `y${idx + 1}`, }; @@ -224,12 +227,13 @@ export const RawDataGraph = ({ xaxis: { ...layoutRef.current.xaxis, autorange: "reversed", - range: [0, maxDataPoints], + 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, { @@ -280,37 +284,34 @@ export const RawDataGraph = ({ - - - } label="Auto" /> - } label="5" /> - } label="10" /> - } label="20" /> - } label="50" /> - } label="100" /> - } label="500" /> - - + - Current Value: {maxDataPoints} + Current Value: {maxDataPoints / samplingRate} diff --git a/gui_dev/src/pages/Dashboard.jsx b/gui_dev/src/pages/Dashboard.jsx index f7b07dcc..0402aed0 100644 --- a/gui_dev/src/pages/Dashboard.jsx +++ b/gui_dev/src/pages/Dashboard.jsx @@ -3,9 +3,10 @@ import { PSDGraph } from '@/components/PSDGraph'; import { DecodingGraph } from '@/components/DecodingGraph'; import { HeatmapGraph } from '@/components/HeatmapGraph'; import { BandPowerGraph } from '@/components/BandPowerGraph'; -import { Box, Button } from '@mui/material'; +import { Box, Button, ToggleButton, ToggleButtonGroup } from '@mui/material'; import { useSessionStore } from "@/stores"; import { useSocketStore } from '@/stores'; +import { useState } from 'react'; export const Dashboard = () => { @@ -16,74 +17,86 @@ export const Dashboard = () => { const startStream = useSessionStore((state) => state.startStream); const stopStream = useSessionStore((state) => state.stopStream); - + const [enabledGraphs, setEnabledGraphs] = useState({ + rawData: true, + psdPlot: true, + heatmap: true, + bandPowerGraph: true, + decodingGraph: true, + }); + + const handleGraphToggle = (event, newEnabledGraphs) => { + setEnabledGraphs((prev) => ({ + ...prev, + [event.target.value]: newEnabledGraphs.includes(event.target.value), + })); + }; + + const graphComponents = { + rawData: RawDataGraph, + psdPlot: PSDGraph, + heatmap: HeatmapGraph, + bandPowerGraph: BandPowerGraph, + decodingGraph: DecodingGraph, + }; return ( <> - {/* */} - - + + + + + + enabledGraphs[key])} + onChange={handleGraphToggle} + aria-label="graph toggle" + > + + Raw Data + + + PSD Plot + + + Heatmap + + + Band Power Graph + + + Decoding Graph + + + - {/* Top Row - RawDataGraph and PSDGraph */} - - {/* RawDataGraph */} - - - - - - - {/* PSDGraph */} - - - - - - - {/* HeatmapGraph */} - - - - - - - - - {/* BandPowerGraph */} - - - - - - {/* DecoddingGraph */} - - - - - + {Object.keys(enabledGraphs).filter((key) => enabledGraphs[key]).map((key) => { + const GraphComponent = graphComponents[key]; + return ( + + + + + + ); + })} + );