From 70188e9d90a8a71d35a110497691914c1497dd45 Mon Sep 17 00:00:00 2001 From: qitech Date: Mon, 8 Dec 2025 14:35:01 +0100 Subject: [PATCH 1/9] button to add new marker --- .../src/machines/mock/mock1/Mock1Graph.tsx | 122 ++++++++++++++++-- 1 file changed, 114 insertions(+), 8 deletions(-) diff --git a/electron/src/machines/mock/mock1/Mock1Graph.tsx b/electron/src/machines/mock/mock1/Mock1Graph.tsx index 5f0805d96..a9e473a2f 100644 --- a/electron/src/machines/mock/mock1/Mock1Graph.tsx +++ b/electron/src/machines/mock/mock1/Mock1Graph.tsx @@ -6,6 +6,7 @@ import { type GraphConfig, } from "@/components/graph"; import React from "react"; +import { useState } from "react"; import { useMock1 } from "./useMock"; import { TimeSeriesValue, type Series, TimeSeries } from "@/lib/timeseries"; @@ -13,6 +14,96 @@ export function Mock1GraphPage() { const { sineWaveSum } = useMock1(); const syncHook = useGraphSync("mock-graphs"); + const graph1Ref = React.useRef(null); + const [marker, setMarker] = useState(null); + const [markers, setMarkers] = useState<{ timestamp: number; name: string }[]>([]); + + const handleAddMarker = () => { + const inputEl = document.getElementById("marker-input") as HTMLInputElement; + if (inputEl && sineWaveSum.current) { + const ts = sineWaveSum.current.timestamp; + const name = inputEl.value; + setMarkers((prev) => [...prev, { timestamp: ts, name }]); + const tsStr = new Date(ts).toLocaleTimeString("en-GB", { hour12: false }); + setMarker(`${name} @ ${tsStr}`); + } else { + setMarker("No data"); + } + }; + + function createMarkerElement( + timestamp: number, + value: number, + name: string, + startTime: number, + endTime: number, + graphWidth: number, + graphHeight: number, + ) { + const ratio = (timestamp - startTime) / (endTime - startTime); + const xPos = Math.min(Math.max(ratio, 0), 1) * graphWidth; + const yPos = graphHeight - value; + + + const line = document.createElement("div"); + line.style.position = "absolute"; + line.style.left = `${xPos}px`; + line.style.top = `${yPos}px`; + line.style.height = `${value}px`; + line.style.width = "2px"; + line.style.background = "black"; + line.className = "vertical-marker"; + + const label = document.createElement("div"); + label.textContent = name; + label.style.position = "absolute"; + label.style.left = `${xPos}px`; + label.style.top = `${yPos - 16}px`; + label.style.transform = "translateX(-50%)"; + label.style.color = "black"; + label.style.fontSize = "12px"; + label.style.padding = "0 2px"; + label.style.whiteSpace = "nowrap"; + label.className = "marker-label"; + + return { line, label }; + } + + React.useEffect(() => { + if (!graph1Ref.current || !sineWaveSum.current) return; + const graphEl = graph1Ref.current; + const graphWidth = graphEl.clientWidth; + const graphHeight = graphEl.clientHeight; + + // Remove previous markers and labels + graphEl.querySelectorAll(".vertical-marker, .marker-label").forEach((el) => el.remove()); + + const endTime = sineWaveSum.current.timestamp; + const startTime = endTime - (singleGraphConfig.defaultTimeWindow as number); + const graphMin = -1; // TODO: do it in general case not hardcord + const graphMax = 1; // TODO: do it in general case not hardcord + + markers.forEach(({ timestamp, name }) => { + const closest = sineWaveSum.long.values + .filter((v): v is TimeSeriesValue => v !== null) + .reduce((prev, curr) => + Math.abs(curr.timestamp - timestamp) < Math.abs(prev.timestamp - timestamp) ? curr : prev + ); + const valueY = ((closest.value - graphMin) / (graphMax - graphMin)) * graphHeight; + const { line, label } = createMarkerElement( + timestamp, + valueY, + name, + startTime, + endTime, + graphWidth, + graphHeight, + ); + + graphEl.appendChild(line); + graphEl.appendChild(label); + }); + }, [markers, sineWaveSum.current]); const config: GraphConfig = { title: "Sine Wave", @@ -98,14 +189,29 @@ export function Mock1GraphPage() { return (
- value.toFixed(3)} - graphId="single-graph1" - /> +
+ value.toFixed(3)} + graphId="single-graph1" + /> +
+
+ Add data marker + + +

{marker ?? "No data"}

+
Date: Mon, 8 Dec 2025 16:01:45 +0100 Subject: [PATCH 2/9] success to adjust with the time --- .../src/machines/mock/mock1/Mock1Graph.tsx | 97 ++++++++++++------- 1 file changed, 61 insertions(+), 36 deletions(-) diff --git a/electron/src/machines/mock/mock1/Mock1Graph.tsx b/electron/src/machines/mock/mock1/Mock1Graph.tsx index a9e473a2f..c2fc3ca31 100644 --- a/electron/src/machines/mock/mock1/Mock1Graph.tsx +++ b/electron/src/machines/mock/mock1/Mock1Graph.tsx @@ -69,42 +69,6 @@ export function Mock1GraphPage() { return { line, label }; } - React.useEffect(() => { - if (!graph1Ref.current || !sineWaveSum.current) return; - const graphEl = graph1Ref.current; - const graphWidth = graphEl.clientWidth; - const graphHeight = graphEl.clientHeight; - - // Remove previous markers and labels - graphEl.querySelectorAll(".vertical-marker, .marker-label").forEach((el) => el.remove()); - - const endTime = sineWaveSum.current.timestamp; - const startTime = endTime - (singleGraphConfig.defaultTimeWindow as number); - const graphMin = -1; // TODO: do it in general case not hardcord - const graphMax = 1; // TODO: do it in general case not hardcord - - markers.forEach(({ timestamp, name }) => { - const closest = sineWaveSum.long.values - .filter((v): v is TimeSeriesValue => v !== null) - .reduce((prev, curr) => - Math.abs(curr.timestamp - timestamp) < Math.abs(prev.timestamp - timestamp) ? curr : prev - ); - const valueY = ((closest.value - graphMin) / (graphMax - graphMin)) * graphHeight; - const { line, label } = createMarkerElement( - timestamp, - valueY, - name, - startTime, - endTime, - graphWidth, - graphHeight, - ); - - graphEl.appendChild(line); - graphEl.appendChild(label); - }); - }, [markers, sineWaveSum.current]); - const config: GraphConfig = { title: "Sine Wave", defaultTimeWindow: 30 * 60 * 1000, @@ -186,6 +150,67 @@ export function Mock1GraphPage() { title: "Sine Wave", }; + const [timeTick, setTimeTick] = useState(0); + + // Component be re-rendered by updating 'timeTick'. + React.useEffect(() => { + if (!sineWaveSum.current) return; + + const intervalId = setInterval(() => { + setTimeTick(prev => prev + 1); + }, 50); + + return () => clearInterval(intervalId); + }, [sineWaveSum.current]); + + // Draw the marker + React.useEffect(() => { + if (!graph1Ref.current || !sineWaveSum.current) return; + const graphEl = graph1Ref.current; + const graphWidth = graphEl.clientWidth; + const graphHeight = graphEl.clientHeight; + + // Remove previous markers and labels + graphEl.querySelectorAll(".vertical-marker, .marker-label").forEach((el) => el.remove()); + + const currentTimeWindow = syncHook.controlProps.timeWindow; + const defaultDuration = singleGraphConfig.defaultTimeWindow as number; + const validTimeWindowMs = + (typeof currentTimeWindow === 'number' && currentTimeWindow) || + defaultDuration; + const endTime = sineWaveSum.current.timestamp; + const startTime = endTime - validTimeWindowMs; + + const graphMin = -1; + const graphMax = 1; + + markers.forEach(({ timestamp, name }) => { + if (timestamp >= startTime && timestamp <= endTime) { + const closest = sineWaveSum.long.values + .filter((v): v is TimeSeriesValue => v !== null) + .reduce((prev, curr) => + Math.abs(curr.timestamp - timestamp) < Math.abs(prev.timestamp - timestamp) ? curr : prev + ); + if (!closest) return; + + const valueY = ((closest.value - graphMin) / (graphMax - graphMin)) * graphHeight; + + const { line, label } = createMarkerElement( + timestamp, + valueY, + name, + startTime, + endTime, + graphWidth, + graphHeight, + ); + + graphEl.appendChild(line); + graphEl.appendChild(label); + } + }); + }, [markers, sineWaveSum.current, timeTick, singleGraphConfig.defaultTimeWindow, syncHook.controlProps.timeWindow]); + return (
From 7469db2a596e780edd857d2894c8fc69ef3b1e79 Mon Sep 17 00:00:00 2001 From: qitech Date: Tue, 9 Dec 2025 11:51:45 +0100 Subject: [PATCH 3/9] timestamp for mock graphs --- .../graph/GraphWithMarkerControls.tsx | 211 ++++++++++++++++++ .../src/machines/mock/mock1/Mock1Graph.tsx | 175 +++------------ 2 files changed, 240 insertions(+), 146 deletions(-) create mode 100644 electron/src/components/graph/GraphWithMarkerControls.tsx diff --git a/electron/src/components/graph/GraphWithMarkerControls.tsx b/electron/src/components/graph/GraphWithMarkerControls.tsx new file mode 100644 index 000000000..176301c53 --- /dev/null +++ b/electron/src/components/graph/GraphWithMarkerControls.tsx @@ -0,0 +1,211 @@ +import React, { useState, useRef, useCallback, useEffect } from "react"; +import { + AutoSyncedBigGraph, + useGraphSync, + type GraphConfig, +} from "@/components/graph"; +import { TimeSeries, TimeSeriesValue } from "@/lib/timeseries"; +import { Unit } from "@/control/units"; + +type TimeSeriesData = { + newData: TimeSeries | null; + title?: string; + color?: string; + lines?: any[]; +}; + +type GraphWithMarkerControlsProps = { + syncHook: ReturnType; + newData: TimeSeriesData | TimeSeriesData[]; + config: GraphConfig; + unit?: Unit; + renderValue?: (value: number) => string; + graphId: string; + // The raw TimeSeries object for capturing timestamps. + currentTimeSeries: TimeSeries | null; + yAxisScale?: { // TODO: why is it neccessary? + min: number; + max: number + }; +}; + +function createMarkerElement( + timestamp: number, + value: number, + name: string, + startTime: number, + endTime: number, + graphWidth: number, + graphHeight: number, +) { + // Calculate the position of the timestamp + const ratio = (timestamp - startTime) / (endTime - startTime); + const xPos = Math.min(Math.max(ratio, 0), 1) * graphWidth; + const yPos = graphHeight - value; + + const line = document.createElement("div"); + line.style.position = "absolute"; + line.style.left = `${xPos}px`; + line.style.top = `${yPos}px`; + line.style.height = `${value}px`; + line.style.width = "2px"; + line.style.background = "rgba(0, 0, 0, 0.5)"; + line.className = "vertical-marker"; + + const label = document.createElement("div"); + label.textContent = name; + label.style.position = "absolute"; + label.style.left = `${xPos}px`; + label.style.top = `${yPos - 20}px`; + label.style.transform = "translateX(-50%)"; + label.style.color = "black"; + label.style.padding = "2px 4px"; + label.style.fontSize = "12px"; + label.style.whiteSpace = "nowrap"; + label.className = "marker-label"; + + return { line, label }; +} + +export function GraphWithMarkerControls({ + syncHook, + newData, + config, + unit, + renderValue, + graphId, + currentTimeSeries, +}: GraphWithMarkerControlsProps) { + const graphWrapperRef = useRef(null); + const [markerName, setMarkerName] = useState(""); + const [markers, setMarkers] = useState<{ timestamp: number; name: string }[]>([]); + const [statusMessage, setStatusMessage] = useState(null); + + // Time Tick for forcing marker redraw + const [timeTick, setTimeTick] = useState(0); + + // Set interval to force redraw the marker effect frequently (e.g., every 50ms) + useEffect(() => { + if (!currentTimeSeries?.current) return; + const intervalId = setInterval(() => { + setTimeTick(prev => prev + 1); + }, 50); + return () => clearInterval(intervalId); + }, [currentTimeSeries?.current]); + + const handleAddMarker = useCallback(() => { + if (currentTimeSeries?.current && markerName.trim()) { + const ts = currentTimeSeries.current.timestamp; + const name = markerName.trim(); + setMarkers((prev) => [...prev, { timestamp: ts, name }]); + const tsStr = new Date(ts).toLocaleTimeString("en-GB", { hour12: false }); + setStatusMessage(`Marker '${name}' added @ ${tsStr}`); + setMarkerName(""); // Clear input after adding + } else { + setStatusMessage("No data or marker name is empty."); + } + }, [currentTimeSeries, markerName]); + + + // Marker Drawing Effect + useEffect(() => { + if (!graphWrapperRef.current || !currentTimeSeries?.current) return; + + const graphEl = graphWrapperRef.current; + // The BigGraph component is the first child (the one with the actual chart) + // TODO: Find a better way to do this + const chartContainer = graphEl.querySelector(".h-\\[50vh\\] > div > div.flex-1 > div"); + if (!chartContainer) return; + + const graphWidth = chartContainer.clientWidth; + const graphHeight = chartContainer.clientHeight; + + const overlayContainer = chartContainer.parentElement; + if (!overlayContainer) return; + + // Remove previous markers and labels from the overlay container + overlayContainer.querySelectorAll(".vertical-marker, .marker-label").forEach((el) => el.remove()); + + // Get the visible time window + const currentTimeWindow = syncHook.controlProps.timeWindow; + const defaultDuration = config.defaultTimeWindow as number; + const validTimeWindowMs = + (typeof currentTimeWindow === 'number' && currentTimeWindow) || + defaultDuration || // Fallback to config default + (30 * 60 * 1000); // Final fallback (30 minutes) + + const endTime = currentTimeSeries.current.timestamp; + const startTime = endTime - validTimeWindowMs; + + // Assuming the graph's fixed Y-scale is from -1 to 1 based on your sine wave example + const graphMin = -1; + const graphMax = 1; + // TODO: For real-world graphs (like Winder), you might need to read the actual min/max scale + // from the uPlot instance or define a safe range if the data is unconstrained. + + markers.forEach(({ timestamp, name }) => { + if (timestamp >= startTime && timestamp <= endTime) { + // Find the data point closest to the marker timestamp to get the correct Y-value + const closest = currentTimeSeries.long.values + .filter((v): v is TimeSeriesValue => v !== null) + .reduce((prev, curr) => + Math.abs(curr.timestamp - timestamp) < Math.abs(prev.timestamp - timestamp) ? curr : prev + ); + if (!closest) return; + + // Calculate the Y-position in pixels from the bottom of the chart area + const normalizedValue = (closest.value - graphMin) / (graphMax - graphMin); + const valueY = normalizedValue * graphHeight; // Height from bottom + + const { line, label } = createMarkerElement( + timestamp, + valueY, + name, + startTime, + endTime, + graphWidth, + graphHeight, + ); + + overlayContainer.appendChild(line); + overlayContainer.appendChild(label); + } + }); + }, [markers, currentTimeSeries, timeTick, config.defaultTimeWindow, syncHook.controlProps.timeWindow]); + + return ( +
+
+ {/* Render the core chart component */} + +
+ + {/* Marker Input and Button */} +
+ Add Marker: + setMarkerName(e.target.value)} + className="border px-2 py-1 rounded" + /> + +

{statusMessage ?? ""}

+
+
+ ); +} \ No newline at end of file diff --git a/electron/src/machines/mock/mock1/Mock1Graph.tsx b/electron/src/machines/mock/mock1/Mock1Graph.tsx index c2fc3ca31..bb154bfdf 100644 --- a/electron/src/machines/mock/mock1/Mock1Graph.tsx +++ b/electron/src/machines/mock/mock1/Mock1Graph.tsx @@ -6,68 +6,19 @@ import { type GraphConfig, } from "@/components/graph"; import React from "react"; -import { useState } from "react"; import { useMock1 } from "./useMock"; import { TimeSeriesValue, type Series, TimeSeries } from "@/lib/timeseries"; +import { GraphWithMarkerControls } from "@/components/graph/GraphWithMarkerControls"; +import { Unit } from "@/control/units"; + +// Define the assumed Y-axis scale for the mock data (-1 to 1) +// TODO: why is it neccessary here? +const MOCK_Y_AXIS_SCALE = { min: -1, max: 1 }; export function Mock1GraphPage() { const { sineWaveSum } = useMock1(); const syncHook = useGraphSync("mock-graphs"); - const graph1Ref = React.useRef(null); - const [marker, setMarker] = useState(null); - const [markers, setMarkers] = useState<{ timestamp: number; name: string }[]>([]); - - const handleAddMarker = () => { - const inputEl = document.getElementById("marker-input") as HTMLInputElement; - if (inputEl && sineWaveSum.current) { - const ts = sineWaveSum.current.timestamp; - const name = inputEl.value; - setMarkers((prev) => [...prev, { timestamp: ts, name }]); - const tsStr = new Date(ts).toLocaleTimeString("en-GB", { hour12: false }); - setMarker(`${name} @ ${tsStr}`); - } else { - setMarker("No data"); - } - }; - - function createMarkerElement( - timestamp: number, - value: number, - name: string, - startTime: number, - endTime: number, - graphWidth: number, - graphHeight: number, - ) { - const ratio = (timestamp - startTime) / (endTime - startTime); - const xPos = Math.min(Math.max(ratio, 0), 1) * graphWidth; - const yPos = graphHeight - value; - - - const line = document.createElement("div"); - line.style.position = "absolute"; - line.style.left = `${xPos}px`; - line.style.top = `${yPos}px`; - line.style.height = `${value}px`; - line.style.width = "2px"; - line.style.background = "black"; - line.className = "vertical-marker"; - - const label = document.createElement("div"); - label.textContent = name; - label.style.position = "absolute"; - label.style.left = `${xPos}px`; - label.style.top = `${yPos - 16}px`; - label.style.transform = "translateX(-50%)"; - label.style.color = "black"; - label.style.fontSize = "12px"; - label.style.padding = "0 2px"; - label.style.whiteSpace = "nowrap"; - label.className = "marker-label"; - - return { line, label }; - } const config: GraphConfig = { title: "Sine Wave", @@ -150,123 +101,55 @@ export function Mock1GraphPage() { title: "Sine Wave", }; - const [timeTick, setTimeTick] = useState(0); - - // Component be re-rendered by updating 'timeTick'. - React.useEffect(() => { - if (!sineWaveSum.current) return; - - const intervalId = setInterval(() => { - setTimeTick(prev => prev + 1); - }, 50); - - return () => clearInterval(intervalId); - }, [sineWaveSum.current]); - - // Draw the marker - React.useEffect(() => { - if (!graph1Ref.current || !sineWaveSum.current) return; - const graphEl = graph1Ref.current; - const graphWidth = graphEl.clientWidth; - const graphHeight = graphEl.clientHeight; - - // Remove previous markers and labels - graphEl.querySelectorAll(".vertical-marker, .marker-label").forEach((el) => el.remove()); - - const currentTimeWindow = syncHook.controlProps.timeWindow; - const defaultDuration = singleGraphConfig.defaultTimeWindow as number; - const validTimeWindowMs = - (typeof currentTimeWindow === 'number' && currentTimeWindow) || - defaultDuration; - const endTime = sineWaveSum.current.timestamp; - const startTime = endTime - validTimeWindowMs; - - const graphMin = -1; - const graphMax = 1; - - markers.forEach(({ timestamp, name }) => { - if (timestamp >= startTime && timestamp <= endTime) { - const closest = sineWaveSum.long.values - .filter((v): v is TimeSeriesValue => v !== null) - .reduce((prev, curr) => - Math.abs(curr.timestamp - timestamp) < Math.abs(prev.timestamp - timestamp) ? curr : prev - ); - if (!closest) return; - - const valueY = ((closest.value - graphMin) / (graphMax - graphMin)) * graphHeight; - - const { line, label } = createMarkerElement( - timestamp, - valueY, - name, - startTime, - endTime, - graphWidth, - graphHeight, - ); - - graphEl.appendChild(line); - graphEl.appendChild(label); - } - }); - }, [markers, sineWaveSum.current, timeTick, singleGraphConfig.defaultTimeWindow, syncHook.controlProps.timeWindow]); - return (
-
- value.toFixed(3)} - graphId="single-graph1" - /> -
-
- Add data marker - - -

