Files
ReinLoopTest/ControlPanel/plot-json.js
T
2026-07-30 11:12:31 +08:00

135 lines
5.4 KiB
JavaScript

const fs = require("node:fs");
const path = require("node:path");
const { ChartJSNodeCanvas } = require("chartjs-node-canvas");
const chartCanvas = new ChartJSNodeCanvas({
width: 1400,
height: 720,
backgroundColour: "white"
});
function normalizeJsonSeries(data) {
if (Array.isArray(data)) {
if (data.every(Number.isFinite)) {
return { labels: data.map((_, index) => index), series: [{ label: "value", data }] };
}
if (data.every((item) => item && typeof item === "object" && !Array.isArray(item))) {
const numericFields = [...new Set(data.flatMap(Object.keys))]
.filter((field) => data.some((item) => Number.isFinite(item[field])));
const xField = ["t", "time", "x", "timestamp", "index", "distance"]
.find((field) => numericFields.includes(field));
const valueFields = numericFields.filter((field) => field !== xField);
const isTravelStability = xField === "distance" && valueFields.length === 1 && valueFields[0] === "pressure";
return {
labels: data.map((item, index) => xField ? item[xField] : index),
series: valueFields.map((field) => ({
label: field,
data: isTravelStability
? data.filter((item) => Number.isFinite(item.distance) && Number.isFinite(item[field]))
.map((item) => ({ x: item.distance, y: item[field] }))
: data.map((item) => Number.isFinite(item[field]) ? item[field] : null)
})),
chartKind: isTravelStability ? "travel-stability" : "line",
xLabel: xField === "distance" ? "行程" : "采样点 / 时间",
yLabel: isTravelStability
? "稳态压力 (kPa)"
: "数值"
};
}
}
if (data && typeof data === "object") {
const arrays = Object.entries(data).filter(([, value]) => Array.isArray(value));
if (arrays.length === 1) return normalizeJsonSeries(arrays[0][1]);
const numericArrays = arrays.filter(([, values]) =>
values.length > 0 && values.every(Number.isFinite)
);
if (numericArrays.length > 0) {
const xEntry = numericArrays.find(([field]) =>
["t", "time", "x", "timestamp", "index"].includes(field)
);
const seriesEntries = numericArrays.filter(([field]) => !xEntry || field !== xEntry[0]);
const pointCount = Math.max(...numericArrays.map(([, values]) => values.length));
return {
labels: xEntry ? xEntry[1] : Array.from({ length: pointCount }, (_, index) => index),
series: seriesEntries.map(([label, values]) => ({ label, data: values }))
};
}
}
throw new Error("JSON 必须包含数字数组、数值对象数组或多个数值数组字段");
}
const travelPointLabels = {
id: "travelPointLabels",
afterDatasetsDraw(chart) {
if (chart.options.plugins.travelPointLabels !== true) return;
const { ctx } = chart;
ctx.save();
ctx.fillStyle = "#15251f";
ctx.font = "12px sans-serif";
ctx.textAlign = "left";
ctx.textBaseline = "bottom";
for (const meta of chart.getSortedVisibleDatasetMetas()) {
meta.data.forEach((element, index) => {
const point = chart.data.datasets[meta.index].data[index];
ctx.fillText(`(${point.x}, ${Number(point.y).toFixed(2)})`, element.x + 7, element.y - 7);
});
}
ctx.restore();
}
};
function buildChartConfiguration(normalized, title) {
const isTravelStability = normalized.chartKind === "travel-stability";
const colors = ["#d62828", "#0077b6", "#2a9d8f", "#f77f00", "#6a4c93", "#495057"];
return {
type: isTravelStability ? "scatter" : "line",
data: {
labels: isTravelStability ? undefined : normalized.labels,
datasets: normalized.series.map((item, index) => ({
...item,
borderColor: colors[index % colors.length],
borderWidth: 2,
pointRadius: isTravelStability ? 4 : 0,
pointHoverRadius: isTravelStability ? 7 : 3,
showLine: isTravelStability,
tension: 0,
fill: false
}))
},
options: {
responsive: false,
animation: false,
layout: isTravelStability ? { padding: { top: 24, right: 92 } } : undefined,
plugins: {
title: { display: true, text: title },
legend: { display: true },
travelPointLabels: isTravelStability
},
scales: {
x: isTravelStability
? { type: "linear", min: 0, max: 1000, title: { display: true, text: normalized.xLabel } }
: { title: { display: true, text: normalized.xLabel } },
y: { title: { display: true, text: normalized.yLabel } }
}
},
plugins: isTravelStability ? [travelPointLabels] : []
};
}
async function plotJson(jsonPath) {
const content = await fs.promises.readFile(jsonPath, "utf8");
const normalized = normalizeJsonSeries(JSON.parse(content));
if (normalized.series.length === 0) throw new Error("JSON 数组中没有可绘制的数值字段");
const image = await chartCanvas.renderToBuffer(buildChartConfiguration(normalized, path.basename(jsonPath)));
const parsedPath = path.parse(jsonPath);
const outputPath = path.join(parsedPath.dir, `${parsedPath.name}-line.png`);
await fs.promises.writeFile(outputPath, image);
console.log(`[${new Date().toISOString()}] 已生成 JSON 折线图: ${outputPath}`);
return outputPath;
}
module.exports = { buildChartConfiguration, normalizeJsonSeries, plotJson };