153 lines
4.9 KiB
JavaScript
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 }; |