使用柱状图展示数据分布;历史数据中使用圆点标识,提高散点数据的表达性;修改属性中的单位显示;新增一些样式属性

This commit is contained in:
JIANG
2025-12-22 10:36:47 +08:00
parent b0101202a7
commit c7f3ff4e5a
5 changed files with 147 additions and 78 deletions

View File

@@ -4,7 +4,7 @@ import MapComponent from "@app/OlMap/MapComponent";
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";
import HealthRiskStatistics from "@components/olmap/HealthRiskAnalysis/HealthRiskStatistics";
import PredictDataPanel from "@components/olmap/HealthRiskAnalysis/PredictDataPanel";
import StyleLegend from "@app/OlMap/Controls/StyleLegend";
import {
@@ -24,7 +24,7 @@ export default function Home() {
HistoryPanel={PredictDataPanel}
/>
<Timeline />
<HealthRiskPieChart />
<HealthRiskStatistics />
<Box className="absolute bottom-40 right-4 drop-shadow-xl flex flex-row items-end max-w-screen-lg overflow-x-auto z-10">
<StyleLegend
layerName="管道"

View File

@@ -410,17 +410,18 @@ const Toolbar: React.FC<ToolbarProps> = ({
const properties = highlightFeature.getProperties();
// 计算属性字段,增加 key 字段
const pipeComputedFields = [
{ key: "flow", label: "流量", unit: "m³/s" },
{ key: "flow", label: "流量", unit: "m³/h" },
{ key: "friction", label: "摩阻", unit: "" },
{ key: "headloss", label: "水头损失", unit: "m" },
{ key: "headlossPerKM", label: "单位水头损失", unit: "m/km" },
{ key: "quality", label: "水质", unit: "mg/L" },
{ key: "reaction", label: "反应", unit: "1/s" },
{ key: "reaction", label: "反应", unit: "1/d" },
{ key: "setting", label: "设置", unit: "" },
{ key: "status", label: "状态", unit: "" },
{ key: "velocity", label: "流速", unit: "m/s" },
];
const nodeComputedFields = [
{ key: "actualdemand", label: "实际需水量", unit: "m³/s" },
{ key: "actualdemand", label: "实际需水量", unit: "m³/h" },
{ key: "head", label: "水头", unit: "m" },
{ key: "pressure", label: "压力", unit: "m" },
{ key: "quality", label: "水质", unit: "mg/L" },

View File

@@ -293,7 +293,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
type: "point",
properties: [
// { name: "需求量", value: "demand" },
// { name: "海拔高度", value: "elevation" },
{ name: "高程", value: "elevation" },
{ name: "实际需求量", value: "actualdemand" },
{ name: "水头", value: "head" },
{ name: "压力", value: "pressure" },
@@ -318,6 +318,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
{ name: "流量", value: "flow" },
{ name: "摩阻系数", value: "friction" },
{ name: "水头损失", value: "headloss" },
{ name: "单位水头损失", value: "headlossPerKM" },
{ name: "水质", value: "quality" },
{ name: "反应速率", value: "reaction" },
{ name: "设置值", value: "setting" },

View File

@@ -3,7 +3,6 @@
import React, { useMemo } from "react";
import ReactECharts from "echarts-for-react";
import {
Paper,
Typography,
Box,
Chip,
@@ -13,11 +12,24 @@ import {
Slide,
Fade,
} from "@mui/material";
import { ChevronLeft, ChevronRight, PieChart } from "@mui/icons-material";
import { ChevronLeft, ChevronRight, BarChart } from "@mui/icons-material";
import { RAINBOW_COLORS, RISK_BREAKS, RISK_LABELS } from "./types";
import { useHealthRisk } from "./HealthRiskContext";
const HealthRiskPieChart: React.FC = () => {
const SIMPLE_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",
];
const HealthRiskStatistics: React.FC = () => {
const { predictionResults, currentYear } = useHealthRisk();
const [isExpanded, setIsExpanded] = React.useState<boolean>(true);
const [hoveredYearIndex, setHoveredYearIndex] = React.useState<number | null>(
@@ -27,22 +39,31 @@ const HealthRiskPieChart: React.FC = () => {
const datasetSource = useMemo(() => {
if (!predictionResults || predictionResults.length === 0) return [];
const years = Array.from({ length: 70 }, (_, i) => 4 + i); // 4 to 73
// 收集所有唯一的年份
const allYears = new Set<number>();
predictionResults.forEach((result) => {
if (result.survival_function.x) {
result.survival_function.x.forEach((year) => allYears.add(year));
}
});
const years = Array.from(allYears).sort((a, b) => a - b);
const header = ["Risk Level", ...years.map(String)];
const rows = RISK_LABELS.map((label, riskIdx) => {
const rows = SIMPLE_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];
const lowerBound = riskIdx === 0 ? -1 : RISK_BREAKS[riskIdx - 1];
const upperBound = RISK_BREAKS[riskIdx];
if (probability > lowerBound && probability <= upperBound) {
count++;
const { x, y } = result.survival_function;
if (x && x.includes(year)) {
const yearIndex = x.indexOf(year);
if (yearIndex >= 0 && yearIndex < y.length) {
const probability = y[yearIndex];
const lowerBound = riskIdx === 0 ? -1 : RISK_BREAKS[riskIdx - 1];
const upperBound = RISK_BREAKS[riskIdx];
if (probability > lowerBound && probability <= upperBound) {
count++;
}
}
}
});
@@ -58,13 +79,19 @@ const HealthRiskPieChart: React.FC = () => {
if (hoveredYearIndex !== null) {
return hoveredYearIndex + 1;
}
// 默认显示当前时间轴年份
return Math.max(1, Math.min(70, currentYear - 3));
}, [hoveredYearIndex, currentYear]);
// 查找 currentYear 在 datasetSource header 中的索引
if (datasetSource.length > 0) {
const header = datasetSource[0] as string[];
const yearStr = String(currentYear);
const index = header.indexOf(yearStr);
if (index !== -1) return index;
}
return 1;
}, [hoveredYearIndex, currentYear, datasetSource]);
const option = {
legend: {
top: "48%",
top: "middle",
left: "center",
itemWidth: 10,
itemHeight: 10,
@@ -76,53 +103,105 @@ const HealthRiskPieChart: React.FC = () => {
trigger: "axis",
showContent: true,
confine: true,
// formatter: function(params: any[]) {
// const year = params[0].axisValue;
// let content = `预测年份:${year}<br/>`;
// params.forEach((p: any) => {
// content += `${p.seriesName}: ${p.value}<br/>`;
// });
// return content;
// },
},
dataset: {
source: datasetSource,
},
xAxis: {
type: "category",
axisLabel: {
fontSize: 10,
grid: [
{
// 折线图网格
top: "62%",
bottom: "8%",
left: "12%",
right: "5%",
},
},
yAxis: {
gridIndex: 0,
axisLabel: {
fontSize: 10,
{
// 柱状图网格
top: "5%",
bottom: "60%",
left: "10%",
right: "10%",
},
},
grid: {
top: "62%",
bottom: "8%",
left: "12%",
right: "5%",
},
],
xAxis: [
{
type: "category",
gridIndex: 0,
data:
datasetSource.length > 0
? (datasetSource[0] as string[]).slice(1)
: [],
axisLabel: {
fontSize: 10,
},
},
{
type: "value",
gridIndex: 1,
name: "数量",
axisLabel: {
fontSize: 10,
},
nameTextStyle: {
fontSize: 10,
},
},
],
yAxis: [
{
gridIndex: 0,
axisLabel: {
fontSize: 10,
},
},
{
type: "category",
gridIndex: 1,
data: SIMPLE_LABELS,
inverse: true, // 让极高风险在上方
axisLabel: {
fontSize: 10,
},
},
],
series: [
...RISK_LABELS.map((_, i) => ({
name: RISK_LABELS[i],
type: "line",
smooth: true,
seriesLayoutBy: "row",
xAxisIndex: 0,
yAxisIndex: 0,
emphasis: { focus: "series" },
itemStyle: { color: RAINBOW_COLORS[i] },
symbol: "none",
})),
{
type: "pie",
id: "pie",
radius: "35%",
center: ["50%", "24%"],
emphasis: {
focus: "self",
type: "bar",
id: "bar",
xAxisIndex: 1,
yAxisIndex: 1,
encode: {
x: displayDimension,
y: "Risk Level",
},
label: {
formatter: "{b}: {@[" + displayDimension + "]} ({d}%)",
show: true,
position: "right",
fontSize: 10,
},
encode: {
itemName: "Risk Level",
value: displayDimension,
tooltip: displayDimension,
itemStyle: {
color: (params: any) => {
return RAINBOW_COLORS[params.dataIndex];
},
},
},
],
@@ -158,7 +237,7 @@ const HealthRiskPieChart: React.FC = () => {
color: "text.secondary",
}}
>
<PieChart sx={{ fontSize: 64, mb: 2, opacity: 0.3 }} />
<BarChart sx={{ fontSize: 64, mb: 2, opacity: 0.3 }} />
<Typography variant="h6" gutterBottom sx={{ fontWeight: 500 }}>
</Typography>
@@ -178,7 +257,7 @@ const HealthRiskPieChart: React.FC = () => {
sx={{ zIndex: 1300 }}
>
<Box className="flex flex-col items-center py-3 px-3 gap-1">
<PieChart className="text-[#257DD4] w-5 h-5" />
<BarChart className="text-[#257DD4] w-5 h-5" />
<Typography
variant="caption"
className="text-gray-700 font-semibold my-1 text-xs"
@@ -196,25 +275,7 @@ const HealthRiskPieChart: React.FC = () => {
{/* 头部 */}
<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>
<BarChart className="w-5 h-5" />
<h3 className="text-lg font-semibold"></h3>
<Chip
size="small"
@@ -262,4 +323,4 @@ const HealthRiskPieChart: React.FC = () => {
);
};
export default HealthRiskPieChart;
export default HealthRiskStatistics;

View File

@@ -718,7 +718,8 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
{
name: `${id} (原始)`,
type: "line",
symbol: "none",
showSymbol: true,
symbolSize: 4,
sampling: "lttb",
itemStyle: { color: colors[index % colors.length] },
data: dataset.map((item) => item[`${id}_raw`]),
@@ -726,7 +727,8 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
{
name: `${id} (清洗)`,
type: "line",
symbol: "none",
showSymbol: true,
symbolSize: 4,
sampling: "lttb",
itemStyle: { color: colors[(index + 3) % colors.length] },
data: dataset.map((item) => item[`${id}_clean`]),
@@ -734,7 +736,8 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
{
name: `${id} (模拟)`,
type: "line",
symbol: "none",
showSymbol: true,
symbolSize: 4,
sampling: "lttb",
itemStyle: { color: colors[(index + 6) % colors.length] },
data: dataset.map((item) => item[`${id}_sim`]),
@@ -744,7 +747,8 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
return deviceIds.map((id, index) => ({
name: id,
type: "line",
symbol: "none",
showSymbol: true,
symbolSize: 4,
sampling: "lttb",
itemStyle: { color: colors[index % colors.length] },
data: dataset.map((item) => item[`${id}_${selectedSource}`]),
@@ -768,7 +772,8 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
: "模拟"
})`,
type: "line",
symbol: "none",
showSymbol: true,
symbolSize: 4,
sampling: "lttb",
itemStyle: {
color: colors[(index * 3 + sIndex) % colors.length],
@@ -795,7 +800,8 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
series.push({
name: id,
type: "line",
symbol: "none",
showSymbol: true,
symbolSize: 4,
sampling: "lttb",
itemStyle: { color: colors[index % colors.length] },
data: dataset.map((item) => item[id]),