diff --git a/app/src/components/SimulationDashboard/AnalyzerManager.tsx b/app/src/components/SimulationDashboard/AnalyzerManager.tsx index ffcba26..c181cf4 100644 --- a/app/src/components/SimulationDashboard/AnalyzerManager.tsx +++ b/app/src/components/SimulationDashboard/AnalyzerManager.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; import { useSceneContext } from "../../modules/scene/sceneContext"; const AnalyzerManager: React.FC = () => { @@ -22,12 +22,37 @@ const AnalyzerManager: React.FC = () => { return String(value ?? "-"); }; + // Keep blocks ref updated to access latest blocks in analysis effect without triggering it + const blocksRef = useRef(blocks); + useEffect(() => { + blocksRef.current = blocks; + }, [blocks]); + + // Clear default graph data on mount + useEffect(() => { + blocksRef.current.forEach((block) => { + block.elements.forEach((element) => { + if (element.type === "graph" && element.dataBinding?.dataType === "single-machine") { + // Check for default data (ElementContent uses Jan, Feb etc as fallback) + // If graphData is undefined, ElementContent shows defaults. We explicitly set to [] to show empty. + // If graphData has "Jan" data, we also clear it. + const hasDefaultData = element.graphData && element.graphData.length > 0 && element.graphData[0].name === "Jan"; + + if (!element.graphData || hasDefaultData) { + updateGraphData(block.blockUuid, element.elementUuid, []); + } + } + }); + }); + }, [updateGraphData]); + useEffect(() => { if (!analysis) return; + // 1. Handle Label-Value (Direct DOM Manipulation) + // Safe to depend on blocks here as we don't update store/blocks blocks.forEach((block) => { block.elements.forEach((element) => { - // 1. Handle Label-Value (Direct DOM Manipulation) if (element.type === "label-value") { if (!element.dataBinding?.dataSource || !element.dataBinding?.dataValue) return; @@ -39,23 +64,13 @@ const AnalyzerManager: React.FC = () => { if (typeof dataValueStr !== "string") return; if (element.dataBinding.dataSource === "global") { - // Handle global metrics - // Expected format: "global.systemPerformance.overallOEE" -> we need last part relative to metrics - // But dataValue usually comes as "systemPerformance.overallOEE" if dataSource is global? - // Let's check ElementEditor: - // It passes "global.systemPerformance.overallOEE" as the ID. - // We need to parse this. - const metrics = getSystemMetrics(); if (metrics) { // If the ID starts with global., strip it. - const path = dataValueStr.startsWith("global.") - ? dataValueStr.replace("global.", "") - : dataValueStr; + const path = dataValueStr.startsWith("global.") ? dataValueStr.replace("global.", "") : dataValueStr; value = resolvePath(metrics, path); } - } else { // Handle Asset Specific const assetAnalysis = getAssetAnalysis(element.dataBinding.dataSource as string); @@ -70,99 +85,72 @@ const AnalyzerManager: React.FC = () => { valueElement.textContent = getFormattedValue(value); } } + }); + }); + }, [analysis, blocks, getAssetAnalysis, getSystemMetrics]); - // 2. Handle Graph (Store Update) - // We shouldn't update store on every frame if data hasn't changed or simpler approach? - // For graphs, we usually push new data points. - // Assuming we want to visualize historical data or current state trends. - else if (element.type === "graph") { + // 2. Handle Graph (Store Update) + // This effect MUST NOT depend on 'blocks' to avoid infinite loop (updateGraphData modifies blocks) + useEffect(() => { + if (!analysis) return; + blocksRef.current.forEach((block) => { + block.elements.forEach((element) => { + if (element.type === "graph") { if (element.dataBinding?.dataType === "single-machine") { if (!element.dataBinding.dataSource || !element.dataBinding.dataValue) return; - // Single machine, multiple values potentially? - // element.dataValue is string[] for single-machine graph const assetId = element.dataBinding.dataSource as string; - const dataKeys = element.dataBinding.dataValue as string[]; // e.g. ["efficiency.overallEffectiveness"] - - // For now support getting the FIRST value as the main value to plot? - // Or if we track history? - // The requirement says: "update the dom ... to show the value". - // UseAnalysisStore has 'historicalData'. - // But usually graphs update by appending new points. - // Here we might just pull the latest historical data or current value. + // Ensure dataValue is an array + const rawDataValue = element.dataBinding.dataValue; + const dataKeys = Array.isArray(rawDataValue) ? rawDataValue : [rawDataValue as string]; const assetAnalysis = getAssetAnalysis(assetId); if (assetAnalysis) { - // If it's a historical plot, we might want to read from assetAnalysis.historicalData (if it exists on client) - // or just push current value to the graph element's local state. - // But here we are asked to manage values using AnalyzerManager. - - // Let's construct a data point. - // GraphDataPoint = { name: string, value: number, ... } - - // We use analysis.lastUpdateTime or similar as 'name' (X-axis) const timeLabel = new Date().toLocaleTimeString(); - - // We need to construct a new GraphDataPoint - const newPoint: any = { name: timeLabel }; + const newPoint: GraphDataPoint = { name: timeLabel, value: 0 }; let hasValidData = false; - dataKeys.forEach((key, index) => { + dataKeys.forEach((key) => { const val = resolvePath(assetAnalysis, key); if (typeof val === "number") { - newPoint["value"] = val; // Primary value - // If we have multiple lines, we might need value1, value2 etc, but Recharts usually takes keys. - // The ElementContent uses 'value' key hardcoded for single line? - // Let's check ElementContent.tsx... - // It renders - // So it only supports ONE series called 'value' currently for default graphs? - // Or we can map index to keys? - - // For now, let's assume single generic 'value' or update if strictly array. - + newPoint["value"] = val; hasValidData = true; } }); if (hasValidData) { - // We need to update the element's graphData. - // This is a store update, triggers re-render. - // BE CAREFUL: varying this too fast will kill performance. - // Maybe throttle this? useAnalysisStore updates often? - const currentGraphData = element.graphData || []; - const newGraphData = [...currentGraphData, newPoint].slice(-20); // Keep last 20 points + const newGraphData = [...currentGraphData, newPoint].slice(-20); + // Always update for single-machine as we are appending time-series data updateGraphData(block.blockUuid, element.elementUuid, newGraphData); } } - } else if (element.dataBinding?.dataType === "multiple-machine") { - // Multiple machines, single common value const assetIds = element.dataBinding.dataSource as string[]; const commonValueKey = element.dataBinding.commonValue as string; if (assetIds && commonValueKey) { - // Bar chart comparing machines? - // Data point per machine. - const newGraphData = assetIds.map(assetId => { + const newGraphData = assetIds.map((assetId) => { const assetAnalysis = getAssetAnalysis(assetId); const val = assetAnalysis ? resolvePath(assetAnalysis, commonValueKey) : 0; return { name: assetAnalysis?.assetName || assetId, - value: typeof val === "number" ? val : 0 + value: typeof val === "number" ? val : 0, }; }); - updateGraphData(block.blockUuid, element.elementUuid, newGraphData); + // Deep check to avoid unnecessary updates + if (JSON.stringify(newGraphData) !== JSON.stringify(element.graphData)) { + updateGraphData(block.blockUuid, element.elementUuid, newGraphData); + } } } } }); }); - - }, [analysis, blocks, getAssetAnalysis, getSystemMetrics, updateGraphData]); + }, [analysis, getAssetAnalysis, updateGraphData]); return null; }; diff --git a/app/src/components/SimulationDashboard/components/element/ElementContent.tsx b/app/src/components/SimulationDashboard/components/element/ElementContent.tsx index e81208b..aac5e73 100644 --- a/app/src/components/SimulationDashboard/components/element/ElementContent.tsx +++ b/app/src/components/SimulationDashboard/components/element/ElementContent.tsx @@ -20,6 +20,7 @@ import { Cell, } from "recharts"; import { UIElement } from "../../../../types/exportedTypes"; +import { usePlayButtonStore } from "../../../../store/ui/usePlayButtonStore"; interface ElementContentProps { element: UIElement; @@ -29,16 +30,18 @@ interface ElementContentProps { const COLORS = ["#6f42c1", "#c4abf1", "#a07ad8", "#d2baff", "#b992e2", "#c4abf1"]; const ElementContent: React.FC = ({ element, resolvedData }) => { + const { isPlaying } = usePlayButtonStore(); const chartData = - resolvedData.graphData || - element.graphData || [ - { name: "Jan", value: 400 }, - { name: "Feb", value: 300 }, - { name: "Mar", value: 600 }, - { name: "Apr", value: 800 }, - { name: "May", value: 500 }, - { name: "Jun", value: 900 }, - ]; + isPlaying || !resolvedData.graphData + ? resolvedData.graphData ?? [] + : [ + { name: "Jan", value: 400 }, + { name: "Feb", value: 300 }, + { name: "Mar", value: 600 }, + { name: "Apr", value: 800 }, + { name: "May", value: 500 }, + { name: "Jun", value: 900 }, + ]; const tooltipStyle = { contentStyle: { @@ -88,14 +91,7 @@ const ElementContent: React.FC = ({ element, resolvedData } - + ) : element.graphType === "pie" ? ( @@ -121,14 +117,7 @@ const ElementContent: React.FC = ({ element, resolvedData } - + ) : ( diff --git a/app/src/store/simulation/useSimulationDashBoardStore.ts b/app/src/store/simulation/useSimulationDashBoardStore.ts index adc8abd..c10806b 100644 --- a/app/src/store/simulation/useSimulationDashBoardStore.ts +++ b/app/src/store/simulation/useSimulationDashBoardStore.ts @@ -1,7 +1,6 @@ import { MathUtils } from "three"; import { create } from "zustand"; import { immer } from "zustand/middleware/immer"; -import { defaultGraphData } from "../../components/SimulationDashboard/data/defaultGraphData"; import { Block, UIElement, ExtendedCSSProperties } from "../../types/exportedTypes"; interface SimulationDashboardStore { @@ -291,7 +290,7 @@ export const createSimulationDashboardStore = () => { borderRadius: "4px", padding: "8px", }, - graphData: defaultGraphData, + graphData: [], size: { width: 400, height: 200 }, }; @@ -482,7 +481,7 @@ export const createSimulationDashboardStore = () => { if (element && element.type === "graph") { element.graphType = graphType; element.graphTitle = `${graphType.charAt(0).toUpperCase() + graphType.slice(1)} Chart`; - element.graphData = defaultGraphData; + element.graphData = []; } } }); @@ -655,7 +654,7 @@ export const createSimulationDashboardStore = () => { borderRadius: "4px", padding: "8px", }, - graphData: defaultGraphData, + graphData: [], size: { width: 400, height: 200 }, }; @@ -841,7 +840,7 @@ export const createSimulationDashboardStore = () => { if (element && element.type === "graph") { element.graphType = graphType; element.graphTitle = `${graphType.charAt(0).toUpperCase() + graphType.slice(1)} Chart`; - element.graphData = defaultGraphData; + element.graphData = []; } } return blocks;