使用 echart 绘制图表

This commit is contained in:
JIANG
2025-11-19 17:46:16 +08:00
parent 1ca2e80645
commit 48716f4876
3 changed files with 200 additions and 272 deletions

View File

@@ -14,7 +14,6 @@ import {
Tooltip,
Typography,
Drawer,
Slider,
} from "@mui/material";
import {
Refresh,
@@ -25,7 +24,8 @@ import {
ChevronRight,
} from "@mui/icons-material";
import { DataGrid, GridColDef } from "@mui/x-data-grid";
import { LineChart } from "@mui/x-charts";
import ReactECharts from "echarts-for-react";
import * as echarts from "echarts";
import "dayjs/locale/zh-cn"; // 引入中文包
import dayjs, { Dayjs } from "dayjs";
import utc from "dayjs/plugin/utc";
@@ -347,10 +347,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
const [isCleaning, setIsCleaning] = useState<boolean>(false);
const [selectedSource, setSelectedSource] = useState<
"raw" | "clean" | "sim" | "all"
>("all");
// 滑块状态:用于图表缩放
const [zoomRange, setZoomRange] = useState<[number, number]>([0, 100]);
>(() => (deviceIds.length === 1 ? "all" : "clean"));
// 获取 SCADA 设备信息,生成 deviceLabels
useEffect(() => {
@@ -396,21 +393,6 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
[timeSeries, deviceIds, fractionDigits, showCleaning]
);
// 根据滑块范围过滤数据集
const filteredDataset = useMemo(() => {
if (dataset.length === 0) return dataset;
const startIndex = Math.floor((zoomRange[0] / 100) * dataset.length);
const endIndex = Math.ceil((zoomRange[1] / 100) * dataset.length);
return dataset.slice(startIndex, endIndex);
}, [dataset, zoomRange]);
// 重置滑块范围当数据变化时
useEffect(() => {
setZoomRange([0, 100]);
}, [timeSeries]);
const handleFetch = useCallback(
async (reason: string) => {
if (!hasDevices) {
@@ -530,10 +512,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
if (deviceIds.length > 1 && selectedSource === "all") {
setSelectedSource("clean");
}
// else if (deviceIds.length === 1 && selectedSource !== "all") {
// setSelectedSource("all");
// }
}, [deviceIds.length]);
}, [deviceIds.length, selectedSource]);
const columns: GridColDef[] = useMemo(() => {
const base: GridColDef[] = [
@@ -680,17 +659,119 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
"#3f51b5", // 靛蓝色
];
// 获取当前显示范围的时间边界
const getTimeRangeLabel = () => {
if (filteredDataset.length === 0) return "";
const firstTime = filteredDataset[0].time;
const lastTime = filteredDataset[filteredDataset.length - 1].time;
if (firstTime instanceof Date && lastTime instanceof Date) {
return `${dayjs(firstTime).format("MM-DD HH:mm")} ~ ${dayjs(
lastTime
).format("MM-DD HH:mm")}`;
const xData = dataset.map((item) => item.label);
const getSeries = () => {
if (showCleaning) {
if (selectedSource === "all") {
return deviceIds.flatMap((id, index) => [
{
name: `${deviceLabels?.[id] ?? id} (原始)`,
type: "line",
symbol: "none",
sampling: "lttb",
itemStyle: { color: colors[index % colors.length] },
data: dataset.map((item) => item[`${id}_raw`]),
},
{
name: `${deviceLabels?.[id] ?? id} (清洗)`,
type: "line",
symbol: "none",
sampling: "lttb",
itemStyle: { color: colors[(index + 3) % colors.length] },
data: dataset.map((item) => item[`${id}_clean`]),
},
{
name: `${deviceLabels?.[id] ?? id} (模拟)`,
type: "line",
symbol: "none",
sampling: "lttb",
itemStyle: { color: colors[(index + 6) % colors.length] },
data: dataset.map((item) => item[`${id}_sim`]),
},
]);
} else {
return deviceIds.map((id, index) => ({
name: deviceLabels?.[id] ?? id,
type: "line",
symbol: "none",
sampling: "lttb",
itemStyle: { color: colors[index % colors.length] },
data: dataset.map((item) => item[`${id}_${selectedSource}`]),
}));
}
} else {
return deviceIds.map((id, index) => ({
name: deviceLabels?.[id] ?? id,
type: "line",
symbol: "none",
sampling: "lttb",
itemStyle: { color: colors[index % colors.length] },
data: dataset.map((item) => item[id]),
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: colors[index % colors.length],
},
{
offset: 1,
color: "rgba(255, 255, 255, 0)",
},
]),
opacity: 0.3,
},
}));
}
return "";
};
const option = {
tooltip: {
trigger: "axis",
confine: true,
position: function (pt: any[]) {
return [pt[0], "10%"];
},
},
legend: {
top: "top",
},
grid: {
left: "5%",
right: "5%",
bottom: "11%",
containLabel: true,
},
toolbox: {
feature: {
dataZoom: {
yAxisIndex: "none",
},
restore: {},
saveAsImage: {},
},
},
xAxis: {
type: "category",
boundaryGap: false,
data: xData,
},
yAxis: {
type: "value",
scale: true,
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: getSeries(),
};
return (
@@ -700,237 +781,15 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
height: "100%",
display: "flex",
flexDirection: "column",
overflow: "hidden",
}}
>
<Box sx={{ flex: 1 }}>
<LineChart
dataset={filteredDataset}
height={480}
margin={{ left: 70, right: 40, top: 30, bottom: 90 }}
xAxis={[
{
dataKey: "time",
scaleType: "time",
valueFormatter: (value) =>
value instanceof Date
? dayjs(value).format("MM-DD HH:mm")
: String(value),
tickLabelStyle: {
angle: -45,
textAnchor: "end",
fontSize: 11,
fill: "#666",
},
},
]}
yAxis={[
{
label: "压力/流量值",
labelStyle: {
fontSize: 13,
fill: "#333",
fontWeight: 500,
},
tickLabelStyle: {
fontSize: 11,
fill: "#666",
},
},
]}
series={(() => {
if (showCleaning) {
if (selectedSource === "all") {
// 全部模式:显示所有设备的三种数据
return deviceIds.flatMap((id, index) => [
{
dataKey: `${id}_raw`,
label: `${deviceLabels?.[id] ?? id} (原始)`,
showMark: dataset.length < 50,
curve: "catmullRom",
color: colors[index % colors.length],
valueFormatter: (value: number | null) =>
value !== null ? value.toFixed(fractionDigits) : "--",
area: false,
stack: undefined,
},
{
dataKey: `${id}_clean`,
label: `${deviceLabels?.[id] ?? id} (清洗)`,
showMark: dataset.length < 50,
curve: "catmullRom",
color: colors[(index + 3) % colors.length],
valueFormatter: (value: number | null) =>
value !== null ? value.toFixed(fractionDigits) : "--",
area: false,
stack: undefined,
},
{
dataKey: `${id}_sim`,
label: `${deviceLabels?.[id] ?? id} (模拟)`,
showMark: dataset.length < 50,
curve: "catmullRom",
color: colors[(index + 6) % colors.length],
valueFormatter: (value: number | null) =>
value !== null ? value.toFixed(fractionDigits) : "--",
area: false,
stack: undefined,
},
]);
} else {
// 单一数据源模式:只显示选中的数据源
return deviceIds.map((id, index) => ({
dataKey: `${id}_${selectedSource}`,
label: deviceLabels?.[id] ?? id,
showMark: dataset.length < 50,
curve: "catmullRom",
color: colors[index % colors.length],
valueFormatter: (value: number | null) =>
value !== null ? value.toFixed(fractionDigits) : "--",
area: false,
stack: undefined,
}));
}
} else {
return deviceIds.map((id, index) => ({
dataKey: id,
label: deviceLabels?.[id] ?? id,
showMark: dataset.length < 50,
curve: "catmullRom",
color: colors[index % colors.length],
valueFormatter: (value: number | null) =>
value !== null ? value.toFixed(fractionDigits) : "--",
area: false,
stack: undefined,
}));
}
})()}
grid={{ vertical: true, horizontal: true }}
sx={{
"& .MuiLineElement-root": {
strokeWidth: 2.5,
strokeLinecap: "round",
strokeLinejoin: "round",
},
"& .MuiMarkElement-root": {
scale: "0.8",
strokeWidth: 2,
},
"& .MuiChartsAxis-line": {
stroke: "#e0e0e0",
strokeWidth: 1,
},
"& .MuiChartsAxis-tick": {
stroke: "#e0e0e0",
strokeWidth: 1,
},
"& .MuiChartsGrid-line": {
stroke: "#d0d0d0",
strokeWidth: 0.8,
strokeDasharray: "4 4",
},
}}
slotProps={{
legend: {
direction: "row",
position: { horizontal: "middle", vertical: "bottom" },
padding: { bottom: 2, left: 0, right: 0 },
itemMarkWidth: 16,
itemMarkHeight: 3,
markGap: 8,
itemGap: 16,
labelStyle: {
fontSize: 12,
fill: "#333",
fontWeight: 500,
},
},
loadingOverlay: {
style: { backgroundColor: "rgba(255, 255, 255, 0.7)" },
},
}}
tooltip={{
trigger: "axis",
}}
/>
</Box>
{/* 时间范围滑块 */}
<Box sx={{ px: 3, pb: 2, pt: 1 }}>
<Stack direction="row" spacing={2} alignItems="center">
<Typography
variant="body2"
sx={{ minWidth: 60, color: "text.secondary", fontSize: "0.8rem" }}
>
</Typography>
<Slider
value={zoomRange}
onChange={(_, newValue) =>
setZoomRange(newValue as [number, number])
}
valueLabelDisplay="auto"
valueLabelFormat={(value) => {
const index = Math.floor((value / 100) * dataset.length);
if (dataset[index] && dataset[index].time instanceof Date) {
return dayjs(dataset[index].time).format("MM-DD HH:mm");
}
return `${value}%`;
}}
marks={[
{
value: 0,
label:
dataset.length > 0 && dataset[0].time instanceof Date
? dayjs(dataset[0].time).format("MM-DD HH:mm")
: "起始",
},
{
value: 100,
label:
dataset.length > 0 &&
dataset[dataset.length - 1].time instanceof Date
? dayjs(dataset[dataset.length - 1].time).format(
"MM-DD HH:mm"
)
: "结束",
},
]}
sx={{
flex: 1,
"& .MuiSlider-thumb": {
width: 16,
height: 16,
},
"& .MuiSlider-markLabel": {
fontSize: "0.7rem",
color: "text.secondary",
},
}}
/>
<Button
size="small"
variant="outlined"
onClick={() => setZoomRange([0, 100])}
sx={{ minWidth: 60, fontSize: "0.75rem" }}
>
</Button>
</Stack>
{getTimeRangeLabel() && (
<Typography
variant="caption"
sx={{
color: "primary.main",
display: "block",
textAlign: "center",
mt: 0.5,
}}
>
: {getTimeRangeLabel()} ( {filteredDataset.length}{" "}
)
</Typography>
)}
</Box>
<ReactECharts
option={option}
style={{ height: "100%", width: "100%" }}
notMerge={true}
lazyUpdate={true}
/>
</Box>
);
};
@@ -1087,11 +946,18 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
<DateTimePicker
label="开始时间"
value={from}
onChange={(value) =>
value && dayjs.isDayjs(value) && setFrom(value)
}
onChange={(value) => {
if (value && dayjs.isDayjs(value) && value.isValid()) {
setFrom(value);
}
}}
onAccept={(value) => {
if (value && dayjs.isDayjs(value) && hasDevices) {
if (
value &&
dayjs.isDayjs(value) &&
value.isValid() &&
hasDevices
) {
handleFetch("date-change");
}
}}
@@ -1103,11 +969,18 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
<DateTimePicker
label="结束时间"
value={to}
onChange={(value) =>
value && dayjs.isDayjs(value) && setTo(value)
}
onChange={(value) => {
if (value && dayjs.isDayjs(value) && value.isValid()) {
setTo(value);
}
}}
onAccept={(value) => {
if (value && dayjs.isDayjs(value) && hasDevices) {
if (
value &&
dayjs.isDayjs(value) &&
value.isValid() &&
hasDevices
) {
handleFetch("date-change");
}
}}