feat: add LiveTemperatureThresholdChart component with SSE-driven data synchronization and automated polling fallback
This commit is contained in:
@@ -295,31 +295,33 @@ export function LiveTemperatureThresholdChart({
|
|||||||
[data, series, viewMode],
|
[data, series, viewMode],
|
||||||
);
|
);
|
||||||
const visibleRange = zoomRange ?? autoWindowRange;
|
const visibleRange = zoomRange ?? autoWindowRange;
|
||||||
|
const visibleRangeKey = visibleRange ? `${visibleRange[0]}:${visibleRange[1]}` : "full";
|
||||||
|
|
||||||
const zoomedData = useMemo(() => {
|
const zoomedData = useMemo(() => {
|
||||||
if (!visibleRange || data.length === 0) return data;
|
if (!visibleRange || data.length === 0) return data;
|
||||||
const [start, end] = visibleRange;
|
const [start, end] = visibleRange;
|
||||||
return data.slice(start, end + 1);
|
return data.slice(start, end + 1);
|
||||||
}, [data, visibleRange]);
|
}, [data, visibleRangeKey]);
|
||||||
|
|
||||||
useEffect(() => {
|
const nextTargetResolution = useMemo(() => {
|
||||||
if (visibleRange && data.length > 0) {
|
if (visibleRange && data.length > 0) {
|
||||||
const zoomedData = data.slice(visibleRange[0], visibleRange[1] + 1);
|
const zoomedData = data.slice(visibleRange[0], visibleRange[1] + 1);
|
||||||
if (zoomedData.length > 0) {
|
if (zoomedData.length > 0) {
|
||||||
const startTs = zoomedData[0].ts;
|
const startTs = zoomedData[0].ts;
|
||||||
const endTs = zoomedData[zoomedData.length - 1].ts;
|
const endTs = zoomedData[zoomedData.length - 1].ts;
|
||||||
if (endTs - startTs <= 2 * 60 * 60 * 1000) {
|
if (endTs - startTs <= 2 * 60 * 60 * 1000) {
|
||||||
setTargetResolution("1m");
|
return "1m";
|
||||||
} else {
|
|
||||||
setTargetResolution("10m");
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
setTargetResolution("10m");
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
setTargetResolution("10m");
|
|
||||||
}
|
}
|
||||||
}, [visibleRange, data]);
|
return "10m";
|
||||||
|
}, [data, visibleRangeKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (targetResolution !== nextTargetResolution) {
|
||||||
|
setTargetResolution(nextTargetResolution);
|
||||||
|
}
|
||||||
|
}, [targetResolution, nextTargetResolution]);
|
||||||
|
|
||||||
const tzOffset = row?.tz_offset_seconds ?? 0;
|
const tzOffset = row?.tz_offset_seconds ?? 0;
|
||||||
const settlementObs = useMemo(() => {
|
const settlementObs = useMemo(() => {
|
||||||
@@ -786,7 +788,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Chart */}
|
{/* Chart */}
|
||||||
<div className="relative min-h-0 flex-1 p-2">
|
<div className="relative min-h-[240px] flex-1 p-2">
|
||||||
{/* Interactive legend */}
|
{/* Interactive legend */}
|
||||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1.5 px-3 py-1.5 text-[11px] border-b border-[#e2e8f0] bg-white">
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-1.5 px-3 py-1.5 text-[11px] border-b border-[#e2e8f0] bg-white">
|
||||||
{chartSeries.length > 1 &&
|
{chartSeries.length > 1 &&
|
||||||
@@ -832,7 +834,13 @@ export function LiveTemperatureThresholdChart({
|
|||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
minWidth={1}
|
||||||
|
minHeight={220}
|
||||||
|
initialDimension={{ width: 1, height: 220 }}
|
||||||
|
>
|
||||||
<ReComposedChart
|
<ReComposedChart
|
||||||
data={zoomedData}
|
data={zoomedData}
|
||||||
margin={{ top: 16, right: compact ? 20 : 44, left: 4, bottom: 8 }}
|
margin={{ top: 16, right: compact ? 20 : 44, left: 4, bottom: 8 }}
|
||||||
|
|||||||
@@ -106,6 +106,15 @@ export function runTests() {
|
|||||||
assert(chart.includes("加载图表") && chart.includes("absolute inset-2"), "temperature chart must render an in-chart loading overlay");
|
assert(chart.includes("加载图表") && chart.includes("absolute inset-2"), "temperature chart must render an in-chart loading overlay");
|
||||||
assert(chart.includes("viewMode"), "temperature chart must expose a view mode for DEB-peak auto view versus full-day view");
|
assert(chart.includes("viewMode"), "temperature chart must expose a view mode for DEB-peak auto view versus full-day view");
|
||||||
assert(chart.includes("getDebPeakWindowRange"), "temperature chart must derive its default view from the DEB peak window");
|
assert(chart.includes("getDebPeakWindowRange"), "temperature chart must derive its default view from the DEB peak window");
|
||||||
|
assert(chart.includes("nextTargetResolution"), "temperature chart must derive target resolution without setting state on every render");
|
||||||
|
assert(
|
||||||
|
chart.includes("targetResolution !== nextTargetResolution"),
|
||||||
|
"temperature chart must guard target-resolution state updates to prevent render/update loops",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
chart.includes("initialDimension") && chart.includes("minHeight={"),
|
||||||
|
"temperature chart ResponsiveContainer must have stable initial/min dimensions to avoid zero-size resize loops",
|
||||||
|
);
|
||||||
assert(!chart.includes("3D"), "temperature chart UI must not expose a 3D/future-forecast mode");
|
assert(!chart.includes("3D"), "temperature chart UI must not expose a 3D/future-forecast mode");
|
||||||
assert(!chart.includes("build3DayChartData"), "temperature chart component must not render future prediction curves");
|
assert(!chart.includes("build3DayChartData"), "temperature chart component must not render future prediction curves");
|
||||||
assert(
|
assert(
|
||||||
|
|||||||
Reference in New Issue
Block a user