feat(burst-detection): update analysis parameters

This commit is contained in:
2026-07-17 16:24:04 +08:00
parent a52c04204d
commit 7af90e495d
5 changed files with 521 additions and 794 deletions
@@ -5,23 +5,23 @@ import { Box, Button, Chip, Tooltip, Typography } from "@mui/material";
import { DataGrid, GridColDef } from "@mui/x-data-grid";
import { zhCN } from "@mui/x-data-grid/locales";
import {
CheckCircleOutline as CheckCircleIcon,
ErrorOutline as ErrorOutlineIcon,
FormatListBulleted,
InfoOutlined as InfoOutlinedIcon,
Room as RoomIcon,
ShowChart as ShowChartIcon,
CheckCircleOutline as CheckCircleIcon,
ErrorOutline as ErrorOutlineIcon,
} from "@mui/icons-material";
import ReactECharts from "echarts-for-react";
import dayjs from "dayjs";
import { useMap } from "@components/olmap/core/MapComponent";
import { queryFeaturesByIds } from "@/utils/mapQueryService";
import { GeoJSON } from "ol/format";
import Feature from "ol/Feature";
import { GeoJSON } from "ol/format";
import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";
import { Circle, Fill, Stroke, Style } from "ol/style";
import { bbox, featureCollection } from "@turf/turf";
import { useMap } from "@components/olmap/core/MapComponent";
import { queryFeaturesByIds } from "@/utils/mapQueryService";
import { BurstDetectionResult, BurstDetectionRow } from "./types";
export interface BurstDetectionResultsState {
@@ -29,9 +29,7 @@ export interface BurstDetectionResultsState {
}
export const createBurstDetectionResultsState =
(): BurstDetectionResultsState => ({
selectedDay: null,
});
(): BurstDetectionResultsState => ({ selectedDay: null });
interface Props {
result: BurstDetectionResult | null;
@@ -46,54 +44,33 @@ interface MetricCardProps {
tone: "blue" | "orange" | "purple" | "green";
}
const toneStyles: Record<
MetricCardProps["tone"],
{ bg: string; border: string; text: string; darkText: string }
> = {
blue: {
bg: "from-blue-50 to-blue-100",
border: "border-blue-200",
text: "text-blue-700",
darkText: "text-blue-900",
},
orange: {
bg: "from-orange-50 to-orange-100",
border: "border-orange-200",
text: "text-orange-700",
darkText: "text-orange-900",
},
purple: {
bg: "from-purple-50 to-purple-100",
border: "border-purple-200",
text: "text-purple-700",
darkText: "text-purple-900",
},
green: {
bg: "from-green-50 to-green-100",
border: "border-green-200",
text: "text-green-700",
darkText: "text-green-900",
},
const toneStyles: Record<MetricCardProps["tone"], string> = {
blue: "border-blue-200 from-blue-50 to-blue-100 text-blue-900",
orange: "border-orange-200 from-orange-50 to-orange-100 text-orange-900",
purple: "border-purple-200 from-purple-50 to-purple-100 text-purple-900",
green: "border-green-200 from-green-50 to-green-100 text-green-900",
};
const MetricCard = ({ label, value, hint, tone }: MetricCardProps) => {
const style = toneStyles[tone];
return (
<Box className={`rounded-lg border bg-gradient-to-br p-3 shadow-sm ${style.bg} ${style.border}`}>
<Typography variant="caption" className={`mb-1 block text-xs font-semibold uppercase tracking-wide ${style.text}`}>
{label}
const MetricCard = ({ label, value, hint, tone }: MetricCardProps) => (
<Box
className={`rounded-lg border bg-gradient-to-br p-3 shadow-sm ${toneStyles[tone]}`}
>
<Typography variant="caption" className="mb-1 block font-semibold">
{label}
</Typography>
<Typography variant="body2" className="font-bold">
{value}
</Typography>
{hint ? (
<Typography variant="caption" className="mt-0.5 block opacity-75">
{hint}
</Typography>
<Typography variant="body2" className={`font-bold ${style.darkText}`}>
{value}
</Typography>
{hint ? (
<Typography variant="caption" className={`mt-0.5 block text-xs opacity-80 ${style.text}`}>
{hint}
</Typography>
) : null}
</Box>
);
};
) : null}
</Box>
);
const formatDateTime = (value?: string) =>
value ? dayjs(value).format("YYYY-MM-DD HH:mm") : "-";
const EmptyState = () => (
<Box className="flex h-full flex-col items-center justify-center bg-gray-50/50 p-6 text-center">
@@ -104,42 +81,27 @@ const EmptyState = () => (
</Typography>
<Typography variant="body2" className="max-w-xs text-gray-500">
14
</Typography>
</Box>
);
const getScoreLevel = (score: number) => {
if (score <= -0.6) return { label: "高风险", color: "error" as const };
if (score <= -0.2) return { label: "需关注", color: "warning" as const };
return { label: "正常", color: "success" as const };
};
const formatDateTime = (value?: string) => (value ? dayjs(value).format("YYYY-MM-DD HH:mm") : "-");
const DetectionResults: React.FC<Props> = ({
result,
state,
onStateChange,
}) => {
const DetectionResults: React.FC<Props> = ({ result, state, onStateChange }) => {
const map = useMap();
const highlightLayerRef = useRef<VectorLayer<VectorSource> | null>(null);
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
const [internalResultsState, setInternalResultsState] =
useState<BurstDetectionResultsState>(createBurstDetectionResultsState);
const resultsState = state ?? internalResultsState;
const selectedDay = resultsState.selectedDay;
const setSelectedDay = (value: number | null) => {
const nextState = { ...resultsState, selectedDay: value };
if (state === undefined) {
setInternalResultsState(nextState);
}
const [internalState, setInternalState] = useState<BurstDetectionResultsState>(
createBurstDetectionResultsState,
);
const resultsState = state ?? internalState;
const setSelectedDay = (selectedDay: number | null) => {
const nextState = { selectedDay };
if (state === undefined) setInternalState(nextState);
onStateChange?.(nextState);
};
useEffect(() => {
if (!map) return;
const layer = new VectorLayer({
source: new VectorSource(),
style: new Style({
@@ -157,10 +119,8 @@ const DetectionResults: React.FC<Props> = ({
queryable: false,
},
});
map.addLayer(layer);
highlightLayerRef.current = layer;
return () => {
highlightLayerRef.current = null;
map.removeLayer(layer);
@@ -174,58 +134,67 @@ const DetectionResults: React.FC<Props> = ({
highlightFeatures.forEach((feature) => source.addFeature(feature));
}, [highlightFeatures]);
const defaultSelectedDay = useMemo(
() =>
result?.summary?.most_anomalous_day ??
result?.summary?.latest_day?.Day ??
result?.rows[0]?.Day ??
null,
const sortedRows = useMemo(
() => [...(result?.rows ?? [])].sort((a, b) => a.Day - b.Day),
[result],
);
const activeSelectedDay = selectedDay ?? defaultSelectedDay;
const timestampForRow = (row: BurstDetectionRow) => {
if (row.Timestamp) return row.Timestamp;
const start = dayjs(result?.scada_window?.start);
return start.isValid() ? start.add(row.Day, "day").toISOString() : undefined;
};
const selectedRow = useMemo<BurstDetectionRow | null>(() => {
if (!result || activeSelectedDay === null) return null;
return result.rows.find((row) => row.Day === activeSelectedDay) ?? null;
}, [activeSelectedDay, result]);
const scoreSeries = useMemo(
() =>
result?.rows.map((row) => ({
value: [row.Day, Number(row.Score.toFixed(4))],
const scoreThreshold = result?.summary.score_threshold ?? 0;
const scoreSeries = sortedRows.map((row) => ({
day: row.Day,
value: [
timestampForRow(row)
? dayjs(timestampForRow(row)).format("MM-DD HH:mm")
: `${row.Day}`,
Number(row.Score.toFixed(4)),
],
itemStyle: {
color: row.IsBurst ? "#ef4444" : row.Score <= -0.2 ? "#f59e0b" : "#10b981",
color:
row.Role === "target"
? row.IsBurst
? "#ef4444"
: "#2563eb"
: "#94a3b8",
},
})) ?? [],
[result],
);
symbolSize: row.Role === "target" ? 11 : 7,
}));
const rankingSeries = useMemo(
() =>
[...(result?.summary?.latest_sensor_rankings ?? [])]
.sort((a, b) => a.latest_high_frequency_value - b.latest_high_frequency_value)
[...(result?.summary.latest_sensor_rankings ?? [])]
.sort(
(a, b) =>
(a.standardized_deviation ?? a.latest_high_frequency_value) -
(b.standardized_deviation ?? b.latest_high_frequency_value),
)
.map((item) => ({
name: item.sensor_node,
value: Number(item.latest_high_frequency_value.toFixed(4)),
value: Number(
(item.standardized_deviation ?? item.latest_high_frequency_value).toFixed(3),
),
})),
[result],
);
const locateSensors = async (sensorIds: string[]) => {
if (!map || sensorIds.length === 0) return;
let features = await queryFeaturesByIds(sensorIds, "geo_junctions_mat");
if (features.length === 0) {
features = await queryFeaturesByIds(sensorIds, "geo_junctions");
}
if (features.length === 0) return;
setHighlightFeatures(features);
const geojsonFormat = new GeoJSON();
const geojsonFeatures = features.map((feature) => geojsonFormat.writeFeatureObject(feature));
// @ts-ignore turf typing with ol geojson objects
const format = new GeoJSON();
const geojsonFeatures = features.map((feature) =>
format.writeFeatureObject(feature),
);
// @ts-ignore turf accepts OpenLayers GeoJSON feature objects
const extent = bbox(featureCollection(geojsonFeatures));
map.getView().fit(extent, {
maxZoom: 18,
@@ -234,60 +203,50 @@ const DetectionResults: React.FC<Props> = ({
});
};
if (!result) {
return <EmptyState />;
}
if (!result) return <EmptyState />;
const latestDay = result.summary?.latest_day;
const latestLevel = latestDay ? getScoreLevel(latestDay.Score) : getScoreLevel(0);
const mostAnomalousRow = result.rows.find((row) => row.Day === result.summary?.most_anomalous_day) ?? null;
const mostAnomalousLevel = getScoreLevel(mostAnomalousRow?.Score ?? 0);
const targetRow = sortedRows.find((row) => row.Role === "target") ?? sortedRows.at(-1);
const isBurstDetected = result.summary.burst_detected;
const targetRank = result.summary.target_rank;
const excludedCount = result.data_quality?.excluded_sensors.length ?? 0;
const chartOption = {
tooltip: {
trigger: "axis",
formatter: (params: Array<{ data: { value: [number, number] } }>) => {
const point = params[0]?.data?.value;
if (!point) return "-";
return `侦测日第 ${point[0]}<br/>异常分数:${point[1]}`;
formatter: (params: Array<{ data: { day: number; value: [string, number] } }>) => {
const data = params[0]?.data;
return data
? `${data.value[0]}<br/>${data.day === 15 ? "目标时刻" : "参考日"}<br/>异常分数:${data.value[1]}`
: "-";
},
},
grid: { top: 30, left: 40, right: 20, bottom: 35 },
grid: { top: 30, left: 48, right: 20, bottom: 48 },
xAxis: {
type: "category",
name: "侦测日",
data: result.rows.map((row) => row.Day),
axisLabel: { fontSize: 10 },
},
yAxis: {
type: "value",
name: "异常分数",
axisLabel: { fontSize: 10 },
name: "同刻日期",
boundaryGap: false,
data: scoreSeries.map((item) => item.value[0]),
axisLabel: { fontSize: 10, interval: 2, rotate: 25 },
},
yAxis: { type: "value", name: "异常分数", axisLabel: { fontSize: 10 } },
series: [
{
type: "line",
smooth: true,
symbolSize: 8,
data: scoreSeries,
lineStyle: { color: "#2563eb", width: 2 },
lineStyle: { color: "#94a3b8", width: 2 },
markLine: {
symbol: "none",
lineStyle: { type: "dashed", color: "#94a3b8" },
data: [{ yAxis: 0 }],
lineStyle: { type: "dashed", color: "#ef4444" },
data: [{ yAxis: scoreThreshold, name: "报警阈值" }],
},
},
],
};
const rankingOption = {
tooltip: {
trigger: "axis",
axisPointer: { type: "shadow" },
},
grid: { top: 20, left: 70, right: 20, bottom: 20 },
xAxis: { type: "value", axisLabel: { fontSize: 10 } },
tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
grid: { top: 12, left: 82, right: 20, bottom: 25 },
xAxis: { type: "value", name: "标准化偏离", axisLabel: { fontSize: 10 } },
yAxis: {
type: "category",
data: rankingSeries.map((item) => item.name),
@@ -298,9 +257,7 @@ const DetectionResults: React.FC<Props> = ({
type: "bar",
data: rankingSeries.map((item) => ({
value: item.value,
itemStyle: {
color: item.value <= -0.6 ? "#ef4444" : item.value <= -0.2 ? "#f59e0b" : "#10b981",
},
itemStyle: { color: item.value < 0 ? "#ef4444" : "#f59e0b" },
})),
barWidth: 14,
},
@@ -309,323 +266,176 @@ const DetectionResults: React.FC<Props> = ({
const columns: GridColDef[] = [
{
field: "Day",
headerName: "侦测日",
width: 96,
valueFormatter: (value?: number) => (typeof value === "number" ? `${value}` : "-"),
field: "Timestamp",
headerName: "同刻日期",
minWidth: 145,
flex: 1,
valueGetter: (_value, row) => formatDateTime(timestampForRow(row)),
},
{
field: "Role",
headerName: "角色",
width: 90,
valueFormatter: (value?: string) => (value === "target" ? "目标" : "参考"),
},
{
field: "Score",
headerName: "异常分数",
width: 120,
valueFormatter: (value?: number) => (typeof value === "number" ? value.toFixed(4) : "-"),
width: 110,
valueFormatter: (value?: number) =>
typeof value === "number" ? value.toFixed(4) : "-",
},
{
field: "IsBurst",
headerName: "判定结果",
width: 120,
renderCell: ({ value }) => {
const level = value ? { label: "爆管异常", color: "error" as const } : { label: "正常", color: "success" as const };
return <Chip size="small" label={level.label} color={level.color} variant="outlined" />;
},
headerName: "目标判定",
width: 110,
renderCell: ({ value, row }) =>
row.Role === "target" || row.Day === result.day_count ? (
<Chip
size="small"
label={value ? "爆管异常" : "正常"}
color={value ? "error" : "success"}
variant="outlined"
/>
) : (
<Typography variant="caption" color="text.secondary">
</Typography>
),
},
];
const rows = result.rows.map((row) => ({ id: row.Day, ...row }));
const tableRows = sortedRows.map((row) => ({ id: row.Day, ...row }));
return (
<Box className="h-full overflow-auto p-1">
<Box className="mb-4 space-y-3">
{/* Status Banner */}
<Box
className={`rounded-lg px-4 py-3 flex items-center gap-3 border ${isBurstDetected
? "bg-red-50 border-red-100 text-red-900"
: "bg-green-50 border-green-100 text-green-900"
}`}
className={`flex items-center gap-3 rounded-lg border px-4 py-3 ${
isBurstDetected
? "border-red-100 bg-red-50 text-red-900"
: "border-green-100 bg-green-50 text-green-900"
}`}
>
{isBurstDetected ? (
<ErrorOutlineIcon className="text-red-600" />
) : (
<CheckCircleIcon className="text-green-600" />
)}
{isBurstDetected ? <ErrorOutlineIcon /> : <CheckCircleIcon />}
<Box className="flex-1">
<Typography variant="subtitle2" className="font-bold">
{isBurstDetected
? `侦测到异常信号 (共 ${result.summary.anomaly_day_count} 天)`
: "未侦测到爆管异常"}
{isBurstDetected ? "目标时刻侦测到爆管异常" : "目标时刻未侦测到爆管异常"}
</Typography>
<Typography variant="caption" className="opacity-80">
{isBurstDetected
? "建议检查异常日期的压力波动情况"
: "当前时间窗口内数据特征平稳,符合历史模式"}
{formatDateTime(result.target_time ?? result.summary.target_time)}
</Typography>
</Box>
</Box>
{/* Header */}
<Box className="flex items-center justify-between px-1">
<Box className="flex items-center gap-2">
<Box className="h-4 w-1 rounded-full bg-blue-600" />
<Typography variant="h6" className="truncate font-bold text-gray-900" sx={{ fontSize: "1.1rem" }}>
{result.scheme_name || "爆管侦测结果"}
</Typography>
</Box>
<Box className="flex items-center gap-2">
{result.username ? (
<Chip
label={result.username}
size="small"
sx={{
height: 24,
backgroundColor: "#f3f4f6",
color: "#4b5563",
border: "none",
fontWeight: 500,
}}
/>
) : null}
<Button
size="small"
variant="outlined"
startIcon={<RoomIcon />}
onClick={() =>
locateSensors(result.summary.latest_sensor_rankings.map((item) => item.sensor_node).slice(0, 5))
}
sx={{
height: 24,
minWidth: 0,
padding: "0 8px",
borderColor: "#bfdbfe",
color: "#2563eb",
fontSize: "0.75rem",
"&:hover": { borderColor: "#60a5fa", backgroundColor: "#eff6ff" },
}}
>
</Button>
</Box>
<Box className="flex items-center justify-between gap-2 px-1">
<Typography variant="h6" className="min-w-0 flex-1 font-bold text-gray-900">
</Typography>
<Button
size="small"
variant="outlined"
startIcon={<RoomIcon />}
onClick={() =>
void locateSensors(
result.summary.latest_sensor_rankings
.slice(0, 5)
.map((item) => item.sensor_node),
)
}
sx={{ flexShrink: 0, whiteSpace: "nowrap" }}
>
</Button>
</Box>
{/* Configuration Summary */}
<Box className="flex flex-wrap items-center gap-x-4 gap-y-2 rounded-lg border border-gray-100 bg-gray-50/50 px-3 py-2 text-xs text-gray-600">
<Box className="flex items-center gap-1.5">
<Box className="h-1.5 w-1.5 rounded-full bg-blue-400" />
<span className="font-medium text-gray-700"></span>
<span className="font-mono text-gray-600">
{formatDateTime(result.scada_window?.start)} ~ {formatDateTime(result.scada_window?.end)}
</span>
</Box>
<Box className="flex items-center gap-1.5">
<Box className="h-1.5 w-1.5 rounded-full bg-purple-400" />
<span className="font-medium text-gray-700"></span>
<span className="text-gray-600">
{(() => {
const ds = result.data_source;
const os = result.observed_source;
if (ds === "simulation") return "模拟数据";
if (ds === "monitoring") return "监测数据";
if (os === "simulation_scheme_timerange") return "模拟数据";
if (os === "backend_timerange") return "监测数据";
return os || "-";
})()}
</span>
</Box>
</Box>
{/* Metrics Grid */}
<Box className="grid grid-cols-2 gap-3">
<MetricCard
label="异常数"
value={`${result.summary.anomaly_day_count} / ${result.day_count}`}
hint={`异常日:${result.summary.anomaly_days.join(", ") || "无"}`}
tone={result.summary.anomaly_day_count > 0 ? "orange" : "green"}
label="目标异常数"
value={targetRow ? targetRow.Score.toFixed(4) : "-"}
hint={`报警阈值 ≤ ${scoreThreshold.toFixed(2)}`}
tone={isBurstDetected ? "orange" : "green"}
/>
<MetricCard
label="最异常日"
value={
result.summary.burst_detected && result.summary.most_anomalous_day
? `${result.summary.most_anomalous_day}`
: "无"
}
hint={
result.summary.burst_detected && mostAnomalousRow
? `分数 ${mostAnomalousRow.Score.toFixed(4)} · ${mostAnomalousLevel.label}`
: "-"
}
label="目标异常排名"
value={targetRank ? `${targetRank} / ${result.day_count}` : "-"}
hint="在目标日与 14 个参考日中排序"
tone="purple"
/>
<MetricCard
label="最新状态"
value={latestLevel.label}
hint={latestDay ? `${latestDay.Day} 天 · 分数 ${latestDay.Score.toFixed(4)}` : "-"}
tone={latestLevel.color === "success" ? "green" : "orange"}
label="参考区间"
value={`${formatDateTime(result.reference_window?.start)} ~ ${formatDateTime(result.reference_window?.end)}`}
hint={`${result.reference_window?.day_count ?? 14} 个同刻参考日`}
tone="blue"
/>
<MetricCard
label="测点 / 样本"
value={`${result.sensor_nodes.length} / ${result.sample_count}`}
hint={`每日采样点数:${result.points_per_day}`}
label="有效 / 排除测点"
value={`${result.sensor_nodes.length} / ${excludedCount}`}
hint={`${result.sampling_interval_minutes ?? 15} 分钟采样,${result.points_per_day} 点/天`}
tone="blue"
/>
</Box>
</Box>
{/* Score Trend Chart */}
<Box className="mb-4 overflow-hidden rounded-xl border border-gray-100 bg-white shadow-sm">
<Box className="flex items-center justify-between border-b border-gray-100 bg-white px-4 py-3">
<Box className="flex items-center justify-between border-b border-gray-100 px-4 py-3">
<Box className="flex items-center gap-2">
<ShowChartIcon className="h-5 w-5 text-blue-600" />
<Typography variant="subtitle1" className="font-bold text-gray-800">
<ShowChartIcon className="text-blue-600" />
<Typography variant="subtitle1" className="font-bold">
15
</Typography>
</Box>
<Tooltip title="分数越小越异常,0 以下通常意味着更值得关注。">
<Tooltip title="灰色点为前 14 天参考,最后一个点为本次目标。">
<InfoOutlinedIcon fontSize="small" className="text-gray-400" />
</Tooltip>
</Box>
<Box sx={{ height: 250, px: 1.5, py: 1 }}>
<Box sx={{ height: 270, px: 1.5, py: 1 }}>
<ReactECharts
option={chartOption}
style={{ height: "100%", width: "100%" }}
onEvents={{
click: (params: { data?: { value?: [number, number] } }) => {
const day = params?.data?.value?.[0];
if (typeof day === "number") {
setSelectedDay(day);
}
},
click: (params: { data?: { day?: number } }) =>
setSelectedDay(params.data?.day ?? null),
}}
/>
</Box>
</Box>
{/* Selected Day Interpretation */}
{/* <Box className="mb-4 overflow-hidden rounded-xl border border-gray-100 bg-white shadow-sm">
<Box className="flex items-center justify-between border-b border-gray-100 bg-white px-4 py-3">
<Typography variant="subtitle1" className="font-bold text-gray-800">
选中日解读
</Typography>
{selectedRow ? (
<Chip
size="small"
label={`第 ${selectedRow.Day} 天`}
sx={{
height: 22,
backgroundColor: "rgba(37, 99, 235, 0.08)",
color: "#2563eb",
fontWeight: 600,
fontSize: "0.75rem",
border: "none",
}}
/>
) : null}
</Box>
{selectedRow ? (
<Box className="space-y-3 px-4 py-3">
<Box className="flex items-center gap-2">
<Chip
label={getScoreLevel(selectedRow.Score).label}
color={getScoreLevel(selectedRow.Score).color}
variant="filled"
/>
</Box>
<Typography variant="body2" className="text-gray-700">
异常分数:<span className="font-semibold">{selectedRow.Score.toFixed(4)}</span>
{rankingSeries.length > 0 ? (
<Box className="mb-4 overflow-hidden rounded-xl border border-gray-100 bg-white shadow-sm">
<Box className="flex items-center justify-between border-b border-gray-100 px-4 py-3">
<Typography variant="subtitle1" className="font-bold">
</Typography>
<Typography variant="body2" className="text-gray-700">
模型判定:{selectedRow.IsBurst ? "异常日(Prediction = -1" : "正常日(Prediction = 1"}
</Typography>
<Typography variant="body2" className="text-gray-700">
解读建议:
{selectedRow.Score <= -0.6
? "高风险异常,建议优先复核对应测点的原始压力曲线与现场工况。"
: selectedRow.Score <= -0.2
? "存在可疑波动,建议结合相邻测点和调度记录进一步确认。"
: "未见明显异常,可作为基线日参考。"}
<Typography variant="caption" color="text.secondary">
</Typography>
</Box>
) : (
<Typography variant="body2" className="px-4 py-3 text-gray-500">
请在趋势图或表格中选择一天查看详细解释。
</Typography>
)}
</Box> */}
<Box sx={{ height: 280, px: 1.5, py: 1 }}>
<ReactECharts option={rankingOption} style={{ height: "100%", width: "100%" }} />
</Box>
</Box>
) : null}
{/* Latest Sensor Rankings */}
{/* <Box className="mb-4 overflow-hidden rounded-xl border border-gray-100 bg-white shadow-sm">
<Box className="flex items-center justify-between border-b border-gray-100 bg-white px-4 py-3">
<Typography variant="subtitle1" className="font-bold text-gray-800">
最新测点高频特征排名
</Typography>
<Typography variant="caption" className="text-gray-500">
仅展示最新一天
</Typography>
</Box>
<Box sx={{ height: 260, px: 1.5, py: 1 }}>
<ReactECharts option={rankingOption} style={{ height: "100%", width: "100%" }} />
</Box>
<Box className="flex flex-wrap gap-2 border-t border-gray-100 px-4 py-3">
{result.summary.latest_sensor_rankings.slice(0, 5).map((item) => (
<Button
key={item.sensor_node}
size="small"
variant="outlined"
onClick={() => locateSensors([item.sensor_node])}
sx={{
borderColor: "#bfdbfe",
color: "#2563eb",
"&:hover": { borderColor: "#60a5fa", backgroundColor: "#eff6ff" },
}}
>
{item.sensor_node}
</Button>
))}
</Box>
</Box> */}
{/* Results Table */}
<Box className="mb-4 overflow-hidden rounded-xl border border-gray-100 bg-white shadow-sm">
<Box className="flex items-center justify-between border-b border-gray-100 bg-white px-4 py-3">
<Box className="flex items-center gap-2">
<FormatListBulleted className="h-5 w-5 text-blue-600" />
<Typography variant="subtitle1" className="font-bold text-gray-800">
</Typography>
</Box>
<Chip
size="small"
label={`${rows.length}`}
sx={{
height: 22,
backgroundColor: "rgba(37, 99, 235, 0.08)",
color: "#2563eb",
fontWeight: 600,
fontSize: "0.75rem",
border: "none",
}}
/>
<Box className="flex items-center gap-2 border-b border-gray-100 px-4 py-3">
<FormatListBulleted className="text-blue-600" />
<Typography variant="subtitle1" className="font-bold">
</Typography>
</Box>
<Box sx={{ height: 320, px: 1, py: 1 }}>
<Box sx={{ height: 360, px: 1, py: 1 }}>
<DataGrid
rows={rows}
rows={tableRows}
columns={columns}
columnBufferPx={100}
localeText={zhCN.components.MuiDataGrid.defaultProps.localeText}
initialState={{
pagination: { paginationModel: { pageSize: 50, page: 0 } },
}}
pageSizeOptions={[50]}
hideFooterSelectedRowCount
sx={{
border: "none",
"& .MuiDataGrid-cell": { borderColor: "#f0f0f0" },
"& .MuiDataGrid-columnHeaders": { backgroundColor: "#fafafa" },
"& .MuiDataGrid-row:hover": { backgroundColor: "#f8fafc" },
// Hide the rows per page selector since it's fixed to 50
"& .MuiTablePagination-selectLabel": { display: "none" },
"& .MuiTablePagination-input": { display: "none" },
}}
pageSizeOptions={[15]}
initialState={{ pagination: { paginationModel: { pageSize: 15, page: 0 } } }}
disableRowSelectionOnClick
onRowClick={(params) => setSelectedDay(Number(params.row.Day))}
getRowClassName={(params) =>
params.row.Day === resultsState.selectedDay ? "bg-blue-50" : ""
}
sx={{ border: "none" }}
/>
</Box>
</Box>