feat: Implement simulation dashboard analysis manager and UI elements for displaying simulation data.
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user