feat: Implement simulation dashboard analysis manager and UI elements for displaying simulation data.

This commit is contained in:
2025-12-18 12:49:35 +05:30
parent 8937e85c1b
commit 7ad185e057
3 changed files with 71 additions and 95 deletions

View File

@@ -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") {
// 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 <Line dataKey="value" ... />
// 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,
};
});
// 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;
};

View File

@@ -20,6 +20,7 @@ import {
Cell,
} from "recharts";
import { UIElement } from "../../../../types/exportedTypes";
import { usePlayButtonStore } from "../../../../store/ui/usePlayButtonStore";
interface ElementContentProps {
element: UIElement;
@@ -29,9 +30,11 @@ interface ElementContentProps {
const COLORS = ["#6f42c1", "#c4abf1", "#a07ad8", "#d2baff", "#b992e2", "#c4abf1"];
const ElementContent: React.FC<ElementContentProps> = ({ element, resolvedData }) => {
const { isPlaying } = usePlayButtonStore();
const chartData =
resolvedData.graphData ||
element.graphData || [
isPlaying || !resolvedData.graphData
? resolvedData.graphData ?? []
: [
{ name: "Jan", value: 400 },
{ name: "Feb", value: 300 },
{ name: "Mar", value: 600 },
@@ -88,14 +91,7 @@ const ElementContent: React.FC<ElementContentProps> = ({ element, resolvedData }
<XAxis dataKey="name" stroke="rgba(255,255,255,0.6)" fontSize={12} />
<YAxis stroke="rgba(255,255,255,0.6)" fontSize={12} />
<Tooltip {...tooltipStyle} />
<Area
type="monotone"
dataKey="value"
stroke="#c4abf1"
fill="#6f42c1"
strokeWidth={2}
isAnimationActive={false}
/>
<Area type="monotone" dataKey="value" stroke="#c4abf1" fill="#6f42c1" strokeWidth={2} isAnimationActive={false} />
</AreaChart>
) : element.graphType === "pie" ? (
<PieChart>
@@ -121,14 +117,7 @@ const ElementContent: React.FC<ElementContentProps> = ({ element, resolvedData }
<PolarGrid stroke="rgba(255,255,255,0.1)" />
<PolarAngleAxis dataKey="name" stroke="rgba(255,255,255,0.6)" />
<PolarRadiusAxis stroke="rgba(255,255,255,0.3)" />
<Radar
name="Value"
dataKey="value"
stroke="#6f42c1"
fill="#c4abf1"
fillOpacity={0.6}
isAnimationActive={false}
/>
<Radar name="Value" dataKey="value" stroke="#6f42c1" fill="#c4abf1" fillOpacity={0.6} isAnimationActive={false} />
<Tooltip {...tooltipStyle} />
</RadarChart>
) : (

View File

@@ -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;