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

153 lines
4.9 KiB
JavaScript

const fs = require("node:fs");
const path = require("node:path");
const { parse } = require("csv-parse/sync");
const { ChartJSNodeCanvas } = require("chartjs-node-canvas");
const { createCanvas, loadImage, registerFont } = require("canvas");
const CHART_WIDTH = 1400;
const CHART_HEIGHT = 540;
const CHINESE_FONT_PATH = "C:\\Windows\\Fonts\\msyh.ttc";
if (fs.existsSync(CHINESE_FONT_PATH)) {
registerFont(CHINESE_FONT_PATH, { family: "Microsoft YaHei" });
}
const chartCanvas = new ChartJSNodeCanvas({
width: CHART_WIDTH,
height: CHART_HEIGHT,
backgroundColour: "white"
});
async function renderTimeSeries(points, options) {
return chartCanvas.renderToBuffer({
type: "line",
data: {
datasets: [{
data: points.map((row) => ({ x: row.t, y: row[options.column] })),
borderColor: options.color,
borderWidth: 2,
pointRadius: 0,
stepped: options.stepped,
tension: 0,
fill: false
}]
},
options: {
responsive: false,
animation: false,
parsing: false,
layout: { padding: { top: 14, right: 34, bottom: 8, left: 24 } },
plugins: {
legend: { display: false },
title: {
display: true,
text: options.title,
color: "#222222",
font: { family: "Microsoft YaHei", size: 21, weight: "normal" },
padding: { bottom: 10 }
}
},
scales: {
x: {
type: "linear",
min: options.xMin,
max: options.xMax,
grid: { color: "#d8d8d8", lineWidth: 1 },
border: { color: "#333333", width: 1.5 },
ticks: { color: "#333333", font: { family: "Microsoft YaHei", size: 14 } },
title: {
display: true,
text: "时间 (s)",
color: "#333333",
font: { family: "Microsoft YaHei", size: 17 }
}
},
y: {
min: 0,
max: options.yMax,
grid: { color: "#d8d8d8", lineWidth: 1 },
border: { color: "#333333", width: 1.5 },
ticks: { color: "#333333", font: { family: "Microsoft YaHei", size: 14 } },
title: {
display: true,
text: options.yLabel,
color: "#333333",
font: { family: "Microsoft YaHei", size: 17 }
}
}
}
}
});
}
async function renderCombinedPlot(points, outputPath) {
const minimumTime = Math.min(...points.map((row) => row.t));
const maximumTime = Math.max(...points.map((row) => row.t));
const timeSpan = Math.max(maximumTime - minimumTime, 1);
const xMin = Math.min(0, minimumTime);
const xMax = Math.ceil((maximumTime + timeSpan * 0.05) / 10) * 10;
const maximumPressure = Math.max(...points.map((row) => row.p));
const pressureStep = maximumPressure <= 100 ? 20 : 50;
const pressureMax = Math.ceil((maximumPressure * 1.1) / pressureStep) * pressureStep;
const upperImage = await renderTimeSeries(points, {
column: "u",
color: "#304ffe",
stepped: true,
title: "阀门开度随时间变化",
yLabel: "阀门开度 u (%)",
xMin,
xMax,
yMax: 100
});
const lowerImage = await renderTimeSeries(points, {
column: "p",
color: "#f5222d",
stepped: false,
title: "压力随时间变化",
yLabel: "压力 p (kPa)",
xMin,
xMax,
yMax: pressureMax
});
const canvas = createCanvas(CHART_WIDTH, CHART_HEIGHT * 2);
const context = canvas.getContext("2d");
context.fillStyle = "white";
context.fillRect(0, 0, canvas.width, canvas.height);
context.drawImage(await loadImage(upperImage), 0, 0);
context.drawImage(await loadImage(lowerImage), 0, CHART_HEIGHT);
await fs.promises.writeFile(outputPath, canvas.toBuffer("image/png"));
console.log(`[${new Date().toISOString()}] 已生成时序图: ${outputPath}`);
}
async function plotCsv(csvPath) {
const content = await fs.promises.readFile(csvPath, "utf8");
const rows = parse(content, {
bom: true,
columns: true,
skip_empty_lines: true,
trim: true
});
const requiredColumns = ["t", "u", "p"];
const headers = rows.length > 0 ? Object.keys(rows[0]) : [];
const missingColumns = requiredColumns.filter((column) => !headers.includes(column));
if (missingColumns.length > 0) {
throw new Error(`CSV 缺少列: ${missingColumns.join(", ")}`);
}
const points = rows
.map((row) => ({ t: Number(row.t), u: Number(row.u), p: Number(row.p) }))
.filter((row) => Number.isFinite(row.t) && Number.isFinite(row.u) && Number.isFinite(row.p));
if (points.length === 0) {
throw new Error("CSV 中没有可绘制的 t、u、p 数值行");
}
const parsedPath = path.parse(csvPath);
const outputPath = path.join(parsedPath.dir, `${parsedPath.name}-u-p-t.png`);
await renderCombinedPlot(points, outputPath);
return outputPath;
}
module.exports = { plotCsv };