|
|
@@ -1,9 +1,4 @@
|
|
|
-import puppeteer from "puppeteer";
|
|
|
-import fs from "fs-extra";
|
|
|
-import path from "path";
|
|
|
-import FormData from "form-data";
|
|
|
-import { colorSchemes } from "../colors.js";
|
|
|
-import axios from "axios";
|
|
|
+import { renderChart } from "../chartService/index.js";
|
|
|
|
|
|
/**
|
|
|
* 生成折线图并上传
|
|
|
@@ -17,199 +12,128 @@ export const generatePlotlyChartsFen = async (
|
|
|
fieldEngineCode,
|
|
|
) => {
|
|
|
try {
|
|
|
- // 创建临时目录
|
|
|
- const tempDir = path.join(process.cwd(), "images");
|
|
|
- await fs.ensureDir(tempDir);
|
|
|
- const tempFilePath = path.join(
|
|
|
- tempDir,
|
|
|
- `temp_heatmap_chart_${Date.now()}.jpeg`,
|
|
|
- );
|
|
|
+ if (!data || !Array.isArray(data.data)) {
|
|
|
+ throw new Error("PlotlyChartsFen data invalid");
|
|
|
+ }
|
|
|
|
|
|
- // 获取 plotly.js 的绝对路径
|
|
|
- const plotlyPath = path.join(
|
|
|
- process.cwd(),
|
|
|
- "src",
|
|
|
- "public",
|
|
|
- "js",
|
|
|
- "plotly-latest.min.js",
|
|
|
- );
|
|
|
- const plotlyContent = await fs.readFile(plotlyPath, "utf-8");
|
|
|
+ const getEngineCode = (item) => item.engineCode || item.enginCode;
|
|
|
+ const getEngineName = (item) => item.engineName || item.enginName || "";
|
|
|
+ const isContractLine = (item) => getEngineName(item) === "合同功率曲线";
|
|
|
|
|
|
- // 创建浏览器实例
|
|
|
- const browser = await puppeteer.launch({
|
|
|
- headless: "new",
|
|
|
- // 根据系统改路径
|
|
|
- executablePath: `${process.env.CHROME_PATH}`, // 根据系统改路径
|
|
|
+ const buildValidPairs = (xData = [], yData = []) => {
|
|
|
+ const maxLen = Math.min(xData.length, yData.length);
|
|
|
+ const pairs = [];
|
|
|
+ for (let i = 0; i < maxLen; i++) {
|
|
|
+ const x = xData[i];
|
|
|
+ const y = Number(yData[i]);
|
|
|
+ if (x === null || x === undefined || x === "") continue;
|
|
|
+ if (!Number.isFinite(y)) continue;
|
|
|
+ pairs.push({ x, y });
|
|
|
+ }
|
|
|
+ return pairs;
|
|
|
+ };
|
|
|
|
|
|
- args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
|
|
- });
|
|
|
+ const sortedData = [...data.data]
|
|
|
+ .filter((item) => !isContractLine(item))
|
|
|
+ .sort((a, b) => {
|
|
|
+ const aIsTarget = getEngineCode(a) === fieldEngineCode;
|
|
|
+ const bIsTarget = getEngineCode(b) === fieldEngineCode;
|
|
|
|
|
|
- try {
|
|
|
- const page = await browser.newPage();
|
|
|
+ if (aIsTarget && !bIsTarget) return 1;
|
|
|
+ if (!aIsTarget && bIsTarget) return -1;
|
|
|
|
|
|
- // 准备图表数据
|
|
|
- const sortedData = data.data.sort((a, b) => {
|
|
|
- if (
|
|
|
- a.enginCode === fieldEngineCode &&
|
|
|
- b.enginCode !== fieldEngineCode
|
|
|
- ) {
|
|
|
- return 1;
|
|
|
- }
|
|
|
- if (
|
|
|
- a.enginCode !== fieldEngineCode &&
|
|
|
- b.enginCode === fieldEngineCode
|
|
|
- ) {
|
|
|
- return -1;
|
|
|
- }
|
|
|
- return 0;
|
|
|
+ return getEngineName(a).localeCompare(getEngineName(b));
|
|
|
});
|
|
|
|
|
|
- const finalData = [];
|
|
|
- let enginName = "";
|
|
|
- sortedData
|
|
|
- .filter((item) => item.enginName !== "合同功率曲线")
|
|
|
- .forEach((turbine) => {
|
|
|
- const color =
|
|
|
- turbine.enginCode === fieldEngineCode ? "#406DAB" : "#D3D3D3";
|
|
|
- enginName =
|
|
|
- turbine.enginCode === fieldEngineCode ? turbine.enginName : "";
|
|
|
- const chartConfig = {
|
|
|
- x: turbine.xData,
|
|
|
- y: turbine.yData,
|
|
|
- connectgaps: false,
|
|
|
- name: turbine.enginName,
|
|
|
- mode: "lines",
|
|
|
- fill: "none",
|
|
|
- line: { color },
|
|
|
- marker: { color },
|
|
|
- };
|
|
|
- finalData.push(chartConfig);
|
|
|
- });
|
|
|
- const filterData = data.data.filter(
|
|
|
- (item) => item.enginName === "合同功率曲线",
|
|
|
- );
|
|
|
- // 添加合同功率曲线
|
|
|
- if (filterData && filterData[0].enginName === "合同功率曲线") {
|
|
|
- finalData.push({
|
|
|
- x: filterData[0].xData,
|
|
|
- y: filterData[0].yData,
|
|
|
+ let targetEngineName = "";
|
|
|
+ const traces = sortedData
|
|
|
+ .map((turbine) => {
|
|
|
+ const isTarget = getEngineCode(turbine) === fieldEngineCode;
|
|
|
+ if (isTarget) targetEngineName = getEngineName(turbine);
|
|
|
+
|
|
|
+ const pairs = buildValidPairs(turbine.xData, turbine.yData);
|
|
|
+ if (!pairs.length) return null;
|
|
|
+
|
|
|
+ const color = isTarget ? "#406DAB" : "#D3D3D3";
|
|
|
+ return {
|
|
|
+ x: pairs.map((pair) => pair.x),
|
|
|
+ y: pairs.map((pair) => pair.y),
|
|
|
+ connectgaps: false,
|
|
|
+ name: getEngineName(turbine),
|
|
|
+ type: "scatter",
|
|
|
+ mode: pairs.length === 1 ? "markers" : "lines",
|
|
|
+ fill: "none",
|
|
|
+ line: { color, width: isTarget ? 2 : 1 },
|
|
|
+ marker: { color, size: pairs.length === 1 ? 7 : 4 },
|
|
|
+ };
|
|
|
+ })
|
|
|
+ .filter(Boolean);
|
|
|
+
|
|
|
+ const contractLine = data.data.find(isContractLine);
|
|
|
+ if (contractLine) {
|
|
|
+ const pairs = buildValidPairs(contractLine.xData, contractLine.yData);
|
|
|
+ if (pairs.length) {
|
|
|
+ traces.push({
|
|
|
+ x: pairs.map((pair) => pair.x),
|
|
|
+ y: pairs.map((pair) => pair.y),
|
|
|
+ type: "scatter",
|
|
|
mode: "lines+markers",
|
|
|
name: "合同功率曲线",
|
|
|
- line: {
|
|
|
- color: "red",
|
|
|
- width: 1,
|
|
|
- },
|
|
|
+ line: { color: "red", width: 1 },
|
|
|
marker: { color: "red", size: 4 },
|
|
|
});
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- // 准备布局配置
|
|
|
- const layout = {
|
|
|
- title: {
|
|
|
- text: `机组 ${enginName}风速功率曲线分析`,
|
|
|
- font: {
|
|
|
- size: 16,
|
|
|
- weight: "bold",
|
|
|
- },
|
|
|
- },
|
|
|
- xaxis: {
|
|
|
- title: {
|
|
|
- text: "风速(m/s)" || "x轴",
|
|
|
- },
|
|
|
- gridcolor: "rgb(255,255,255)",
|
|
|
- tickcolor: "rgb(255,255,255)",
|
|
|
- backgroundcolor: "#e5ecf6",
|
|
|
- showbackground: true,
|
|
|
- showline: true, // ✅ 显示 X 轴轴线
|
|
|
- linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
|
|
|
- zeroline: false,
|
|
|
- },
|
|
|
- yaxis: {
|
|
|
- title: { text: "功率(kW)" || "Y轴" },
|
|
|
- gridcolor: "rgb(255,255,255)",
|
|
|
- tickcolor: "rgb(255,255,255)",
|
|
|
- backgroundcolor: "#e5ecf6",
|
|
|
- showbackground: true,
|
|
|
- showline: true, // ✅ 显示 X 轴轴线
|
|
|
- zeroline: false,
|
|
|
- linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
|
|
|
- },
|
|
|
- margin: {
|
|
|
- l: 50,
|
|
|
- r: 50,
|
|
|
- t: 50,
|
|
|
- b: 50,
|
|
|
- },
|
|
|
- autosize: true,
|
|
|
- plot_bgcolor: "#e5ecf6",
|
|
|
- gridcolor: "#fff",
|
|
|
- bgcolor: "#e5ecf6",
|
|
|
- legend: {
|
|
|
- orientation: "h",
|
|
|
- xanchor: "center",
|
|
|
- x: 0.5,
|
|
|
- y: -0.2,
|
|
|
- },
|
|
|
- };
|
|
|
- // 创建HTML内容
|
|
|
- const htmlContent = `
|
|
|
- <!DOCTYPE html>
|
|
|
- <html>
|
|
|
- <head>
|
|
|
- <script>${plotlyContent}</script>
|
|
|
- <style>
|
|
|
- body { margin: 0; }
|
|
|
- #chart { width: 800px; height: 600px; }
|
|
|
- </style>
|
|
|
- </head>
|
|
|
- <body>
|
|
|
- <div id="chart"></div>
|
|
|
- <script>
|
|
|
- window.onload = function() {
|
|
|
- const data = ${JSON.stringify(finalData)};
|
|
|
- const layout = ${JSON.stringify(layout)};
|
|
|
- Plotly.newPlot('chart', data, layout, {
|
|
|
- responsive: true
|
|
|
- }).then(() => {
|
|
|
- window.chartRendered = true;
|
|
|
- });
|
|
|
- };
|
|
|
- </script>
|
|
|
- </body>
|
|
|
- </html>
|
|
|
- `;
|
|
|
-
|
|
|
- // 设置页面内容
|
|
|
- await page.setContent(htmlContent, {
|
|
|
- waitUntil: "networkidle0",
|
|
|
- });
|
|
|
-
|
|
|
- // 等待图表渲染完成
|
|
|
- await page.waitForFunction(() => window.chartRendered === true, {
|
|
|
- timeout: 60000,
|
|
|
- });
|
|
|
+ if (!traces.length) {
|
|
|
+ throw new Error("PlotlyChartsFen has no valid points");
|
|
|
+ }
|
|
|
|
|
|
- // 截图并保存到临时文件
|
|
|
- // 截图并保存到临时文件
|
|
|
- const chartElement = await page.$("#chart");
|
|
|
- await chartElement.screenshot({
|
|
|
- path: tempFilePath,
|
|
|
- type: "jpeg",
|
|
|
- });
|
|
|
+ const layout = {
|
|
|
+ title: {
|
|
|
+ text:
|
|
|
+ data.title ||
|
|
|
+ `机组 ${targetEngineName || fieldEngineCode || ""}风速功率曲线分析`,
|
|
|
+ font: { size: 16 },
|
|
|
+ },
|
|
|
+ xaxis: {
|
|
|
+ title: data.xaixs || "风速(m/s)",
|
|
|
+ gridcolor: "rgb(255,255,255)",
|
|
|
+ tickcolor: "rgb(255,255,255)",
|
|
|
+ backgroundcolor: "#e5ecf6",
|
|
|
+ showbackground: true,
|
|
|
+ showline: true,
|
|
|
+ linecolor: "#ffffff",
|
|
|
+ zeroline: false,
|
|
|
+ },
|
|
|
+ yaxis: {
|
|
|
+ title: data.yaixs || "功率(kW)",
|
|
|
+ gridcolor: "rgb(255,255,255)",
|
|
|
+ tickcolor: "rgb(255,255,255)",
|
|
|
+ backgroundcolor: "#e5ecf6",
|
|
|
+ showbackground: true,
|
|
|
+ showline: true,
|
|
|
+ zeroline: false,
|
|
|
+ linecolor: "#ffffff",
|
|
|
+ },
|
|
|
+ margin: { l: 50, r: 50, t: 50, b: 50 },
|
|
|
+ autosize: true,
|
|
|
+ plot_bgcolor: "#e5ecf6",
|
|
|
+ paper_bgcolor: "#e5ecf6",
|
|
|
+ legend: {
|
|
|
+ orientation: "h",
|
|
|
+ xanchor: "center",
|
|
|
+ x: 0.5,
|
|
|
+ y: -0.2,
|
|
|
+ },
|
|
|
+ };
|
|
|
|
|
|
- // 上传图片到服务器
|
|
|
- const formData = new FormData();
|
|
|
- formData.append("file", fs.createReadStream(tempFilePath));
|
|
|
- // return formData;
|
|
|
- // 发送上传请求
|
|
|
- const response = await axios.post(
|
|
|
- `${process.env.API_BASE_URL}/examples/upload`,
|
|
|
- { filePath: tempFilePath, bucketName, objectName },
|
|
|
- );
|
|
|
- return response?.data?.url;
|
|
|
- } finally {
|
|
|
- await browser.close();
|
|
|
- }
|
|
|
+ return await renderChart({
|
|
|
+ traces,
|
|
|
+ layout,
|
|
|
+ bucketName,
|
|
|
+ objectName,
|
|
|
+ });
|
|
|
} catch (error) {
|
|
|
console.error("生成折线图失败:", error);
|
|
|
throw error;
|