{marker ?? "No data"}

-
- value.toFixed(3)} + graphId="single-graph1" + currentTimeSeries={sineWaveSum} + yAxisScale={MOCK_Y_AXIS_SCALE} + /> + value.toFixed(3)} graphId="combined-graph" + currentTimeSeries={sineWaveSum} + yAxisScale={MOCK_Y_AXIS_SCALE} /> - value.toFixed(3)} graphId="single-graph2" + currentTimeSeries={sineWaveSum} + yAxisScale={MOCK_Y_AXIS_SCALE} /> - value.toFixed(3)} graphId="single-graph" + currentTimeSeries={sineWaveSum} + yAxisScale={MOCK_Y_AXIS_SCALE} />
); -} +} \ No newline at end of file From a294945522f8ad5d272466dd80e5761edcbce942 Mon Sep 17 00:00:00 2001 From: qitech Date: Tue, 9 Dec 2025 12:08:18 +0100 Subject: [PATCH 4/9] time stamp for winder --- .../machines/winder/winder2/Winder2Graphs.tsx | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/electron/src/machines/winder/winder2/Winder2Graphs.tsx b/electron/src/machines/winder/winder2/Winder2Graphs.tsx index ab3adb066..3429d6768 100644 --- a/electron/src/machines/winder/winder2/Winder2Graphs.tsx +++ b/electron/src/machines/winder/winder2/Winder2Graphs.tsx @@ -11,6 +11,15 @@ import { useWinder2 } from "./useWinder"; import { roundDegreesToDecimals, roundToDecimals } from "@/lib/decimal"; import { TimeSeries } from "@/lib/timeseries"; import { Unit } from "@/control/units"; +import { GraphWithMarkerControls } from "@/components/graph/GraphWithMarkerControls"; + +// Define placeholder Y-Axis scales (MUST BE UPDATED WITH ACTUAL MACHINE LIMITS) +// TODO: why is it necessary here? +const WINDER_RPM_SCALE = { min: 0, max: 1000 }; +const WINDER_ANGLE_SCALE = { min: 0, max: 90 }; +const WINDER_POS_SCALE = { min: 0, max: 500 }; +const WINDER_SPEED_SCALE = { min: 0, max: 100 }; +const WINDER_PROGRESS_SCALE = { min: 0, max: 50000 }; export function Winder2GraphsPage() { const { @@ -96,7 +105,7 @@ export function SpoolRpmGraph({ }; return ( - ); } @@ -155,7 +166,7 @@ export function TraversePositionGraph({ }; return ( - ); } @@ -191,7 +204,7 @@ export function TensionArmAngleGraph({ }; return ( - ); } @@ -225,8 +240,9 @@ export function SpoolProgressGraph({ exportFilename: "spool_progress", }; + // NOTE: Assuming this graph starts at 0, and the max is the total capacity. return ( - ); } @@ -274,7 +292,7 @@ export function PullerSpeedGraph({ }; return ( - ); } From 9ce105d3e604dc2c3b911bd1868f47af40da70d1 Mon Sep 17 00:00:00 2001 From: qitech Date: Tue, 9 Dec 2025 14:10:38 +0100 Subject: [PATCH 5/9] time stamp for extruder --- .../extruder/extruder2/Extruder2Graph.tsx | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/electron/src/machines/extruder/extruder2/Extruder2Graph.tsx b/electron/src/machines/extruder/extruder2/Extruder2Graph.tsx index b8bc86e26..c57329073 100644 --- a/electron/src/machines/extruder/extruder2/Extruder2Graph.tsx +++ b/electron/src/machines/extruder/extruder2/Extruder2Graph.tsx @@ -7,6 +7,7 @@ import { } from "@/components/graph"; import React from "react"; import { useExtruder2 } from "./useExtruder"; +import { GraphWithMarkerControls } from "@/components/graph/GraphWithMarkerControls"; export function Extruder2GraphsPage() { const { @@ -28,6 +29,13 @@ export function Extruder2GraphsPage() { const syncHook = useGraphSync("extruder-graphs"); + // ESTIMATED SCALES (REPLACE WITH REAL LIMITS) + const SCALE_TEMP = { min: 0, max: 450 }; // Typical max temp for extruders (in °C) + const SCALE_POWER = { min: 0, max: 10000 }; // Estimated max power (in W) + const SCALE_CURRENT = { min: 0, max: 50 }; // Estimated max motor current (in A) + const SCALE_PRESSURE = { min: 0, max: 250 }; // Estimated max pressure (in bar) + const SCALE_RPM = { min: 0, max: 100 }; // Estimated max screw RPM (in rpm) + // Base config const baseConfig: GraphConfig = { defaultTimeWindow: 30 * 60 * 1000, // 30 minutes @@ -242,7 +250,7 @@ export function Extruder2GraphsPage() { return (
- value.toFixed(2)} graphId="pressure-graph" + currentTimeSeries={pressure} + yAxisScale={SCALE_PRESSURE} /> - value.toFixed(1)} graphId="combined-temperatures" + currentTimeSeries={nozzleTemperature} + yAxisScale={SCALE_TEMP} /> - value.toFixed(1)} graphId="combined-power" + currentTimeSeries={combinedPower} + yAxisScale={SCALE_POWER} /> - value.toFixed(2)} graphId="motor-current" + currentTimeSeries={motorCurrent} + yAxisScale={SCALE_CURRENT} /> - value.toFixed(0)} graphId="rpm-graph" + currentTimeSeries={motorScrewRpm} + yAxisScale={SCALE_RPM} />
From 8e4cc676252a4fcb5c16c32a6d02c646e1f6db02 Mon Sep 17 00:00:00 2001 From: qitech Date: Tue, 9 Dec 2025 17:04:25 +0100 Subject: [PATCH 6/9] first approach for time stamp in excel --- .../graph/GraphWithMarkerControls.tsx | 43 ++++-- electron/src/components/graph/excelExport.ts | 133 ++++++++++++++++++ electron/src/components/graph/types.ts | 3 +- 3 files changed, 167 insertions(+), 12 deletions(-) diff --git a/electron/src/components/graph/GraphWithMarkerControls.tsx b/electron/src/components/graph/GraphWithMarkerControls.tsx index 176301c53..a4631b30c 100644 --- a/electron/src/components/graph/GraphWithMarkerControls.tsx +++ b/electron/src/components/graph/GraphWithMarkerControls.tsx @@ -78,9 +78,23 @@ export function GraphWithMarkerControls({ }: GraphWithMarkerControlsProps) { const graphWrapperRef = useRef(null); const [markerName, setMarkerName] = useState(""); - const [markers, setMarkers] = useState<{ timestamp: number; name: string }[]>([]); + const [markers, setMarkers] = useState<{ timestamp: number; name: string; value: number }[]>([]) const [statusMessage, setStatusMessage] = useState(null); + // Convert local markers state into GraphLine format + // TODO: do I really need this? + const dynamicMarkerLines = markers.map((marker, index) => ({ + type: "user_marker" as const, // Use a unique type identifier + value: marker.value, // We need to store the value as well! + label: marker.name, + color: "#ff0000", // e.g., Red for user-added markers + width: 2, + show: true, + + // *** FIX: Store the actual time here *** + markerTimestamp: marker.timestamp, + })); + // Time Tick for forcing marker redraw const [timeTick, setTimeTick] = useState(0); @@ -95,15 +109,15 @@ export function GraphWithMarkerControls({ const handleAddMarker = useCallback(() => { if (currentTimeSeries?.current && markerName.trim()) { - const ts = currentTimeSeries.current.timestamp; - const name = markerName.trim(); - setMarkers((prev) => [...prev, { timestamp: ts, name }]); - const tsStr = new Date(ts).toLocaleTimeString("en-GB", { hour12: false }); - setStatusMessage(`Marker '${name}' added @ ${tsStr}`); - setMarkerName(""); // Clear input after adding - } else { - setStatusMessage("No data or marker name is empty."); - } + const ts = currentTimeSeries.current.timestamp; + const val = currentTimeSeries.current.value; // <--- Value extracted + const name = markerName.trim(); + + // Ensure 'value' is stored here + setMarkers((prev) => [...prev, { timestamp: ts, name, value: val }]); // <--- Value stored + + // ... (rest of the function) + } // ... }, [currentTimeSeries, markerName]); @@ -173,6 +187,13 @@ export function GraphWithMarkerControls({ }); }, [markers, currentTimeSeries, timeTick, config.defaultTimeWindow, syncHook.controlProps.timeWindow]); + // Combine the base config lines with the dynamic lines + // TODO: do I really need this? if not change back to config + const finalConfig = { + ...config, + lines: [...(config.lines || []), ...dynamicMarkerLines], + }; + return (
@@ -180,7 +201,7 @@ export function GraphWithMarkerControls({ 0) { + const markerReportData = createGraphLineMarkerReportSheet(graphLineData); + const markerReportWorksheet = XLSX.utils.aoa_to_sheet(markerReportData); + // Set column widths here (e.g., Column A = 15, Column B = 25) + markerReportWorksheet["!cols"] = [ + { wch: 20 }, // Column A (Labels: 'Timestamp', 'Value', etc.) + { wch: 30 }, // Column B (Values, where the Date object resides) + ]; + const markerReportSheetName = generateUniqueSheetName( + `${seriesTitle} Marker Report`, + usedSheetNames, + ); + XLSX.utils.book_append_sheet( + workbook, + markerReportWorksheet, + markerReportSheetName, + ); + } + // TODO: clean and refactor + processedCount++; }); @@ -290,6 +311,118 @@ function createGraphLineDataSheet(graphLine: { }); } +/** + * TODO: clean and refactor + */ +function createGraphLineMarkerReportSheet(graphLine: { + graphTitle: string; + lineTitle: string; + series: TimeSeries; + color?: string; + unit?: Unit; + renderValue?: (value: number) => string; + config: GraphConfig; + targetLines: GraphLine[]; +}): any[][] { + const [timestamps, values] = seriesToUPlotData(graphLine.series.long); + const unitSymbol = renderUnitSymbol(graphLine.unit) || ""; + // Initialize Report Data and Header + const reportData: any[][] = [ + [`Marker Report: ${graphLine.lineTitle}`], + ["Graph", graphLine.graphTitle], + ["Line Name", graphLine.lineTitle], + ["", ""], + ["--- Data Point Marker Status ---", ""], + ["", ""], + ]; + + if (timestamps.length === 0) { + reportData.push(["No data points to report"]); + return reportData; + } + + // 2. Filter User Markers + const allTargetLines = graphLine.targetLines.filter(line => line.show !== false); + const userMarkers = allTargetLines.filter(line => line.type === 'user_marker' && line.label); + + // 3. Map Markers to Closest Data Point Index + const markerIndexMap = new Map(); + + userMarkers.forEach(line => { + const markerTime = line.markerTimestamp || line.value; // Use the correct high-precision timestamp + let closestDataPointIndex = -1; + let minTimeDifference = Infinity; + + // Find the data point with the closest timestamp + timestamps.forEach((ts, index) => { + const difference = Math.abs(ts - markerTime); + if (difference < minTimeDifference) { + minTimeDifference = difference; + closestDataPointIndex = index; + } + }); + + // Store the marker data at the index of the closest data point + if (closestDataPointIndex !== -1) { + markerIndexMap.set(closestDataPointIndex, { + label: line.label || 'User Marker', + originalTimestamp: markerTime, + }); + } + }); + + // Add the final header before the detailed report starts + reportData.push( + ["--- BEGIN DETAILED REPORT ---", ""], + ["", ""], + ); + + // Handle case where no user markers were created + if (userMarkers.length === 0) { + reportData.push(["No user-created markers found.", ""]); + } + + // 4. Detailed Report Generation Loop + timestamps.forEach((dataPointTimestamp, index) => { + const value = values[index]; + const markerData = markerIndexMap.get(index); + + let finalMarkerLabel = "N/A"; + let timeToDisplay = dataPointTimestamp; // Default to data sample time + + if (markerData) { + finalMarkerLabel = `Marker: ${markerData.label}`; + // CRITICAL FIX: Use the marker's high-precision time for display + timeToDisplay = markerData.originalTimestamp; + } + + // Format the time (using timeToDisplay) + const formattedTime = new Date(timeToDisplay).toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false + }).replace(/ /g, ''); + + // Row 1: Timestamp + reportData.push(["Timestamp", formattedTime]); + + // Row 2: Value + const formattedValue = graphLine.renderValue + ? graphLine.renderValue(value) + : value?.toFixed(3) || ""; + reportData.push([`Value (${unitSymbol})`, formattedValue]); + + // Row 3: Marker Event + reportData.push(["Marker Event", finalMarkerLabel]); + + // Separator + reportData.push(["", ""]); + }); + + return reportData; +} + // Ensure sheet names are unique and valid for Excel function generateUniqueSheetName( name: string, diff --git a/electron/src/components/graph/types.ts b/electron/src/components/graph/types.ts index bc8001aec..ccbae607e 100644 --- a/electron/src/components/graph/types.ts +++ b/electron/src/components/graph/types.ts @@ -25,13 +25,14 @@ export type PropGraphSync = { // Configuration types for additional lines export type GraphLine = { - type: "threshold" | "target"; + type: "threshold" | "target" | "user_marker"; // TODO: redundant or not? value: number; color: string; label?: string; width?: number; dash?: number[]; show?: boolean; + markerTimestamp?: number; }; export type GraphConfig = { From 6a2cda5bb97635b76204124550b3a3d10a175b3e Mon Sep 17 00:00:00 2001 From: qitech Date: Thu, 11 Dec 2025 11:11:32 +0100 Subject: [PATCH 7/9] refactor code --- electron/src/components/graph/excelExport.ts | 22 +++++++------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/electron/src/components/graph/excelExport.ts b/electron/src/components/graph/excelExport.ts index 150f4b85b..b51ddaa77 100644 --- a/electron/src/components/graph/excelExport.ts +++ b/electron/src/components/graph/excelExport.ts @@ -84,7 +84,7 @@ export function exportGraphsToExcel( XLSX.utils.book_append_sheet(workbook, dataWorksheet, dataSheetName); } - // TODO: clean and refactor + // Excel worksheet for timestamps and timestamp markers if (graphLineData.targetLines.length > 0) { const markerReportData = createGraphLineMarkerReportSheet(graphLineData); const markerReportWorksheet = XLSX.utils.aoa_to_sheet(markerReportData); @@ -103,7 +103,6 @@ export function exportGraphsToExcel( markerReportSheetName, ); } - // TODO: clean and refactor processedCount++; }); @@ -311,9 +310,6 @@ function createGraphLineDataSheet(graphLine: { }); } -/** - * TODO: clean and refactor - */ function createGraphLineMarkerReportSheet(graphLine: { graphTitle: string; lineTitle: string; @@ -341,11 +337,11 @@ function createGraphLineMarkerReportSheet(graphLine: { return reportData; } - // 2. Filter User Markers + // Filter User Markers const allTargetLines = graphLine.targetLines.filter(line => line.show !== false); const userMarkers = allTargetLines.filter(line => line.type === 'user_marker' && line.label); - // 3. Map Markers to Closest Data Point Index + // Map Markers to Closest Data Point Index const markerIndexMap = new Map(); userMarkers.forEach(line => { @@ -371,7 +367,7 @@ function createGraphLineMarkerReportSheet(graphLine: { } }); - // Add the final header before the detailed report starts + // Add the final header before the timestamp report starts reportData.push( ["--- BEGIN DETAILED REPORT ---", ""], ["", ""], @@ -382,17 +378,15 @@ function createGraphLineMarkerReportSheet(graphLine: { reportData.push(["No user-created markers found.", ""]); } - // 4. Detailed Report Generation Loop timestamps.forEach((dataPointTimestamp, index) => { const value = values[index]; const markerData = markerIndexMap.get(index); - let finalMarkerLabel = "N/A"; + let finalMarkerLabel = ""; let timeToDisplay = dataPointTimestamp; // Default to data sample time if (markerData) { - finalMarkerLabel = `Marker: ${markerData.label}`; - // CRITICAL FIX: Use the marker's high-precision time for display + finalMarkerLabel = `${markerData.label}`; timeToDisplay = markerData.originalTimestamp; } @@ -413,8 +407,8 @@ function createGraphLineMarkerReportSheet(graphLine: { : value?.toFixed(3) || ""; reportData.push([`Value (${unitSymbol})`, formattedValue]); - // Row 3: Marker Event - reportData.push(["Marker Event", finalMarkerLabel]); + // Row 3: Marker Name + reportData.push(["Marker", finalMarkerLabel]); // Separator reportData.push(["", ""]); From 61a247088a67c1c4f5869a3a6adecd9760b38eed Mon Sep 17 00:00:00 2001 From: qitech Date: Thu, 11 Dec 2025 12:02:16 +0100 Subject: [PATCH 8/9] clean up redundant code and comments --- .../graph/GraphWithMarkerControls.tsx | 30 +++++-------------- .../extruder/extruder2/Extruder2Graph.tsx | 12 -------- .../src/machines/mock/mock1/Mock1Graph.tsx | 8 ----- .../machines/winder/winder2/Winder2Graphs.tsx | 13 -------- 4 files changed, 8 insertions(+), 55 deletions(-) diff --git a/electron/src/components/graph/GraphWithMarkerControls.tsx b/electron/src/components/graph/GraphWithMarkerControls.tsx index a4631b30c..3da483f38 100644 --- a/electron/src/components/graph/GraphWithMarkerControls.tsx +++ b/electron/src/components/graph/GraphWithMarkerControls.tsx @@ -21,12 +21,7 @@ type GraphWithMarkerControlsProps = { unit?: Unit; renderValue?: (value: number) => string; graphId: string; - // The raw TimeSeries object for capturing timestamps. currentTimeSeries: TimeSeries | null; - yAxisScale?: { // TODO: why is it neccessary? - min: number; - max: number - }; }; function createMarkerElement( @@ -81,17 +76,13 @@ export function GraphWithMarkerControls({ const [markers, setMarkers] = useState<{ timestamp: number; name: string; value: number }[]>([]) const [statusMessage, setStatusMessage] = useState(null); - // Convert local markers state into GraphLine format - // TODO: do I really need this? const dynamicMarkerLines = markers.map((marker, index) => ({ - type: "user_marker" as const, // Use a unique type identifier - value: marker.value, // We need to store the value as well! + type: "user_marker" as const, + value: marker.value, label: marker.name, - color: "#ff0000", // e.g., Red for user-added markers + color: "#ff0000", width: 2, show: true, - - // *** FIX: Store the actual time here *** markerTimestamp: marker.timestamp, })); @@ -110,14 +101,11 @@ export function GraphWithMarkerControls({ const handleAddMarker = useCallback(() => { if (currentTimeSeries?.current && markerName.trim()) { const ts = currentTimeSeries.current.timestamp; - const val = currentTimeSeries.current.value; // <--- Value extracted + const val = currentTimeSeries.current.value; const name = markerName.trim(); - // Ensure 'value' is stored here - setMarkers((prev) => [...prev, { timestamp: ts, name, value: val }]); // <--- Value stored - - // ... (rest of the function) - } // ... + setMarkers((prev) => [...prev, { timestamp: ts, name, value: val }]); + } }, [currentTimeSeries, markerName]); @@ -151,7 +139,7 @@ export function GraphWithMarkerControls({ const endTime = currentTimeSeries.current.timestamp; const startTime = endTime - validTimeWindowMs; - // Assuming the graph's fixed Y-scale is from -1 to 1 based on your sine wave example + // Assuming the graph's fixed Y-scale is from -1 to 1 based on the sine wave example const graphMin = -1; const graphMax = 1; // TODO: For real-world graphs (like Winder), you might need to read the actual min/max scale @@ -169,7 +157,7 @@ export function GraphWithMarkerControls({ // Calculate the Y-position in pixels from the bottom of the chart area const normalizedValue = (closest.value - graphMin) / (graphMax - graphMin); - const valueY = normalizedValue * graphHeight; // Height from bottom + const valueY = normalizedValue * graphHeight; const { line, label } = createMarkerElement( timestamp, @@ -187,8 +175,6 @@ export function GraphWithMarkerControls({ }); }, [markers, currentTimeSeries, timeTick, config.defaultTimeWindow, syncHook.controlProps.timeWindow]); - // Combine the base config lines with the dynamic lines - // TODO: do I really need this? if not change back to config const finalConfig = { ...config, lines: [...(config.lines || []), ...dynamicMarkerLines], diff --git a/electron/src/machines/extruder/extruder2/Extruder2Graph.tsx b/electron/src/machines/extruder/extruder2/Extruder2Graph.tsx index c57329073..69d118afa 100644 --- a/electron/src/machines/extruder/extruder2/Extruder2Graph.tsx +++ b/electron/src/machines/extruder/extruder2/Extruder2Graph.tsx @@ -29,13 +29,6 @@ export function Extruder2GraphsPage() { const syncHook = useGraphSync("extruder-graphs"); - // ESTIMATED SCALES (REPLACE WITH REAL LIMITS) - const SCALE_TEMP = { min: 0, max: 450 }; // Typical max temp for extruders (in °C) - const SCALE_POWER = { min: 0, max: 10000 }; // Estimated max power (in W) - const SCALE_CURRENT = { min: 0, max: 50 }; // Estimated max motor current (in A) - const SCALE_PRESSURE = { min: 0, max: 250 }; // Estimated max pressure (in bar) - const SCALE_RPM = { min: 0, max: 100 }; // Estimated max screw RPM (in rpm) - // Base config const baseConfig: GraphConfig = { defaultTimeWindow: 30 * 60 * 1000, // 30 minutes @@ -272,7 +265,6 @@ export function Extruder2GraphsPage() { renderValue={(value) => value.toFixed(2)} graphId="pressure-graph" currentTimeSeries={pressure} - yAxisScale={SCALE_PRESSURE} /> value.toFixed(1)} graphId="combined-temperatures" currentTimeSeries={nozzleTemperature} - yAxisScale={SCALE_TEMP} /> value.toFixed(1)} graphId="combined-power" currentTimeSeries={combinedPower} - yAxisScale={SCALE_POWER} /> value.toFixed(2)} graphId="motor-current" currentTimeSeries={motorCurrent} - yAxisScale={SCALE_CURRENT} /> value.toFixed(0)} graphId="rpm-graph" currentTimeSeries={motorScrewRpm} - yAxisScale={SCALE_RPM} />
diff --git a/electron/src/machines/mock/mock1/Mock1Graph.tsx b/electron/src/machines/mock/mock1/Mock1Graph.tsx index bb154bfdf..ca6a59823 100644 --- a/electron/src/machines/mock/mock1/Mock1Graph.tsx +++ b/electron/src/machines/mock/mock1/Mock1Graph.tsx @@ -11,10 +11,6 @@ import { TimeSeriesValue, type Series, TimeSeries } from "@/lib/timeseries"; import { GraphWithMarkerControls } from "@/components/graph/GraphWithMarkerControls"; import { Unit } from "@/control/units"; -// Define the assumed Y-axis scale for the mock data (-1 to 1) -// TODO: why is it neccessary here? -const MOCK_Y_AXIS_SCALE = { min: -1, max: 1 }; - export function Mock1GraphPage() { const { sineWaveSum } = useMock1(); @@ -112,7 +108,6 @@ export function Mock1GraphPage() { renderValue={(value) => value.toFixed(3)} graphId="single-graph1" currentTimeSeries={sineWaveSum} - yAxisScale={MOCK_Y_AXIS_SCALE} /> value.toFixed(3)} graphId="combined-graph" currentTimeSeries={sineWaveSum} - yAxisScale={MOCK_Y_AXIS_SCALE} /> value.toFixed(3)} graphId="single-graph2" currentTimeSeries={sineWaveSum} - yAxisScale={MOCK_Y_AXIS_SCALE} /> value.toFixed(3)} graphId="single-graph" currentTimeSeries={sineWaveSum} - yAxisScale={MOCK_Y_AXIS_SCALE} />
diff --git a/electron/src/machines/winder/winder2/Winder2Graphs.tsx b/electron/src/machines/winder/winder2/Winder2Graphs.tsx index 3429d6768..450ceea40 100644 --- a/electron/src/machines/winder/winder2/Winder2Graphs.tsx +++ b/electron/src/machines/winder/winder2/Winder2Graphs.tsx @@ -13,14 +13,6 @@ import { TimeSeries } from "@/lib/timeseries"; import { Unit } from "@/control/units"; import { GraphWithMarkerControls } from "@/components/graph/GraphWithMarkerControls"; -// Define placeholder Y-Axis scales (MUST BE UPDATED WITH ACTUAL MACHINE LIMITS) -// TODO: why is it necessary here? -const WINDER_RPM_SCALE = { min: 0, max: 1000 }; -const WINDER_ANGLE_SCALE = { min: 0, max: 90 }; -const WINDER_POS_SCALE = { min: 0, max: 500 }; -const WINDER_SPEED_SCALE = { min: 0, max: 100 }; -const WINDER_PROGRESS_SCALE = { min: 0, max: 50000 }; - export function Winder2GraphsPage() { const { state, @@ -116,7 +108,6 @@ export function SpoolRpmGraph({ config={config} graphId="spool-rpm" currentTimeSeries={newData} - yAxisScale={WINDER_RPM_SCALE} // TODO: is it necessary? /> ); } @@ -178,7 +169,6 @@ export function TraversePositionGraph({ config={config} graphId="traverse-position" currentTimeSeries={newData} - yAxisScale={WINDER_POS_SCALE} // TODO: is it necessary? /> ); } @@ -215,7 +205,6 @@ export function TensionArmAngleGraph({ config={config} graphId="tension-arm-angle" currentTimeSeries={newData} - yAxisScale={WINDER_ANGLE_SCALE} // TODO: is it necessary? /> ); } @@ -253,7 +242,6 @@ export function SpoolProgressGraph({ config={config} graphId="spool-progress" currentTimeSeries={newData} - yAxisScale={WINDER_PROGRESS_SCALE} // TODO: is it necessary? /> ); } @@ -304,7 +292,6 @@ export function PullerSpeedGraph({ config={config} graphId="puller-speed" currentTimeSeries={newData} - yAxisScale={WINDER_SPEED_SCALE} // TODO: is it necessary? /> ); } From 8a59d69dbe6380599ba36c69c95db300d067c31e Mon Sep 17 00:00:00 2001 From: qitech Date: Thu, 11 Dec 2025 12:09:07 +0100 Subject: [PATCH 9/9] Fix: Applied Prettier formatting fixes --- .../graph/GraphWithMarkerControls.tsx | 133 ++++++++++-------- electron/src/components/graph/excelExport.ts | 83 ++++++----- .../src/machines/mock/mock1/Mock1Graph.tsx | 2 +- 3 files changed, 120 insertions(+), 98 deletions(-) diff --git a/electron/src/components/graph/GraphWithMarkerControls.tsx b/electron/src/components/graph/GraphWithMarkerControls.tsx index 3da483f38..498d9b4fe 100644 --- a/electron/src/components/graph/GraphWithMarkerControls.tsx +++ b/electron/src/components/graph/GraphWithMarkerControls.tsx @@ -21,7 +21,7 @@ type GraphWithMarkerControlsProps = { unit?: Unit; renderValue?: (value: number) => string; graphId: string; - currentTimeSeries: TimeSeries | null; + currentTimeSeries: TimeSeries | null; }; function createMarkerElement( @@ -36,7 +36,7 @@ function createMarkerElement( // Calculate the position of the timestamp const ratio = (timestamp - startTime) / (endTime - startTime); const xPos = Math.min(Math.max(ratio, 0), 1) * graphWidth; - const yPos = graphHeight - value; + const yPos = graphHeight - value; const line = document.createElement("div"); line.style.position = "absolute"; @@ -73,7 +73,9 @@ export function GraphWithMarkerControls({ }: GraphWithMarkerControlsProps) { const graphWrapperRef = useRef(null); const [markerName, setMarkerName] = useState(""); - const [markers, setMarkers] = useState<{ timestamp: number; name: string; value: number }[]>([]) + const [markers, setMarkers] = useState< + { timestamp: number; name: string; value: number }[] + >([]); const [statusMessage, setStatusMessage] = useState(null); const dynamicMarkerLines = markers.map((marker, index) => ({ @@ -93,22 +95,21 @@ export function GraphWithMarkerControls({ useEffect(() => { if (!currentTimeSeries?.current) return; const intervalId = setInterval(() => { - setTimeTick(prev => prev + 1); - }, 50); + setTimeTick((prev) => prev + 1); + }, 50); return () => clearInterval(intervalId); - }, [currentTimeSeries?.current]); + }, [currentTimeSeries?.current]); const handleAddMarker = useCallback(() => { if (currentTimeSeries?.current && markerName.trim()) { - const ts = currentTimeSeries.current.timestamp; - const val = currentTimeSeries.current.value; - const name = markerName.trim(); - - setMarkers((prev) => [...prev, { timestamp: ts, name, value: val }]); + const ts = currentTimeSeries.current.timestamp; + const val = currentTimeSeries.current.value; + const name = markerName.trim(); + + setMarkers((prev) => [...prev, { timestamp: ts, name, value: val }]); } }, [currentTimeSeries, markerName]); - // Marker Drawing Effect useEffect(() => { if (!graphWrapperRef.current || !currentTimeSeries?.current) return; @@ -116,64 +117,78 @@ export function GraphWithMarkerControls({ const graphEl = graphWrapperRef.current; // The BigGraph component is the first child (the one with the actual chart) // TODO: Find a better way to do this - const chartContainer = graphEl.querySelector(".h-\\[50vh\\] > div > div.flex-1 > div"); - if (!chartContainer) return; + const chartContainer = graphEl.querySelector( + ".h-\\[50vh\\] > div > div.flex-1 > div", + ); + if (!chartContainer) return; const graphWidth = chartContainer.clientWidth; const graphHeight = chartContainer.clientHeight; - + const overlayContainer = chartContainer.parentElement; if (!overlayContainer) return; // Remove previous markers and labels from the overlay container - overlayContainer.querySelectorAll(".vertical-marker, .marker-label").forEach((el) => el.remove()); + overlayContainer + .querySelectorAll(".vertical-marker, .marker-label") + .forEach((el) => el.remove()); // Get the visible time window - const currentTimeWindow = syncHook.controlProps.timeWindow; + const currentTimeWindow = syncHook.controlProps.timeWindow; const defaultDuration = config.defaultTimeWindow as number; - const validTimeWindowMs = - (typeof currentTimeWindow === 'number' && currentTimeWindow) || - defaultDuration || // Fallback to config default - (30 * 60 * 1000); // Final fallback (30 minutes) - - const endTime = currentTimeSeries.current.timestamp; - const startTime = endTime - validTimeWindowMs; + const validTimeWindowMs = + (typeof currentTimeWindow === "number" && currentTimeWindow) || + defaultDuration || // Fallback to config default + 30 * 60 * 1000; // Final fallback (30 minutes) + + const endTime = currentTimeSeries.current.timestamp; + const startTime = endTime - validTimeWindowMs; // Assuming the graph's fixed Y-scale is from -1 to 1 based on the sine wave example - const graphMin = -1; - const graphMax = 1; - // TODO: For real-world graphs (like Winder), you might need to read the actual min/max scale + const graphMin = -1; + const graphMax = 1; + // TODO: For real-world graphs (like Winder), you might need to read the actual min/max scale // from the uPlot instance or define a safe range if the data is unconstrained. markers.forEach(({ timestamp, name }) => { if (timestamp >= startTime && timestamp <= endTime) { - // Find the data point closest to the marker timestamp to get the correct Y-value - const closest = currentTimeSeries.long.values - .filter((v): v is TimeSeriesValue => v !== null) - .reduce((prev, curr) => - Math.abs(curr.timestamp - timestamp) < Math.abs(prev.timestamp - timestamp) ? curr : prev - ); - if (!closest) return; - - // Calculate the Y-position in pixels from the bottom of the chart area - const normalizedValue = (closest.value - graphMin) / (graphMax - graphMin); - const valueY = normalizedValue * graphHeight; - - const { line, label } = createMarkerElement( - timestamp, - valueY, - name, - startTime, - endTime, - graphWidth, - graphHeight, + // Find the data point closest to the marker timestamp to get the correct Y-value + const closest = currentTimeSeries.long.values + .filter((v): v is TimeSeriesValue => v !== null) + .reduce((prev, curr) => + Math.abs(curr.timestamp - timestamp) < + Math.abs(prev.timestamp - timestamp) + ? curr + : prev, ); - - overlayContainer.appendChild(line); - overlayContainer.appendChild(label); + if (!closest) return; + + // Calculate the Y-position in pixels from the bottom of the chart area + const normalizedValue = + (closest.value - graphMin) / (graphMax - graphMin); + const valueY = normalizedValue * graphHeight; + + const { line, label } = createMarkerElement( + timestamp, + valueY, + name, + startTime, + endTime, + graphWidth, + graphHeight, + ); + + overlayContainer.appendChild(line); + overlayContainer.appendChild(label); } }); - }, [markers, currentTimeSeries, timeTick, config.defaultTimeWindow, syncHook.controlProps.timeWindow]); + }, [ + markers, + currentTimeSeries, + timeTick, + config.defaultTimeWindow, + syncHook.controlProps.timeWindow, + ]); const finalConfig = { ...config, @@ -193,26 +208,26 @@ export function GraphWithMarkerControls({ graphId={graphId} />
- + {/* Marker Input and Button */} -
+
Add Marker: setMarkerName(e.target.value)} - className="border px-2 py-1 rounded" + className="rounded border px-2 py-1" /> - -

{statusMessage ?? ""}

+

{statusMessage ?? ""}

); -} \ No newline at end of file +} diff --git a/electron/src/components/graph/excelExport.ts b/electron/src/components/graph/excelExport.ts index b51ddaa77..8b86fcab9 100644 --- a/electron/src/components/graph/excelExport.ts +++ b/electron/src/components/graph/excelExport.ts @@ -86,12 +86,13 @@ export function exportGraphsToExcel( // Excel worksheet for timestamps and timestamp markers if (graphLineData.targetLines.length > 0) { - const markerReportData = createGraphLineMarkerReportSheet(graphLineData); + const markerReportData = + createGraphLineMarkerReportSheet(graphLineData); const markerReportWorksheet = XLSX.utils.aoa_to_sheet(markerReportData); // Set column widths here (e.g., Column A = 15, Column B = 25) markerReportWorksheet["!cols"] = [ - { wch: 20 }, // Column A (Labels: 'Timestamp', 'Value', etc.) - { wch: 30 }, // Column B (Values, where the Date object resides) + { wch: 20 }, // Column A (Labels: 'Timestamp', 'Value', etc.) + { wch: 30 }, // Column B (Values, where the Date object resides) ]; const markerReportSheetName = generateUniqueSheetName( `${seriesTitle} Marker Report`, @@ -338,46 +339,50 @@ function createGraphLineMarkerReportSheet(graphLine: { } // Filter User Markers - const allTargetLines = graphLine.targetLines.filter(line => line.show !== false); - const userMarkers = allTargetLines.filter(line => line.type === 'user_marker' && line.label); + const allTargetLines = graphLine.targetLines.filter( + (line) => line.show !== false, + ); + const userMarkers = allTargetLines.filter( + (line) => line.type === "user_marker" && line.label, + ); // Map Markers to Closest Data Point Index - const markerIndexMap = new Map(); + const markerIndexMap = new Map< + number, + { label: string; originalTimestamp: number } + >(); - userMarkers.forEach(line => { + userMarkers.forEach((line) => { const markerTime = line.markerTimestamp || line.value; // Use the correct high-precision timestamp let closestDataPointIndex = -1; let minTimeDifference = Infinity; // Find the data point with the closest timestamp timestamps.forEach((ts, index) => { - const difference = Math.abs(ts - markerTime); - if (difference < minTimeDifference) { - minTimeDifference = difference; - closestDataPointIndex = index; - } + const difference = Math.abs(ts - markerTime); + if (difference < minTimeDifference) { + minTimeDifference = difference; + closestDataPointIndex = index; + } }); - + // Store the marker data at the index of the closest data point - if (closestDataPointIndex !== -1) { - markerIndexMap.set(closestDataPointIndex, { - label: line.label || 'User Marker', - originalTimestamp: markerTime, - }); + if (closestDataPointIndex !== -1) { + markerIndexMap.set(closestDataPointIndex, { + label: line.label || "User Marker", + originalTimestamp: markerTime, + }); } }); // Add the final header before the timestamp report starts - reportData.push( - ["--- BEGIN DETAILED REPORT ---", ""], - ["", ""], - ); + reportData.push(["--- BEGIN DETAILED REPORT ---", ""], ["", ""]); // Handle case where no user markers were created if (userMarkers.length === 0) { - reportData.push(["No user-created markers found.", ""]); + reportData.push(["No user-created markers found.", ""]); } - + timestamps.forEach((dataPointTimestamp, index) => { const value = values[index]; const markerData = markerIndexMap.get(index); @@ -386,34 +391,36 @@ function createGraphLineMarkerReportSheet(graphLine: { let timeToDisplay = dataPointTimestamp; // Default to data sample time if (markerData) { - finalMarkerLabel = `${markerData.label}`; - timeToDisplay = markerData.originalTimestamp; + finalMarkerLabel = `${markerData.label}`; + timeToDisplay = markerData.originalTimestamp; } // Format the time (using timeToDisplay) - const formattedTime = new Date(timeToDisplay).toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false - }).replace(/ /g, ''); + const formattedTime = new Date(timeToDisplay) + .toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }) + .replace(/ /g, ""); // Row 1: Timestamp reportData.push(["Timestamp", formattedTime]); - + // Row 2: Value const formattedValue = graphLine.renderValue - ? graphLine.renderValue(value) - : value?.toFixed(3) || ""; + ? graphLine.renderValue(value) + : value?.toFixed(3) || ""; reportData.push([`Value (${unitSymbol})`, formattedValue]); - + // Row 3: Marker Name reportData.push(["Marker", finalMarkerLabel]); - + // Separator reportData.push(["", ""]); }); - + return reportData; } diff --git a/electron/src/machines/mock/mock1/Mock1Graph.tsx b/electron/src/machines/mock/mock1/Mock1Graph.tsx index ca6a59823..066d623b1 100644 --- a/electron/src/machines/mock/mock1/Mock1Graph.tsx +++ b/electron/src/machines/mock/mock1/Mock1Graph.tsx @@ -144,4 +144,4 @@ export function Mock1GraphPage() {
); -} \ No newline at end of file +}