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