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 };