diff --git a/electron/src/components/graph/AddMarkerDialog.tsx b/electron/src/components/graph/AddMarkerDialog.tsx new file mode 100644 index 000000000..11e352480 --- /dev/null +++ b/electron/src/components/graph/AddMarkerDialog.tsx @@ -0,0 +1,154 @@ +import React, { useState, useEffect } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { TouchButton } from "@/components/touch/TouchButton"; +import { TimeInput } from "@/components/time/TimeInput"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; +import { Icon } from "@/components/Icon"; + +type AddMarkerDialogProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + onAddMarker: (name: string, timestamp: number, color?: string) => void; + currentTimestamp: number | null; + defaultName?: string; +}; + +export function AddMarkerDialog({ + open, + onOpenChange, + onAddMarker, + currentTimestamp, + defaultName = "", +}: AddMarkerDialogProps) { + const [name, setName] = useState(defaultName); + const [selectedTimestamp, setSelectedTimestamp] = useState( + null, + ); + const [color, setColor] = useState("#000000"); + + // Reset form when dialog opens/closes + useEffect(() => { + if (open) { + setName(defaultName); + // Always use current time when dialog opens + setSelectedTimestamp(currentTimestamp); + } else { + setName(""); + setSelectedTimestamp(null); + } + }, [open, currentTimestamp, defaultName]); + + const handleAdd = () => { + if (!name.trim()) return; + + // Always use current timestamp when dialog is opened (as per requirement) + // The time input is optional and only for historical markers + const timestamp = currentTimestamp || Date.now(); + if (!timestamp) return; + + onAddMarker(name.trim(), timestamp, color); + onOpenChange(false); + }; + + const handleCancel = () => { + onOpenChange(false); + }; + + return ( + + + + + + Add Marker + + + Create a marker for all graphs of this machine at the current time. + + + + +
+ {/* Name Input */} +
+ + setName(e.target.value)} + placeholder="Enter marker name" + autoFocus + onKeyDown={(e) => { + if (e.key === "Enter") { + handleAdd(); + } + }} + /> +
+ + {/* Time Input (optional) */} +
+ + setSelectedTimestamp(currentTimestamp)} + /> +

+ Leave empty to use current time +

