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

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 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 { 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 PredictDataPanel from "@components/olmap/HealthRiskAnalysis/PredictDataPanel";
import StyleLegend from "@app/OlMap/Controls/StyleLegend"; import StyleLegend from "@app/OlMap/Controls/StyleLegend";
import { import {
@@ -24,7 +24,7 @@ export default function Home() {
HistoryPanel={PredictDataPanel} HistoryPanel={PredictDataPanel}
/> />
<Timeline /> <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"> <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 <StyleLegend
layerName="管道" layerName="管道"

View File

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

View File

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

View File

@@ -3,7 +3,6 @@
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import ReactECharts from "echarts-for-react"; import ReactECharts from "echarts-for-react";
import { import {
Paper,
Typography, Typography,
Box, Box,
Chip, Chip,
@@ -13,11 +12,24 @@ import {
Slide, Slide,
Fade, Fade,
} from "@mui/material"; } 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 { RAINBOW_COLORS, RISK_BREAKS, RISK_LABELS } from "./types";
import { useHealthRisk } from "./HealthRiskContext"; 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 { predictionResults, currentYear } = useHealthRisk();
const [isExpanded, setIsExpanded] = React.useState<boolean>(true); const [isExpanded, setIsExpanded] = React.useState<boolean>(true);
const [hoveredYearIndex, setHoveredYearIndex] = React.useState<number | null>( const [hoveredYearIndex, setHoveredYearIndex] = React.useState<number | null>(
@@ -27,22 +39,31 @@ const HealthRiskPieChart: React.FC = () => {
const datasetSource = useMemo(() => { const datasetSource = useMemo(() => {
if (!predictionResults || predictionResults.length === 0) return []; 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 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]; const row: (string | number)[] = [label];
years.forEach((year) => { years.forEach((year) => {
let count = 0; let count = 0;
predictionResults.forEach((result) => { predictionResults.forEach((result) => {
const { y } = result.survival_function; const { x, y } = result.survival_function;
const index = year - 4; if (x && x.includes(year)) {
if (index >= 0 && index < y.length) { const yearIndex = x.indexOf(year);
const probability = y[index]; if (yearIndex >= 0 && yearIndex < y.length) {
const lowerBound = riskIdx === 0 ? -1 : RISK_BREAKS[riskIdx - 1]; const probability = y[yearIndex];
const upperBound = RISK_BREAKS[riskIdx]; const lowerBound = riskIdx === 0 ? -1 : RISK_BREAKS[riskIdx - 1];
if (probability > lowerBound && probability <= upperBound) { const upperBound = RISK_BREAKS[riskIdx];
count++; if (probability > lowerBound && probability <= upperBound) {
count++;
}
} }
} }
}); });
@@ -58,13 +79,19 @@ const HealthRiskPieChart: React.FC = () => {
if (hoveredYearIndex !== null) { if (hoveredYearIndex !== null) {
return hoveredYearIndex + 1; return hoveredYearIndex + 1;
} }
// 默认显示当前时间轴年份 // 查找 currentYear 在 datasetSource header 中的索引
return Math.max(1, Math.min(70, currentYear - 3)); if (datasetSource.length > 0) {
}, [hoveredYearIndex, currentYear]); 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 = { const option = {
legend: { legend: {
top: "48%", top: "middle",
left: "center", left: "center",
itemWidth: 10, itemWidth: 10,
itemHeight: 10, itemHeight: 10,
@@ -76,53 +103,105 @@ const HealthRiskPieChart: React.FC = () => {
trigger: "axis", trigger: "axis",
showContent: true, showContent: true,
confine: 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: { dataset: {
source: datasetSource, source: datasetSource,
}, },
xAxis: { grid: [
type: "category", {
axisLabel: { // 折线图网格
fontSize: 10, top: "62%",
bottom: "8%",
left: "12%",
right: "5%",
}, },
}, {
yAxis: { // 柱状图网格
gridIndex: 0, top: "5%",
axisLabel: { bottom: "60%",
fontSize: 10, left: "10%",
right: "10%",
}, },
}, ],
grid: { xAxis: [
top: "62%", {
bottom: "8%", type: "category",
left: "12%", gridIndex: 0,
right: "5%", 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: [ series: [
...RISK_LABELS.map((_, i) => ({ ...RISK_LABELS.map((_, i) => ({
name: RISK_LABELS[i],
type: "line", type: "line",
smooth: true, smooth: true,
seriesLayoutBy: "row", seriesLayoutBy: "row",
xAxisIndex: 0,
yAxisIndex: 0,
emphasis: { focus: "series" }, emphasis: { focus: "series" },
itemStyle: { color: RAINBOW_COLORS[i] }, itemStyle: { color: RAINBOW_COLORS[i] },
symbol: "none", symbol: "none",
})), })),
{ {
type: "pie", type: "bar",
id: "pie", id: "bar",
radius: "35%", xAxisIndex: 1,
center: ["50%", "24%"], yAxisIndex: 1,
emphasis: { encode: {
focus: "self", x: displayDimension,
y: "Risk Level",
}, },
label: { label: {
formatter: "{b}: {@[" + displayDimension + "]} ({d}%)", show: true,
position: "right",
fontSize: 10, fontSize: 10,
}, },
encode: { itemStyle: {
itemName: "Risk Level", color: (params: any) => {
value: displayDimension, return RAINBOW_COLORS[params.dataIndex];
tooltip: displayDimension, },
}, },
}, },
], ],
@@ -158,7 +237,7 @@ const HealthRiskPieChart: React.FC = () => {
color: "text.secondary", 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 variant="h6" gutterBottom sx={{ fontWeight: 500 }}>
</Typography> </Typography>
@@ -178,7 +257,7 @@ const HealthRiskPieChart: React.FC = () => {
sx={{ zIndex: 1300 }} sx={{ zIndex: 1300 }}
> >
<Box className="flex flex-col items-center py-3 px-3 gap-1"> <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 <Typography
variant="caption" variant="caption"
className="text-gray-700 font-semibold my-1 text-xs" 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 justify-between items-center px-5 py-4 bg-[#257DD4] text-white">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<svg <BarChart className="w-5 h-5" />
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> <h3 className="text-lg font-semibold"></h3>
<Chip <Chip
size="small" 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} (原始)`, name: `${id} (原始)`,
type: "line", type: "line",
symbol: "none", showSymbol: true,
symbolSize: 4,
sampling: "lttb", sampling: "lttb",
itemStyle: { color: colors[index % colors.length] }, itemStyle: { color: colors[index % colors.length] },
data: dataset.map((item) => item[`${id}_raw`]), data: dataset.map((item) => item[`${id}_raw`]),
@@ -726,7 +727,8 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
{ {
name: `${id} (清洗)`, name: `${id} (清洗)`,
type: "line", type: "line",
symbol: "none", showSymbol: true,
symbolSize: 4,
sampling: "lttb", sampling: "lttb",
itemStyle: { color: colors[(index + 3) % colors.length] }, itemStyle: { color: colors[(index + 3) % colors.length] },
data: dataset.map((item) => item[`${id}_clean`]), data: dataset.map((item) => item[`${id}_clean`]),
@@ -734,7 +736,8 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
{ {
name: `${id} (模拟)`, name: `${id} (模拟)`,
type: "line", type: "line",
symbol: "none", showSymbol: true,
symbolSize: 4,
sampling: "lttb", sampling: "lttb",
itemStyle: { color: colors[(index + 6) % colors.length] }, itemStyle: { color: colors[(index + 6) % colors.length] },
data: dataset.map((item) => item[`${id}_sim`]), data: dataset.map((item) => item[`${id}_sim`]),
@@ -744,7 +747,8 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
return deviceIds.map((id, index) => ({ return deviceIds.map((id, index) => ({
name: id, name: id,
type: "line", type: "line",
symbol: "none", showSymbol: true,
symbolSize: 4,
sampling: "lttb", sampling: "lttb",
itemStyle: { color: colors[index % colors.length] }, itemStyle: { color: colors[index % colors.length] },
data: dataset.map((item) => item[`${id}_${selectedSource}`]), data: dataset.map((item) => item[`${id}_${selectedSource}`]),
@@ -768,7 +772,8 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
: "模拟" : "模拟"
})`, })`,
type: "line", type: "line",
symbol: "none", showSymbol: true,
symbolSize: 4,
sampling: "lttb", sampling: "lttb",
itemStyle: { itemStyle: {
color: colors[(index * 3 + sIndex) % colors.length], color: colors[(index * 3 + sIndex) % colors.length],
@@ -795,7 +800,8 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
series.push({ series.push({
name: id, name: id,
type: "line", type: "line",
symbol: "none", showSymbol: true,
symbolSize: 4,
sampling: "lttb", sampling: "lttb",
itemStyle: { color: colors[index % colors.length] }, itemStyle: { color: colors[index % colors.length] },
data: dataset.map((item) => item[id]), data: dataset.map((item) => item[id]),