update server
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user