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 (极低风险)",
+];