+
+ + {/* Color Input */} +
+ +
+ setColor(e.target.value)} + className="border-input h-9 w-20 cursor-pointer rounded-md border" + /> + setColor(e.target.value)} + placeholder="#000000" + /> +
+
+
+ + +
+ + Abort + + + Add Marker + +
+
+
+ ); +} diff --git a/electron/src/components/graph/GraphControls.tsx b/electron/src/components/graph/GraphControls.tsx index a5daa43d4..53a9d2601 100644 --- a/electron/src/components/graph/GraphControls.tsx +++ b/electron/src/components/graph/GraphControls.tsx @@ -21,6 +21,7 @@ export function GraphControls({ onSwitchToLive, onSwitchToHistorical, onExport, + onAddMarker, timeWindowOptions = DEFAULT_TIME_WINDOW_OPTIONS, showFromTimestamp, onShowFromChange, @@ -102,16 +103,27 @@ export function GraphControls({ Live - {onExport && ( + {(onExport || onAddMarker) && ( <>
- - Export - + {onAddMarker && ( + + Add Marker + + )} + {onExport && ( + + Export + + )} )} @@ -127,6 +139,7 @@ export function FloatingControlPanel({ onSwitchToLive, onSwitchToHistorical, onExport, + onAddMarker, timeWindowOptions = DEFAULT_TIME_WINDOW_OPTIONS, showFromTimestamp, onShowFromChange, @@ -218,9 +231,18 @@ export function FloatingControlPanel({ > Live - {isExpanded && onExport && ( + {isExpanded && (onExport || onAddMarker) && (
)} + {onAddMarker && ( + + Add Marker + + )} {onExport && ( ; + newData: TimeSeriesData | TimeSeriesData[]; + config: GraphConfig; + unit?: Unit; + renderValue?: (value: number) => string; + graphId: string; + currentTimeSeries: TimeSeries | null; + machineId?: string; + markers?: Array<{ + timestamp: number; + name: string; + value?: number; + color?: string; + }>; +}; + +function createMarkerElement( + timestamp: number, + name: string, + value: number, + graphMin: number, + graphMax: number, + startTime: number, + endTime: number, + graphWidth: number, + graphHeight: number, + color?: string, +) { + // Calculate the X position of the timestamp + const ratio = (timestamp - startTime) / (endTime - startTime); + const xPos = Math.min(Math.max(ratio, 0), 1) * graphWidth; + + // Calculate the Y position of the value (from bottom of graph) + const normalizedValue = (value - graphMin) / (graphMax - graphMin); + const valueY = graphHeight - normalizedValue * graphHeight; + + // Create vertical line that spans full height (shows time position) + const line = document.createElement("div"); + line.style.position = "absolute"; + line.style.left = `${xPos}px`; + line.style.top = "0px"; + line.style.height = `${graphHeight}px`; + line.style.width = "2px"; + // Use custom color if provided, otherwise default gray + const lineColor = color || "rgba(0, 0, 0, 0.5)"; + line.style.background = lineColor; + line.className = "vertical-marker"; + + // Create a point at the actual data value position + const point = document.createElement("div"); + point.style.position = "absolute"; + point.style.left = `${xPos}px`; + point.style.top = `${valueY}px`; + point.style.width = "8px"; + point.style.height = "8px"; + point.style.borderRadius = "50%"; + // Use custom color if provided, otherwise default black + const pointColor = color || "rgba(0, 0, 0, 0.8)"; + point.style.background = pointColor; + point.style.transform = "translate(-50%, -50%)"; + point.style.border = "2px solid white"; + point.className = "marker-point"; + + const label = document.createElement("div"); + label.textContent = name; + label.style.position = "absolute"; + label.style.left = `${xPos}px`; + label.style.top = `${graphHeight + 5}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, point, label }; +} + +function GraphWithMarkerControlsContent({ + syncHook, + newData, + config, + unit, + renderValue, + graphId, + currentTimeSeries, + machineId: providedMachineId, + markers: providedMarkers, + graphWrapperRef, +}: GraphWithMarkerControlsProps & { + graphWrapperRef: React.RefObject; +}) { + const { setMachineId, setCurrentTimestamp } = useMarkerContext(); + + // Auto-detect machineId from graphId if not provided (extract base name) + // e.g., "pressure-graph" -> "pressure", "extruder-graphs" -> "extruder-graphs" + const machineId = providedMachineId || graphId.split("-")[0] || "default"; + + // Update context with machineId and current timestamp + useEffect(() => { + setMachineId(machineId); + }, [machineId, setMachineId]); + + useEffect(() => { + if (currentTimeSeries?.current?.timestamp) { + setCurrentTimestamp(currentTimeSeries.current.timestamp); + } + }, [currentTimeSeries?.current?.timestamp, setCurrentTimestamp]); + + // Use provided markers or load from marker manager + const markerManager = useMarkerManager(machineId); + const markers = providedMarkers || markerManager.markers; + + // 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]); + + // Marker Drawing Effect + useEffect(() => { + if (!graphWrapperRef.current || !currentTimeSeries?.current) return; + + const graphEl = graphWrapperRef.current; + // Find chart container via uPlot canvas (canvas is always a direct child) + const canvas = graphEl.querySelector("canvas"); + if (!canvas) return; + const chartContainer = canvas.parentElement; + if (!chartContainer) return; + + const graphWidth = chartContainer.clientWidth; + const graphHeight = chartContainer.clientHeight; + + const overlayContainer = chartContainer.parentElement; + if (!overlayContainer) return; + + // Remove previous markers, points and labels from the overlay container + overlayContainer + .querySelectorAll(".vertical-marker, .marker-point, .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; + + // Calculate Y-axis scale from visible data (similar to createChart.ts) + const visibleValues: number[] = []; + + // Collect values from the time series in the visible time window + currentTimeSeries.long.values + .filter((v): v is TimeSeriesValue => v !== null) + .forEach((v) => { + if (v.timestamp >= startTime && v.timestamp <= endTime) { + visibleValues.push(v.value); + } + }); + + // Include config lines in the scale calculation + config.lines?.forEach((line) => { + if (line.show !== false) { + visibleValues.push(line.value); + } + }); + + // Calculate min/max with 10% padding (matching createChart.ts behavior) + let graphMin: number, graphMax: number; + if (visibleValues.length > 0) { + const minY = Math.min(...visibleValues); + const maxY = Math.max(...visibleValues); + const range = maxY - minY || Math.abs(maxY) * 0.1 || 1; + graphMin = minY - range * 0.1; + graphMax = maxY + range * 0.1; + } else { + // Fallback if no data is available + graphMin = -1; + graphMax = 1; + } + + markers.forEach(({ timestamp, name, value, color }) => { + if (timestamp >= startTime && timestamp <= endTime) { + // Find the data point closest to the marker timestamp to get the correct Y-value + // If value is not provided, use the closest data point + let markerValue = value; + if (markerValue === undefined && currentTimeSeries) { + const validValues = currentTimeSeries.long.values.filter( + (v): v is TimeSeriesValue => v !== null, + ); + if (validValues.length > 0) { + const closest = validValues.reduce((prev, curr) => + Math.abs(curr.timestamp - timestamp) < + Math.abs(prev.timestamp - timestamp) + ? curr + : prev, + ); + markerValue = closest.value; + } + } + + // Use a default value if still undefined + if (markerValue === undefined) { + markerValue = (graphMin + graphMax) / 2; + } + + // Create marker element (full height line + point at data value) + const { line, point, label } = createMarkerElement( + timestamp, + name, + markerValue, + graphMin, + graphMax, + startTime, + endTime, + graphWidth, + graphHeight, + color, + ); + + overlayContainer.appendChild(line); + overlayContainer.appendChild(point); + overlayContainer.appendChild(label); + } + }); + }, [ + markers, + currentTimeSeries, + timeTick, + config.defaultTimeWindow, + syncHook.controlProps.timeWindow, + ]); + + // Use original config without adding marker lines (markers are overlay elements) + const finalConfig = config; + + return ( +
+
+ {/* Render the core chart component */} + +
+
+ ); +} + +export function GraphWithMarkerControls(props: GraphWithMarkerControlsProps) { + const graphWrapperRef = useRef(null); + + // Use context if available (from SyncedFloatingControlPanel), otherwise work without it + return ( + + ); +} diff --git a/electron/src/components/graph/MarkerContext.tsx b/electron/src/components/graph/MarkerContext.tsx new file mode 100644 index 000000000..111899d3d --- /dev/null +++ b/electron/src/components/graph/MarkerContext.tsx @@ -0,0 +1,42 @@ +import React, { createContext, useContext, useState } from "react"; + +type MarkerContextType = { + machineId: string | null; + setMachineId: (id: string) => void; + currentTimestamp: number | null; + setCurrentTimestamp: (timestamp: number) => void; +}; + +const MarkerContext = createContext(null); + +export function MarkerProvider({ children }: { children: React.ReactNode }) { + const [machineId, setMachineId] = useState(null); + const [currentTimestamp, setCurrentTimestamp] = useState(null); + + return ( + + {children} + + ); +} + +export function useMarkerContext() { + const context = useContext(MarkerContext); + // Return a default context if not within a provider (for non-graph pages) + if (!context) { + return { + machineId: null, + setMachineId: () => {}, + currentTimestamp: null, + setCurrentTimestamp: () => {}, + }; + } + return context; +} diff --git a/electron/src/components/graph/SyncedComponents.tsx b/electron/src/components/graph/SyncedComponents.tsx index 7c182d5a3..5cfd3ed65 100644 --- a/electron/src/components/graph/SyncedComponents.tsx +++ b/electron/src/components/graph/SyncedComponents.tsx @@ -1,9 +1,12 @@ -import React from "react"; +import React, { useState } from "react"; import { BigGraph } from "./BigGraph"; import { GraphControls, FloatingControlPanel } from "./GraphControls"; import { useGraphSync } from "./useGraphSync"; import { BigGraphProps, PropGraphSync, TimeWindowOption } from "./types"; import { GraphExportData } from "./excelExport"; +import { useMarkerManager } from "./useMarkerManager"; +import { AddMarkerDialog } from "./AddMarkerDialog"; +import { MarkerProvider, useMarkerContext } from "./MarkerContext"; export function SyncedBigGraph({ syncGraph: externalSyncGraph, @@ -50,7 +53,7 @@ export function SyncedGraphControls({ ); } -export function SyncedFloatingControlPanel({ +function SyncedFloatingControlPanelInner({ controlProps, timeWindowOptions, ...props @@ -60,13 +63,56 @@ export function SyncedFloatingControlPanel({ }) { const defaultSync = useGraphSync(); const finalProps = controlProps || defaultSync.controlProps; + const { machineId, currentTimestamp } = useMarkerContext(); + + // Use machineId from context (set by GraphWithMarkerControls) or fallback to "default" + const detectedMachineId = machineId || "default"; + const { addMarker } = useMarkerManager(detectedMachineId); + const [isMarkerDialogOpen, setIsMarkerDialogOpen] = useState(false); + + // Always use current timestamp from context (live time from graphs) or current time + // As per requirement: "always use the current time" + const markerTimestamp = currentTimestamp || Date.now(); + + const handleAddMarker = (name: string, timestamp: number, color?: string) => { + addMarker(name, timestamp, color); + }; return ( - + <> + setIsMarkerDialogOpen(true)} + {...props} + /> + + + ); +} + +export function SyncedFloatingControlPanel({ + controlProps, + timeWindowOptions, + ...props +}: { + controlProps?: ReturnType["controlProps"]; + timeWindowOptions?: TimeWindowOption[]; +}) { + // Wrap in MarkerProvider so it's only available for graph pages + return ( + + + ); } diff --git a/electron/src/components/graph/excelExport.ts b/electron/src/components/graph/excelExport.ts index 493975862..8b86fcab9 100644 --- a/electron/src/components/graph/excelExport.ts +++ b/electron/src/components/graph/excelExport.ts @@ -84,6 +84,27 @@ export function exportGraphsToExcel( XLSX.utils.book_append_sheet(workbook, dataWorksheet, dataSheetName); } + // Excel worksheet for timestamps and timestamp markers + if (graphLineData.targetLines.length > 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, + ); + } + processedCount++; }); @@ -290,6 +311,119 @@ function createGraphLineDataSheet(graphLine: { }); } +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; + } + + // Filter User Markers + 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< + number, + { label: string; originalTimestamp: number } + >(); + + 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 timestamp 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.", ""]); + } + + timestamps.forEach((dataPointTimestamp, index) => { + const value = values[index]; + const markerData = markerIndexMap.get(index); + + let finalMarkerLabel = ""; + let timeToDisplay = dataPointTimestamp; // Default to data sample time + + if (markerData) { + 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, ""); + + // 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 Name + reportData.push(["Marker", 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/index.ts b/electron/src/components/graph/index.ts index bfa18fb38..6ef87b1e0 100644 --- a/electron/src/components/graph/index.ts +++ b/electron/src/components/graph/index.ts @@ -10,6 +10,9 @@ export { AutoSyncedBigGraph, } from "./SyncedComponents"; +// Context +export { MarkerProvider } from "./MarkerContext"; + // Hooks export { useGraphSync } from "./useGraphSync"; diff --git a/electron/src/components/graph/types.ts b/electron/src/components/graph/types.ts index beff72807..a529cd480 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 = { @@ -85,6 +86,7 @@ export type ControlProps = { onSwitchToLive: () => void; onSwitchToHistorical: () => void; onExport?: () => void; + onAddMarker?: () => void; timeWindowOptions?: TimeWindowOption[]; showFromTimestamp?: number | null; onShowFromChange?: (timestamp: number | null) => void; diff --git a/electron/src/components/graph/useMarkerManager.ts b/electron/src/components/graph/useMarkerManager.ts new file mode 100644 index 000000000..45d82976b --- /dev/null +++ b/electron/src/components/graph/useMarkerManager.ts @@ -0,0 +1,139 @@ +import { useState, useCallback, useEffect } from "react"; + +export type Marker = { + timestamp: number; + name: string; + value?: number; // Optional: value at that timestamp + color?: string; // Optional: color for the marker +}; + +// Custom event for marker updates to ensure immediate propagation +const MARKER_UPDATE_EVENT = "marker-update"; + +/** + * Centralized marker management for all graphs of a machine + * Markers are stored per machine (machineId) and appear on all graphs + */ +export function useMarkerManager(machineId: string) { + const storageKey = `machine-markers-${machineId}`; + + // Load markers from localStorage + const loadMarkers = useCallback((): Marker[] => { + try { + const stored = localStorage.getItem(storageKey); + if (stored) { + const allMarkers: Marker[] = JSON.parse(stored); + + // Remove markers older than 7 days to save storage space + const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000; + const recentMarkers = allMarkers.filter( + (marker) => marker.timestamp >= sevenDaysAgo, + ); + + // Limit to max 200 markers per machine to prevent storage bloat + const maxMarkers = 200; + const limitedMarkers = + recentMarkers.length > maxMarkers + ? recentMarkers.slice(-maxMarkers) + : recentMarkers; + + // Save cleaned markers back if we removed any + if (limitedMarkers.length !== allMarkers.length) { + localStorage.setItem(storageKey, JSON.stringify(limitedMarkers)); + } + + return limitedMarkers; + } + } catch (error) { + console.warn("Failed to load markers from localStorage:", error); + } + return []; + }, [storageKey]); + + const [markers, setMarkers] = useState(loadMarkers); + + // Listen for marker updates from other components + useEffect(() => { + const handleMarkerUpdate = (event: CustomEvent) => { + if (event.detail?.machineId === machineId) { + // Reload markers immediately when updated + setMarkers(loadMarkers()); + } + }; + + window.addEventListener( + MARKER_UPDATE_EVENT, + handleMarkerUpdate as EventListener, + ); + + return () => { + window.removeEventListener( + MARKER_UPDATE_EVENT, + handleMarkerUpdate as EventListener, + ); + }; + }, [machineId, loadMarkers]); + + // Save markers to localStorage whenever they change + useEffect(() => { + try { + // Limit to max 200 markers per machine + const maxMarkers = 200; + const markersToSave = + markers.length > maxMarkers ? markers.slice(-maxMarkers) : markers; + + localStorage.setItem(storageKey, JSON.stringify(markersToSave)); + } catch (error) { + console.warn("Failed to save markers to localStorage:", error); + } + }, [markers, storageKey]); + + const addMarker = useCallback( + (name: string, timestamp: number, color?: string, value?: number) => { + const newMarker: Marker = { + timestamp, + name, + color, + value, + }; + setMarkers((prev) => { + const updated = [...prev, newMarker]; + // Save immediately to localStorage + try { + const maxMarkers = 200; + const markersToSave = + updated.length > maxMarkers ? updated.slice(-maxMarkers) : updated; + localStorage.setItem(storageKey, JSON.stringify(markersToSave)); + } catch (error) { + console.warn("Failed to save marker to localStorage:", error); + } + // Dispatch event to notify other components immediately + window.dispatchEvent( + new CustomEvent(MARKER_UPDATE_EVENT, { + detail: { machineId, markers: updated }, + }), + ); + return updated; + }); + return newMarker; + }, + [storageKey, machineId], + ); + + const removeMarker = useCallback((timestamp: number) => { + setMarkers((prev) => + prev.filter((marker) => marker.timestamp !== timestamp), + ); + }, []); + + const clearMarkers = useCallback(() => { + setMarkers([]); + }, []); + + return { + markers, + addMarker, + removeMarker, + clearMarkers, + }; +} diff --git a/electron/src/machines/extruder/extruder2/Extruder2Graph.tsx b/electron/src/machines/extruder/extruder2/Extruder2Graph.tsx index b8bc86e26..69d118afa 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 { @@ -242,7 +243,7 @@ export function Extruder2GraphsPage() { return (
- value.toFixed(2)} graphId="pressure-graph" + currentTimeSeries={pressure} /> - value.toFixed(1)} graphId="combined-temperatures" + currentTimeSeries={nozzleTemperature} /> - value.toFixed(1)} graphId="combined-power" + currentTimeSeries={combinedPower} /> - value.toFixed(2)} graphId="motor-current" + currentTimeSeries={motorCurrent} /> - value.toFixed(0)} graphId="rpm-graph" + currentTimeSeries={motorScrewRpm} />
diff --git a/electron/src/machines/mock/mock1/Mock1Graph.tsx b/electron/src/machines/mock/mock1/Mock1Graph.tsx index 5f0805d96..066d623b1 100644 --- a/electron/src/machines/mock/mock1/Mock1Graph.tsx +++ b/electron/src/machines/mock/mock1/Mock1Graph.tsx @@ -8,6 +8,8 @@ import { import React 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"; export function Mock1GraphPage() { const { sineWaveSum } = useMock1(); @@ -98,40 +100,44 @@ export function Mock1GraphPage() { return (
- value.toFixed(3)} graphId="single-graph1" + currentTimeSeries={sineWaveSum} /> - value.toFixed(3)} graphId="combined-graph" + currentTimeSeries={sineWaveSum} /> - value.toFixed(3)} graphId="single-graph2" + currentTimeSeries={sineWaveSum} /> - value.toFixed(3)} graphId="single-graph" + currentTimeSeries={sineWaveSum} />
diff --git a/electron/src/machines/winder/winder2/Winder2Graphs.tsx b/electron/src/machines/winder/winder2/Winder2Graphs.tsx index ab3adb066..450ceea40 100644 --- a/electron/src/machines/winder/winder2/Winder2Graphs.tsx +++ b/electron/src/machines/winder/winder2/Winder2Graphs.tsx @@ -11,6 +11,7 @@ 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"; export function Winder2GraphsPage() { const { @@ -96,7 +97,7 @@ export function SpoolRpmGraph({ }; return ( - ); } @@ -155,7 +157,7 @@ export function TraversePositionGraph({ }; return ( - ); } @@ -191,7 +194,7 @@ export function TensionArmAngleGraph({ }; return ( - ); } @@ -225,8 +229,9 @@ export function SpoolProgressGraph({ exportFilename: "spool_progress", }; + // NOTE: Assuming this graph starts at 0, and the max is the total capacity. return ( - ); } @@ -274,7 +280,7 @@ export function PullerSpeedGraph({ }; return ( - ); }