238 lines
6.1 KiB
TypeScript
238 lines
6.1 KiB
TypeScript
"use client";
|
|
|
|
import React, { useMemo, useRef } from "react";
|
|
import Draggable from "react-draggable";
|
|
|
|
import { Box, Chip, Stack, Typography } from "@mui/material";
|
|
import { ShowChart } from "@mui/icons-material";
|
|
import ReactECharts from "echarts-for-react";
|
|
import "dayjs/locale/zh-cn";
|
|
import { useHealthRisk } from "./HealthRiskContext";
|
|
|
|
export interface PredictDataPanelProps {
|
|
/** 选中的要素信息列表,格式为 [[id, type], [id, type]] */
|
|
featureInfos: [string, string][];
|
|
/** Y 轴数值的小数位数 */
|
|
fractionDigits?: number;
|
|
}
|
|
|
|
const PredictDataPanel: React.FC<PredictDataPanelProps> = ({
|
|
featureInfos,
|
|
fractionDigits = 4,
|
|
}) => {
|
|
const { predictionResults } = useHealthRisk();
|
|
const draggableRef = useRef<HTMLDivElement>(null);
|
|
|
|
// 提取选中的设备 ID
|
|
const selectedIds = useMemo(
|
|
() => featureInfos.map(([id]) => id),
|
|
[featureInfos]
|
|
);
|
|
|
|
// 过滤出选中管道的预测结果
|
|
const filteredResults = useMemo(() => {
|
|
return predictionResults.filter((res) => selectedIds.includes(res.link_id));
|
|
}, [predictionResults, selectedIds]);
|
|
|
|
const hasData = filteredResults.length > 0;
|
|
|
|
// 构建图表所需的数据集
|
|
const dataset = useMemo(() => {
|
|
if (filteredResults.length === 0) return [];
|
|
|
|
// 获取所有唯一的时间点并排序
|
|
const allX = Array.from(
|
|
new Set(filteredResults.flatMap((res) => res.survival_function.x))
|
|
).sort((a, b) => a - b);
|
|
|
|
return allX.map((x) => {
|
|
const row: any = { x, label: `${x}年` };
|
|
filteredResults.forEach((res) => {
|
|
const index = res.survival_function.x.indexOf(x);
|
|
if (index !== -1) {
|
|
row[res.link_id] = res.survival_function.y[index];
|
|
}
|
|
});
|
|
return row;
|
|
});
|
|
}, [filteredResults]);
|
|
|
|
const renderEmpty = () => (
|
|
<Box
|
|
sx={{
|
|
flex: 1,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
py: 8,
|
|
color: "text.secondary",
|
|
height: "100%",
|
|
}}
|
|
>
|
|
<ShowChart sx={{ fontSize: 64, mb: 2, opacity: 0.3 }} />
|
|
<Typography variant="h6" gutterBottom sx={{ fontWeight: 500 }}>
|
|
暂无预测数据
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
请在地图上选择已分析的管道
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
|
|
const renderChart = () => {
|
|
if (!hasData) return renderEmpty();
|
|
|
|
const colors = [
|
|
"#1976d2",
|
|
"#dc004e",
|
|
"#ff9800",
|
|
"#4caf50",
|
|
"#9c27b0",
|
|
"#00bcd4",
|
|
"#f44336",
|
|
"#8bc34a",
|
|
"#ff5722",
|
|
"#3f51b5",
|
|
];
|
|
|
|
const xData = dataset.map((item) => item.x);
|
|
|
|
const series = filteredResults.map((res, index) => ({
|
|
name: `管道 ${res.link_id}`,
|
|
type: "line",
|
|
smooth: true,
|
|
symbol: "circle",
|
|
symbolSize: 6,
|
|
itemStyle: {
|
|
color: colors[index % colors.length],
|
|
},
|
|
data: res.survival_function.y,
|
|
}));
|
|
|
|
const option = {
|
|
tooltip: {
|
|
trigger: "axis",
|
|
formatter: (params: any) => {
|
|
let res = `${params[0].name}年<br/>`;
|
|
params.forEach((item: any) => {
|
|
res += `${item.marker} ${item.seriesName}: ${item.value.toFixed(
|
|
fractionDigits
|
|
)}<br/>`;
|
|
});
|
|
return res;
|
|
},
|
|
},
|
|
legend: {
|
|
top: "top",
|
|
type: "scroll",
|
|
},
|
|
grid: {
|
|
left: "5%",
|
|
right: "5%",
|
|
bottom: "10%",
|
|
containLabel: true,
|
|
},
|
|
xAxis: {
|
|
type: "category",
|
|
name: "年",
|
|
boundaryGap: false,
|
|
data: xData,
|
|
},
|
|
yAxis: {
|
|
type: "value",
|
|
name: "生存概率",
|
|
min: 0,
|
|
max: 1,
|
|
},
|
|
series,
|
|
};
|
|
|
|
return (
|
|
<Box sx={{ width: "100%", height: "100%" }}>
|
|
<ReactECharts
|
|
option={option}
|
|
style={{ height: "100%", width: "100%" }}
|
|
notMerge={true}
|
|
/>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Draggable nodeRef={draggableRef}>
|
|
<Box
|
|
ref={draggableRef}
|
|
sx={{
|
|
position: "absolute",
|
|
right: "2rem",
|
|
top: "2rem",
|
|
width: "min(920px, calc(100vw - 2rem))",
|
|
maxWidth: "100vw",
|
|
height: "40vh",
|
|
maxHeight: "calc(100vh - 2rem)",
|
|
boxSizing: "border-box",
|
|
borderRadius: "12px",
|
|
boxShadow:
|
|
"0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)",
|
|
backdropFilter: "blur(8px)",
|
|
opacity: 0.95,
|
|
transition: "opacity 0.3s ease-in-out",
|
|
border: "none",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
zIndex: 1300,
|
|
backgroundColor: "white",
|
|
overflow: "hidden",
|
|
"&:hover": {
|
|
opacity: 1,
|
|
},
|
|
}}
|
|
>
|
|
<Box
|
|
className="flex flex-col h-full rounded-xl"
|
|
sx={{ height: "100%", width: "100%" }}
|
|
>
|
|
{/* Header */}
|
|
<Box
|
|
sx={{
|
|
p: 2,
|
|
borderBottom: 1,
|
|
borderColor: "divider",
|
|
backgroundColor: "primary.main",
|
|
color: "primary.contrastText",
|
|
}}
|
|
>
|
|
<Stack
|
|
direction="row"
|
|
alignItems="center"
|
|
justifyContent="space-between"
|
|
>
|
|
<Stack direction="row" spacing={1} alignItems="center">
|
|
<ShowChart fontSize="small" />
|
|
<Typography variant="h6" sx={{ fontWeight: "bold" }}>
|
|
健康预测曲线
|
|
</Typography>
|
|
<Chip
|
|
size="small"
|
|
label={`${filteredResults.length}`}
|
|
sx={{
|
|
backgroundColor: "rgba(255,255,255,0.2)",
|
|
color: "primary.contrastText",
|
|
fontWeight: "bold",
|
|
}}
|
|
/>
|
|
</Stack>
|
|
</Stack>
|
|
</Box>
|
|
|
|
{/* Content */}
|
|
<Box sx={{ flex: 1, p: 2, overflow: "hidden" }}>{renderChart()}</Box>
|
|
</Box>
|
|
</Box>
|
|
</Draggable>
|
|
);
|
|
};
|
|
|
|
export default PredictDataPanel;
|