使用柱状图展示数据分布;历史数据中使用圆点标识,提高散点数据的表达性;修改属性中的单位显示;新增一些样式属性
This commit is contained in:
@@ -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="管道"
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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;
|
||||
@@ -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]),
|
||||
|
||||
Reference in New Issue
Block a user