新增统计饼图显示;调整组件结构

This commit is contained in:
JIANG
2025-12-18 16:54:07 +08:00
parent 5cc7275186
commit 9134dff67c
5 changed files with 312 additions and 49 deletions

View File

@@ -1,16 +1,21 @@
"use client"; "use client";
import MapComponent from "@app/OlMap/MapComponent"; 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 MapToolbar from "@app/OlMap/Controls/Toolbar";
import { HealthRiskProvider } from "@components/olmap/HealthRiskAnalysis/HealthRiskContext";
import HealthRiskPieChart from "@components/olmap/HealthRiskAnalysis/HealthRiskPieChart";
export default function Home() { export default function Home() {
return ( return (
<div className="relative w-full h-full overflow-hidden"> <div className="relative w-full h-full overflow-hidden">
<MapComponent> <HealthRiskProvider>
<MapToolbar queryType="realtime" /> <MapComponent>
<Timeline /> <MapToolbar queryType="realtime" />
</MapComponent> <Timeline />
<HealthRiskPieChart />
</MapComponent>
</HealthRiskProvider>
</div> </div>
); );
} }

View File

@@ -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<SetStateAction<PredictionResult[]>>;
currentYear: number;
setCurrentYear: Dispatch<SetStateAction<number>>;
}
const HealthRiskContext = createContext<HealthRiskContextType | undefined>(undefined);
export const HealthRiskProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [predictionResults, setPredictionResults] = useState<PredictionResult[]>([]);
const [currentYear, setCurrentYear] = useState<number>(4);
return (
<HealthRiskContext.Provider
value={{
predictionResults,
setPredictionResults,
currentYear,
setCurrentYear,
}}
>
{children}
</HealthRiskContext.Provider>
);
};
export const useHealthRisk = () => {
const context = useContext(HealthRiskContext);
if (context === undefined) {
throw new Error("useHealthRisk must be used within a HealthRiskProvider");
}
return context;
};

View File

@@ -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<boolean>(true);
const [hoveredYearIndex, setHoveredYearIndex] = React.useState<number | null>(
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<any>(null);
// 监听鼠标离开图表容器,恢复到当前年份
const handleMouseLeave = () => {
setHoveredYearIndex(null);
};
if (!predictionResults || predictionResults.length === 0) {
return null;
}
return (
<div className="absolute top-4 right-4 bg-white shadow-2xl rounded-xl overflow-hidden w-120 flex flex-col backdrop-blur-sm z-[1000] opacity-95 hover:opacity-100 transition-all duration-300">
{/* 头部 */}
<div className="flex justify-between items-center px-5 py-4 bg-[#257DD4] text-white">
<div className="flex items-center gap-2">
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z"
/>
</svg>
<h3 className="text-lg font-semibold"></h3>
<Chip
size="small"
label={`${predictionResults.length}`}
sx={{
backgroundColor: "rgba(255,255,255,0.2)",
color: "white",
fontWeight: "bold",
ml: 1,
}}
/>
</div>
</div>
{/* 内容区域 */}
<div className="p-2 h-[550px]" onMouseLeave={handleMouseLeave}>
<ReactECharts
ref={(e) => {
chartRef.current = e;
}}
option={option}
onEvents={onEvents}
style={{ height: "100%", width: "100%" }}
opts={{ renderer: "canvas" }}
/>
</div>
</div>
);
};
export default HealthRiskPieChart;

View File

@@ -27,41 +27,19 @@ import dayjs from "dayjs";
import { PlayArrow, Pause, Stop, Refresh } from "@mui/icons-material"; import { PlayArrow, Pause, Stop, Refresh } from "@mui/icons-material";
import { TbArrowBackUp, TbArrowForwardUp } from "react-icons/tb"; import { TbArrowBackUp, TbArrowForwardUp } from "react-icons/tb";
import { FiSkipBack, FiSkipForward } from "react-icons/fi"; 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 { 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; 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 { interface TimelineProps {
schemeDate?: Date; schemeDate?: Date;
timeRange?: { start: Date; end: Date }; timeRange?: { start: Date; end: Date };
@@ -77,14 +55,17 @@ const Timeline: React.FC<TimelineProps> = ({
return <div>Loading...</div>; // 或其他占位符 return <div>Loading...</div>; // 或其他占位符
} }
const { open } = useNotification(); const { open } = useNotification();
const {
predictionResults,
setPredictionResults,
currentYear,
setCurrentYear,
} = useHealthRisk();
const [selectedDateTime, setSelectedDateTime] = useState<Date>(new Date()); const [selectedDateTime, setSelectedDateTime] = useState<Date>(new Date());
const [currentYear, setCurrentYear] = useState<number>(4); // 时间轴当前值 (4-73)
const [isPlaying, setIsPlaying] = useState<boolean>(false); const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [playInterval, setPlayInterval] = useState<number>(15000); // 毫秒 const [playInterval, setPlayInterval] = useState<number>(15000); // 毫秒
const [isPredicting, setIsPredicting] = useState<boolean>(false); const [isPredicting, setIsPredicting] = useState<boolean>(false);
const [predictionResults, setPredictionResults] = useState<
PredictionResult[]
>([]);
const [pipeLayer, setPipeLayer] = useState<WebGLVectorTileLayer | null>(null); const [pipeLayer, setPipeLayer] = useState<WebGLVectorTileLayer | null>(null);
// 使用 ref 存储当前的健康数据,供事件监听器读取,避免重复绑定 // 使用 ref 存储当前的健康数据,供事件监听器读取,避免重复绑定
@@ -349,7 +330,7 @@ const Timeline: React.FC<TimelineProps> = ({
predictionResults.forEach((result) => { predictionResults.forEach((result) => {
const probability = getSurvivalProbabilityAtYear( const probability = getSurvivalProbabilityAtYear(
result.survival_function, result.survival_function,
currentYear - 1 // 使用索引 (0-based) currentYear - 4 // 使用索引 (0-based)
); );
pipeHealthData.set(result.link_id, probability); pipeHealthData.set(result.link_id, probability);
}); });
@@ -370,7 +351,7 @@ const Timeline: React.FC<TimelineProps> = ({
if (probabilities.length === 0) return; if (probabilities.length === 0) return;
// 使用等距分段从0-1分为十类 // 使用等距分段从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; const colors = RAINBOW_COLORS;
@@ -658,12 +639,7 @@ const Timeline: React.FC<TimelineProps> = ({
color: "primary.main", color: "primary.main",
}} }}
> >
{currentYear}
{predictionResults.length > 0 &&
predictionResults[0].survival_function.x[currentYear - 1] !==
undefined
? predictionResults[0].survival_function.x[currentYear - 1]
: currentYear}
</Typography> </Typography>
</Stack> </Stack>

View File

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