diff --git a/src/app/(main)/health-risk-analysis/page.tsx b/src/app/(main)/health-risk-analysis/page.tsx index 71d565e..a42c5f2 100644 --- a/src/app/(main)/health-risk-analysis/page.tsx +++ b/src/app/(main)/health-risk-analysis/page.tsx @@ -1,16 +1,21 @@ "use client"; import MapComponent from "@app/OlMap/MapComponent"; -import Timeline from "@app/OlMap/Controls/Timeline_health_risk_analysis"; +import Timeline from "@components/olmap/HealthRiskAnalysis/Timeline"; import MapToolbar from "@app/OlMap/Controls/Toolbar"; +import { HealthRiskProvider } from "@components/olmap/HealthRiskAnalysis/HealthRiskContext"; +import HealthRiskPieChart from "@components/olmap/HealthRiskAnalysis/HealthRiskPieChart"; export default function Home() { return (
- - - - + + + + + + +
); } diff --git a/src/components/olmap/HealthRiskAnalysis/HealthRiskContext.tsx b/src/components/olmap/HealthRiskAnalysis/HealthRiskContext.tsx new file mode 100644 index 0000000..c44d021 --- /dev/null +++ b/src/components/olmap/HealthRiskAnalysis/HealthRiskContext.tsx @@ -0,0 +1,39 @@ +"use client"; + +import React, { createContext, useContext, useState, ReactNode, Dispatch, SetStateAction } from "react"; +import { PredictionResult } from "./types"; + +interface HealthRiskContextType { + predictionResults: PredictionResult[]; + setPredictionResults: Dispatch>; + currentYear: number; + setCurrentYear: Dispatch>; +} + +const HealthRiskContext = createContext(undefined); + +export const HealthRiskProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [predictionResults, setPredictionResults] = useState([]); + const [currentYear, setCurrentYear] = useState(4); + + return ( + + {children} + + ); +}; + +export const useHealthRisk = () => { + const context = useContext(HealthRiskContext); + if (context === undefined) { + throw new Error("useHealthRisk must be used within a HealthRiskProvider"); + } + return context; +}; diff --git a/src/components/olmap/HealthRiskAnalysis/HealthRiskPieChart.tsx b/src/components/olmap/HealthRiskAnalysis/HealthRiskPieChart.tsx new file mode 100644 index 0000000..8547ede --- /dev/null +++ b/src/components/olmap/HealthRiskAnalysis/HealthRiskPieChart.tsx @@ -0,0 +1,201 @@ +"use client"; + +import React, { useMemo } from "react"; +import ReactECharts from "echarts-for-react"; +import { Paper, Typography, Box, Chip, IconButton, Tooltip, Stack } from "@mui/material"; +import { ChevronLeft, ChevronRight, PieChart } from "@mui/icons-material"; +import { RAINBOW_COLORS, RISK_BREAKS, RISK_LABELS } from "./types"; +import { useHealthRisk } from "./HealthRiskContext"; + +const HealthRiskPieChart: React.FC = () => { + const { predictionResults, currentYear } = useHealthRisk(); + const [isExpanded, setIsExpanded] = React.useState(true); + const [hoveredYearIndex, setHoveredYearIndex] = React.useState( + null + ); + + const datasetSource = useMemo(() => { + if (!predictionResults || predictionResults.length === 0) return []; + + const years = Array.from({ length: 70 }, (_, i) => 4 + i); // 4 to 73 + const header = ["Risk Level", ...years.map(String)]; + + const rows = RISK_LABELS.map((label, riskIdx) => { + const row: (string | number)[] = [label]; + years.forEach((year) => { + let count = 0; + predictionResults.forEach((result) => { + const { y } = result.survival_function; + const index = year - 4; + if (index >= 0 && index < y.length) { + const probability = y[index]; + if ( + probability >= RISK_BREAKS[riskIdx] && + probability < RISK_BREAKS[riskIdx + 1] + ) { + count++; + } else if (riskIdx === 9 && probability === 1.0) { + count++; + } + } + }); + row.push(count); + }); + return row; + }); + + return [header, ...rows]; + }, [predictionResults]); + + const displayDimension = useMemo(() => { + if (hoveredYearIndex !== null) { + return hoveredYearIndex + 1; + } + // 默认显示当前时间轴年份 + return Math.max(1, Math.min(70, currentYear - 3)); + }, [hoveredYearIndex, currentYear]); + + const option = { + legend: { + top: "48%", + left: "center", + itemWidth: 10, + itemHeight: 10, + textStyle: { + fontSize: 10, + }, + }, + tooltip: { + trigger: "axis", + showContent: true, + confine: true, + }, + dataset: { + source: datasetSource, + }, + xAxis: { + type: "category", + axisLabel: { + fontSize: 10, + }, + }, + yAxis: { + gridIndex: 0, + axisLabel: { + fontSize: 10, + }, + }, + grid: { + top: "62%", + bottom: "8%", + left: "12%", + right: "5%", + }, + series: [ + ...RISK_LABELS.map((_, i) => ({ + type: "line", + smooth: true, + seriesLayoutBy: "row", + emphasis: { focus: "series" }, + itemStyle: { color: RAINBOW_COLORS[i] }, + symbol: "none", + })), + { + type: "pie", + id: "pie", + radius: "35%", + center: ["50%", "24%"], + emphasis: { + focus: "self", + }, + label: { + formatter: "{b}: {@[" + displayDimension + "]} ({d}%)", + fontSize: 10, + }, + encode: { + itemName: "Risk Level", + value: displayDimension, + tooltip: displayDimension, + }, + }, + ], + }; + + const onEvents = { + updateAxisPointer: (event: any) => { + const xAxisInfo = event.axesInfo[0]; + if (xAxisInfo) { + setHoveredYearIndex(xAxisInfo.value); + } + }, + finished: () => { + // 可以在这里处理一些渲染完成后的逻辑 + }, + }; + + const chartRef = React.useRef(null); + + // 监听鼠标离开图表容器,恢复到当前年份 + const handleMouseLeave = () => { + setHoveredYearIndex(null); + }; + + if (!predictionResults || predictionResults.length === 0) { + return null; + } + + return ( +
+ {/* 头部 */} +
+
+ + + + +

管道健康风险分布

+ +
+
+ + {/* 内容区域 */} +
+ { + chartRef.current = e; + }} + option={option} + onEvents={onEvents} + style={{ height: "100%", width: "100%" }} + opts={{ renderer: "canvas" }} + /> +
+
+ ); +}; + +export default HealthRiskPieChart; diff --git a/src/app/OlMap/Controls/Timeline_health_risk_analysis.tsx b/src/components/olmap/HealthRiskAnalysis/Timeline.tsx similarity index 93% rename from src/app/OlMap/Controls/Timeline_health_risk_analysis.tsx rename to src/components/olmap/HealthRiskAnalysis/Timeline.tsx index 40c7367..0751fa8 100644 --- a/src/app/OlMap/Controls/Timeline_health_risk_analysis.tsx +++ b/src/components/olmap/HealthRiskAnalysis/Timeline.tsx @@ -27,41 +27,19 @@ import dayjs from "dayjs"; import { PlayArrow, Pause, Stop, Refresh } from "@mui/icons-material"; import { TbArrowBackUp, TbArrowForwardUp } from "react-icons/tb"; import { FiSkipBack, FiSkipForward } from "react-icons/fi"; -import { useData } from "../MapComponent"; +import { useData } from "../../../app/OlMap/MapComponent"; import { config, NETWORK_NAME } from "@/config/config"; -import { useMap } from "../MapComponent"; +import { useMap } from "../../../app/OlMap/MapComponent"; +import { useHealthRisk } from "./HealthRiskContext"; +import { + PredictionResult, + SurvivalFunction, + RAINBOW_COLORS, + RISK_BREAKS, +} from "./types"; + const backendUrl = config.BACKEND_URL; -// 预测结果数据类型 -interface SurvivalFunction { - x: number[]; // 时间点(年) - y: number[]; // 生存概率 - a: number; - b: number; -} - -interface PredictionResult { - link_id: string; - diameter: number; - velocity: number; - pressure: number; - survival_function: SurvivalFunction; -} - -// 彩虹色配置 -const RAINBOW_COLORS = [ - "rgba(142, 68, 173, 0.9)", // 紫 - "rgba(63, 81, 181, 0.9)", // 靛青 - "rgba(33, 150, 243, 0.9)", // 天蓝 - "rgba(0, 188, 212, 0.9)", // 青色 - "rgba(0, 158, 115, 0.9)", // 青绿 - "rgba(76, 175, 80, 0.9)", // 中绿 - "rgba(199, 224, 0, 0.9)", // 黄绿 - "rgba(255, 215, 0, 0.9)", // 金黄 - "rgba(255, 127, 0, 0.9)", // 橙 - "rgba(255, 0, 0, 0.9)", // 红 -]; - interface TimelineProps { schemeDate?: Date; timeRange?: { start: Date; end: Date }; @@ -77,14 +55,17 @@ const Timeline: React.FC = ({ return
Loading...
; // 或其他占位符 } const { open } = useNotification(); + const { + predictionResults, + setPredictionResults, + currentYear, + setCurrentYear, + } = useHealthRisk(); + const [selectedDateTime, setSelectedDateTime] = useState(new Date()); - const [currentYear, setCurrentYear] = useState(4); // 时间轴当前值 (4-73) const [isPlaying, setIsPlaying] = useState(false); const [playInterval, setPlayInterval] = useState(15000); // 毫秒 const [isPredicting, setIsPredicting] = useState(false); - const [predictionResults, setPredictionResults] = useState< - PredictionResult[] - >([]); const [pipeLayer, setPipeLayer] = useState(null); // 使用 ref 存储当前的健康数据,供事件监听器读取,避免重复绑定 @@ -349,7 +330,7 @@ const Timeline: React.FC = ({ predictionResults.forEach((result) => { const probability = getSurvivalProbabilityAtYear( result.survival_function, - currentYear - 1 // 使用索引 (0-based) + currentYear - 4 // 使用索引 (0-based) ); pipeHealthData.set(result.link_id, probability); }); @@ -370,7 +351,7 @@ const Timeline: React.FC = ({ if (probabilities.length === 0) return; // 使用等距分段,从0-1分为十类 - const breaks = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]; + const breaks = RISK_BREAKS; // 生成彩虹色(从紫色到红色,低生存概率=高风险=红色) const colors = RAINBOW_COLORS; @@ -658,12 +639,7 @@ const Timeline: React.FC = ({ color: "primary.main", }} > - 预测年份: - {predictionResults.length > 0 && - predictionResults[0].survival_function.x[currentYear - 1] !== - undefined - ? predictionResults[0].survival_function.x[currentYear - 1] - : currentYear} + 预测年份:{currentYear} diff --git a/src/components/olmap/HealthRiskAnalysis/types.ts b/src/components/olmap/HealthRiskAnalysis/types.ts new file mode 100644 index 0000000..6a3f58b --- /dev/null +++ b/src/components/olmap/HealthRiskAnalysis/types.ts @@ -0,0 +1,42 @@ +export interface SurvivalFunction { + x: number[]; // 时间点(年) + y: number[]; // 生存概率 + a: number; + b: number; +} + +export interface PredictionResult { + link_id: string; + diameter: number; + velocity: number; + pressure: number; + survival_function: SurvivalFunction; +} + +export const RAINBOW_COLORS = [ + "rgba(255, 0, 0, 0.9)", // 红 (0.0 - 0.1) - 高风险 + "rgba(255, 127, 0, 0.9)", // 橙 + "rgba(255, 215, 0, 0.9)", // 金黄 + "rgba(199, 224, 0, 0.9)", // 黄绿 + "rgba(76, 175, 80, 0.9)", // 中绿 + "rgba(0, 158, 115, 0.9)", // 青绿 + "rgba(0, 188, 212, 0.9)", // 青色 + "rgba(33, 150, 243, 0.9)", // 天蓝 + "rgba(63, 81, 181, 0.9)", // 靛青 + "rgba(142, 68, 173, 0.9)", // 紫 (0.9 - 1.0) - 低风险 +]; + +export const RISK_BREAKS = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]; + +export const RISK_LABELS = [ + "0.0 - 0.1 (极高风险)", + "0.1 - 0.2", + "0.2 - 0.3", + "0.3 - 0.4", + "0.4 - 0.5", + "0.5 - 0.6", + "0.6 - 0.7", + "0.7 - 0.8", + "0.8 - 0.9", + "0.9 - 1.0 (极低风险)", +];