Bladeren bron

修改下载报告方式

liujiejie 3 weken geleden
bovenliggende
commit
f8745e2c68
41 gewijzigde bestanden met toevoegingen van 2559 en 3369 verwijderingen
  1. 4 3
      downLoadServer/.env
  2. BIN
      downLoadServer/src.zip
  3. 36 23
      downLoadServer/src/server/controllers/uploadController.js
  4. 72 23
      downLoadServer/src/server/server.js
  5. 95 0
      downLoadServer/src/server/utils/chartService/browserPool.js
  6. 85 0
      downLoadServer/src/server/utils/chartService/chartRenderer.js
  7. 31 0
      downLoadServer/src/server/utils/chartService/fileManager.js
  8. 247 0
      downLoadServer/src/server/utils/chartService/index.js
  9. 11 0
      downLoadServer/src/server/utils/chartService/limiter.js
  10. 22 0
      downLoadServer/src/server/utils/chartService/plotlyLoader.js
  11. 37 0
      downLoadServer/src/server/utils/chartService/uploadService.js
  12. 103 0
      downLoadServer/src/server/utils/chartService/uploader.js
  13. 15 14
      downLoadServer/src/server/utils/chartsCom/3DDrawingChart.js
  14. 129 209
      downLoadServer/src/server/utils/chartsCom/BarChart.js
  15. 90 171
      downLoadServer/src/server/utils/chartsCom/BoxLineCharts.js
  16. 72 116
      downLoadServer/src/server/utils/chartsCom/BoxMarkersCharts.js
  17. 97 219
      downLoadServer/src/server/utils/chartsCom/ColorbarInitTwoDmarkersChart.js
  18. 85 176
      downLoadServer/src/server/utils/chartsCom/FaultAll.js
  19. 86 161
      downLoadServer/src/server/utils/chartsCom/FaultUnit.js
  20. 115 242
      downLoadServer/src/server/utils/chartsCom/GeneratorTemperature.js
  21. 93 169
      downLoadServer/src/server/utils/chartsCom/HeatmapCharts.js
  22. 93 151
      downLoadServer/src/server/utils/chartsCom/PlotlyCharts.js
  23. 11 16
      downLoadServer/src/server/utils/chartsCom/PlotlyChartsFen.js
  24. 13 17
      downLoadServer/src/server/utils/chartsCom/Time3DChart.js
  25. 111 204
      downLoadServer/src/server/utils/chartsCom/TwoDMarkersChart.js
  26. 132 195
      downLoadServer/src/server/utils/chartsCom/TwoDMarkersChart1.js
  27. 49 139
      downLoadServer/src/server/utils/chartsCom/WindRoseChart.js
  28. 56 157
      downLoadServer/src/server/utils/chartsCom/YewErrorBarChart.js
  29. 103 194
      downLoadServer/src/server/utils/chartsCom/lineAndChildLine.js
  30. 125 176
      downLoadServer/src/server/utils/chartsCom/lineChartsFen.js
  31. 120 252
      downLoadServer/src/server/utils/chartsCom/powerMarkers2DCharts.js
  32. 51 144
      downLoadServer/src/server/utils/chartsCom/yawErrorBarSum.js
  33. 73 167
      downLoadServer/src/server/utils/chartsCom/yawErrorLine.js
  34. 97 31
      downLoadServer/src/server/utils/minioService.js
  35. BIN
      downLoadServer/temp-images/image_1773380356597_0.webp
  36. BIN
      downLoadServer/temp-images/image_1773380356597_1.webp
  37. BIN
      downLoadServer/temp-images/image_1773380356604_0.webp
  38. BIN
      downLoadServer/temp-images/image_1773381971053_0.webp
  39. BIN
      downLoadServer/temp-images/image_1773381971053_1.webp
  40. BIN
      downLoadServer/temp-images/image_1773381971056_0.webp
  41. BIN
      downLoadServer/temp-images/images_1773380356605.zip

+ 4 - 3
downLoadServer/.env

@@ -9,11 +9,12 @@ API_BASE_URL=http://0.0.0.0:3001
 
 MINIO_ENDPOINT=192.168.50.234 #minio 生产
 MINIO_PORT=6900 #minio 生产
-MINIO_ACCESS_KEY=6VkF2ul6X7udr7RLsG2W,
-MINIO_SECRET_KEY=jtBuqZ80biRWQf6sbwzDQJwHtEBicPjkZBtvjTrA,
+MINIO_ACCESS_KEY=6VkF2ul6X7udr7RLsG2W
+MINIO_SECRET_KEY=jtBuqZ80biRWQf6sbwzDQJwHtEBicPjkZBtvjTrA
 # MINIO_ACCESS_KEY=haH1vePq7unSp4TG1One
 # MINIO_SECRET_KEY=idxO5SAjboUYERpDICgHgBoHX7bcYv355lMQANt6
-# CHROME_PATH=/Applications/Google Chrome.app/Contents/MacOS/Google Chrome
+CHROME_PATH=/Applications/Google Chrome.app/Contents/MacOS/Google Chrome
+CHART_RENDER_TIMEOUT_MS=180000
 #   nginx 配置 minio
 #   env MINIO_ENDPOINT=192.168.50.233;
 #   env MINIO_PORT=6900;

BIN
downLoadServer/src.zip


+ 36 - 23
downLoadServer/src/server/controllers/uploadController.js

@@ -1,14 +1,14 @@
 /*
  * @Author: your name
  * @Date: 2025-04-28 14:46:54
- * @LastEditTime: 2026-03-14 10:15:41
- * @LastEditors: milo-MacBook-Pro.local
+ * @LastEditTime: 2026-03-17 15:42:47
+ * @LastEditors: bogon
  * @Description: In User Settings Edit
  * @FilePath: /downLoadServer/src/server/controllers/uploadController.js
  */
 // src/server/controllers/uploadController.js
 import multer from "multer";
-import { uploadFileToMinIO } from "../utils/minioService.js";
+import { uploadBufferToMinIO } from "../utils/minioService.js";
 import path from "path";
 import fs from "fs-extra";
 import { fileURLToPath } from "url";
@@ -31,33 +31,46 @@ const upload = multer({ storage });
 
 // 上传文件的控制器
 export const uploadFile = async (req, res) => {
-  const bucketName = req.body.bucketName; // 您的桶名称
-  const filePath = req.body.filePath; // 从 req.body 中获取文件路径
-  const objectName = req.body.objectName; // 在 MinIO 中的文件名
-  // 检查 filePath 是否有效
-  if (typeof filePath !== "string" || !filePath) {
-    return res.status(400).json({ message: "Invalid file path" });
-  }
-  try {
-    // 确保文件存在
-    await fs.access(filePath);
-    // 调用 uploadFileToMinIO 函数上传文件
-    await uploadFileToMinIO(bucketName, filePath, objectName);
-    // 构造文件的 URL
-    const url = `http://${process.env.MINIO_ENDPOINT}:${process.env.MINIO_PORT}/${bucketName}/${objectName}`;
-    // 删除临时文件
-    if (filePath) {
-      await fs.unlink(filePath);
-    } else {
-      console.error("filePath is undefined, cannot unlink");
-    }
+  const { bucketName, filePath, objectName } = req.body;
 
+  try {
+    const buffer = await fs.promises.readFile(filePath); // ✅ 关键
+    const url = await uploadBufferToMinIO(bucketName, objectName, buffer);
+    await fs.promises.unlink(filePath); // 删除临时文件
     res.status(200).json({ url, message: "File uploaded successfully" });
   } catch (error) {
     console.error("File upload failed:", error);
     res.status(500).json({ message: "File upload failed", error });
   }
 };
+// export const uploadFile = async (req, res) => {
+//   const bucketName = req.body.bucketName; // 您的桶名称
+//   const filePath = req.body.filePath; // 从 req.body 中获取文件路径
+//   const objectName = req.body.objectName; // 在 MinIO 中的文件名
+//   // 检查 filePath 是否有效
+//   if (typeof filePath !== "string" || !filePath) {
+//     return res.status(400).json({ message: "Invalid file path" });
+//   }
+//   try {
+//     // 确保文件存在
+//     await fs.access(filePath);
+//     // 调用 uploadFileToMinIO 函数上传文件
+//     await uploadBufferToMinIO(bucketName, filePath, objectName);
+//     // 构造文件的 URL
+//     const url = `http://${process.env.MINIO_ENDPOINT}:${process.env.MINIO_PORT}/${bucketName}/${objectName}`;
+//     // 删除临时文件
+//     if (filePath) {
+//       await fs.unlink(filePath);
+//     } else {
+//       console.error("filePath is undefined, cannot unlink");
+//     }
+
+//     res.status(200).json({ url, message: "File uploaded successfully" });
+//   } catch (error) {
+//     console.error("File upload failed:", error);
+//     res.status(500).json({ message: "File upload failed", error });
+//   }
+// };
 export const uploadMiddleware = upload.single("file");
 
 export const downloadFile = async (req, res) => {

+ 72 - 23
downLoadServer/src/server/server.js

@@ -1,48 +1,97 @@
 import dotenv from "dotenv";
-// 加载 .env 文件中的环境变量
 dotenv.config();
+
 import express from "express";
+import cors from "cors";
+import path from "path";
+import { fileURLToPath } from "url";
+
 import { serverConfig } from "./config.js";
 import { logger } from "./middleware/logger.js";
 import { errorHandler } from "./middleware/errorHandler.js";
+
 import exampleRoutes from "./routes/exampleRoutes.js";
 import chartRoutes from "./routes/chartRoutes.js";
-import path from "path";
-import { fileURLToPath } from "url";
-// 引入 cors 模块
-import cors from "cors";
+
+import {
+  initChartService,
+  shutdownChartService,
+} from "./utils/chartService/index.js";
 
 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);
 
 const app = express();
-// 使用 cors 中间件
+
+/**
+ * =========================
+ * 中间件
+ * =========================
+ */
 app.use(cors());
-// 中间件
-app.use(express.json({ limit: "100mb" }));
+app.use(express.json({ limit: "10mb" }));
 app.use(logger);
 
-// 静态文件服务
-app.use("/images", express.static(path.join(process.cwd(), "src", "images")));
-app.use("/js", express.static(path.join(process.cwd(), "src", "public", "js")));
-app.use(
-  "/testData.json",
-  express.static(path.join(process.cwd(), "src", "testData.json"))
-);
+/**
+ * =========================
+ * 静态资源
+ * =========================
+ */
+app.use("/js", express.static(path.join(process.cwd(), "src/public/js")));
 
-// 路由
+/**
+ * =========================
+ * 路由
+ * =========================
+ */
 app.use("/examples", exampleRoutes);
 app.use("/chartServer/charts", chartRoutes);
 
-// 错误处理
+/**
+ * =========================
+ * 错误处理
+ * =========================
+ */
 app.use(errorHandler);
 
-export const startServer = () => {
-  app.listen(serverConfig.port, serverConfig.host, () => {
-    console.log(
-      `Server is running on http://${serverConfig.host}:${serverConfig.port}`
-    );
-  });
+/**
+ * =========================
+ * 启动服务
+ * =========================
+ */
+export const startServer = async () => {
+  try {
+    await initChartService(); // ✅统一入口
+
+    app.listen(serverConfig.port, serverConfig.host, () => {
+      console.log(
+        `🚀 Server running at http://${serverConfig.host}:${serverConfig.port}`,
+      );
+    });
+  } catch (err) {
+    console.error("❌ Server start failed:", err);
+    process.exit(1);
+  }
+};
+
+/**
+ * =========================
+ * 优雅退出
+ * =========================
+ */
+const shutdown = async () => {
+  console.log("🛑 Shutting down server...");
+
+  try {
+    await shutdownChartService();
+  } catch (err) {
+    console.error("Shutdown error:", err);
+  }
+
+  process.exit(0);
 };
 
+process.on("SIGINT", shutdown);
+process.on("SIGTERM", shutdown);
+
 export default app;

+ 95 - 0
downLoadServer/src/server/utils/chartService/browserPool.js

@@ -0,0 +1,95 @@
+import puppeteer from "puppeteer";
+
+let browser;
+const pagePool = [];
+const MAX_PAGES = 5; // 并发上限(可以调)
+const DEFAULT_TIMEOUT_MS = Number.parseInt(
+  process.env.CHART_RENDER_TIMEOUT_MS || "",
+  10,
+);
+const EFFECTIVE_TIMEOUT_MS = Number.isFinite(DEFAULT_TIMEOUT_MS)
+  ? DEFAULT_TIMEOUT_MS
+  : 500000;
+
+/**
+ * 初始化浏览器 + 预创建 page
+ */
+export const initBrowserPool = async () => {
+  if (!browser) {
+    browser = await puppeteer.launch({
+      headless: "new",
+      executablePath: process.env.CHROME_PATH || undefined,
+      args: ["--no-sandbox", "--disable-setuid-sandbox"],
+      // 避免渲染压力下 CDP 调用超时(对应报错:Runtime.callFunctionOn timed out)
+      protocolTimeout: EFFECTIVE_TIMEOUT_MS,
+    });
+
+    console.log("✅ Browser initialized");
+
+    // 预创建 page
+    for (let i = 0; i < MAX_PAGES; i++) {
+      const page = await browser.newPage();
+      page.setDefaultTimeout(EFFECTIVE_TIMEOUT_MS);
+      page.setDefaultNavigationTimeout(EFFECTIVE_TIMEOUT_MS);
+      pagePool.push(page);
+    }
+
+    console.log(`✅ Page pool initialized: ${MAX_PAGES}`);
+  }
+};
+
+/**
+ * 获取 page(核心)
+ */
+export const getPage = async () => {
+  if (!browser) {
+    throw new Error("Browser not initialized");
+  }
+
+  if (pagePool.length > 0) {
+    return pagePool.pop();
+  }
+
+  // pool 空了就新建(兜底)
+  const page = await browser.newPage();
+  page.setDefaultTimeout(EFFECTIVE_TIMEOUT_MS);
+  page.setDefaultNavigationTimeout(EFFECTIVE_TIMEOUT_MS);
+  return page;
+};
+
+/**
+ * 释放 page
+ */
+export const releasePage = async (page) => {
+  if (!page) return;
+
+  try {
+    if (pagePool.length < MAX_PAGES) {
+      pagePool.push(page);
+    } else {
+      await page.close();
+    }
+  } catch (err) {
+    console.error("❌ releasePage error:", err);
+  }
+};
+
+/**
+ * 关闭
+ */
+export const closeBrowserPool = async () => {
+  try {
+    for (const page of pagePool) {
+      await page.close();
+    }
+
+    if (browser) {
+      await browser.close();
+      browser = null;
+    }
+
+    console.log("🛑 Browser pool closed");
+  } catch (err) {
+    console.error("❌ closeBrowserPool error:", err);
+  }
+};

+ 85 - 0
downLoadServer/src/server/utils/chartService/chartRenderer.js

@@ -0,0 +1,85 @@
+import { getPage, releasePage } from "./browserPool.js";
+import { getPlotly } from "./plotlyLoader.js";
+
+export const renderToBuffer = async (data, layout) => {
+  let page;
+
+  try {
+    const timeoutMs = Number.isFinite(
+      Number.parseInt(process.env.CHART_RENDER_TIMEOUT_MS || "", 10),
+    )
+      ? Number.parseInt(process.env.CHART_RENDER_TIMEOUT_MS || "", 10)
+      : 500000;
+    page = await getPage(); // 🔹 从 page 池获取
+    page.setDefaultTimeout(timeoutMs);
+    page.setDefaultNavigationTimeout(timeoutMs);
+    const plotly = getPlotly();
+
+    const html = `
+      <html>
+        <head>
+          <script>${plotly}</script>
+        </head>
+        <body>
+          <div id="chart" style="width:800px;height:600px;"></div>
+          <script>
+            Plotly.newPlot('chart', ${JSON.stringify(data)}, ${JSON.stringify(
+      layout,
+    )})
+            .then(() => { window.chartRendered = true; });
+          </script>
+        </body>
+      </html>
+    `;
+
+    await page.setContent(html, {
+      waitUntil: "domcontentloaded",
+      timeout: timeoutMs,
+    });
+    await page.waitForFunction(() => window.chartRendered === true, {
+      timeout: timeoutMs,
+    });
+    return await page.screenshot({ type: "jpeg" });
+  } finally {
+    if (page) await releasePage(page); // 🔹 使用 releasePage 回收
+  }
+};
+
+export function buildHTML(traces, layout) {
+  const plotlyContent = getPlotly();
+
+  return `
+    <!DOCTYPE html>
+    <html>
+      <head>
+        <meta charset="UTF-8" />
+        <title>Chart</title>
+        <script>${plotlyContent}</script>
+        <style>
+          body { margin: 0; background: #e5ecf6; }
+          #chart { width: 800px; height: 600px; }
+        </style>
+      </head>
+      <body>
+        <div id="chart"></div>
+        <script>
+          (function() {
+            try {
+              const traces = ${JSON.stringify(traces)};
+              const layout = ${JSON.stringify(layout)};
+              Plotly.newPlot('chart', traces, layout, {
+                responsive: true,
+                displayModeBar: false,
+                staticPlot: true
+              })
+              .then(() => { window.chartReady = true; })
+              .catch(() => { window.chartError = true; });
+            } catch {
+              window.chartError = true;
+            }
+          })();
+        </script>
+      </body>
+    </html>
+  `;
+}

+ 31 - 0
downLoadServer/src/server/utils/chartService/fileManager.js

@@ -0,0 +1,31 @@
+/*
+ * @Author: your name
+ * @Date: 2026-03-17 11:49:34
+ * @LastEditTime: 2026-03-17 11:49:42
+ * @LastEditors: bogon
+ * @Description: In User Settings Edit
+ * @FilePath: /performance-test/downLoadServer/src/server/utils/chartService/fileManager.js
+ */
+import path from "path";
+import fs from "fs-extra";
+import { randomUUID } from "crypto";
+
+const tempDir = path.join(process.cwd(), "tmp/charts");
+
+export async function initTempDir() {
+  await fs.ensureDir(tempDir);
+}
+
+export function createTempImagePath() {
+  return path.join(tempDir, `chart_${Date.now()}_${randomUUID()}.jpeg`);
+}
+
+export async function safeRemove(filePath) {
+  try {
+    await fs.remove(filePath);
+  } catch (e) {
+    if (e.code !== "ENOENT") {
+      console.error("remove file error:", e);
+    }
+  }
+}

+ 247 - 0
downLoadServer/src/server/utils/chartService/index.js

@@ -0,0 +1,247 @@
+// /*
+//  * Chart Service - 生产级版本
+//  */
+
+// import { getPage, releasePage } from "./browserPool.js";
+// import { buildHTML } from "./chartRenderer.js";
+// import { uploadImage } from "./uploader.js";
+// import { chartLimit } from "./limiter.js";
+// import { initBrowserPool, closeBrowserPool } from "./browserPool.js";
+// import { initPlotly } from "./plotlyLoader.js";
+
+// /**
+//  * 初始化
+//  */
+// let initialized = false;
+
+// export const initChartService = async () => {
+//   if (initialized) return;
+
+//   await initBrowserPool();
+//   await initPlotly();
+
+//   initialized = true;
+
+//   console.log("🚀 Chart service ready");
+// };
+
+// /**
+//  * 关闭
+//  */
+// export const shutdownChartService = async () => {
+//   try {
+//     await closeBrowserPool();
+//     initialized = false;
+//     console.log("🛑 Chart service shutdown complete");
+//   } catch (err) {
+//     console.error("❌ Shutdown error:", err);
+//   }
+// };
+
+// /**
+//  * 核心渲染函数(带重试)
+//  */
+// export async function renderChart({
+//   traces,
+//   layout,
+//   bucketName,
+//   objectName,
+//   retry = 2, // 默认重试2次
+// }) {
+//   return chartLimit(async () => {
+//     let attempt = 0;
+
+//     while (attempt <= retry) {
+//       const page = await getPage();
+
+//       try {
+//         const html = buildHTML(traces, layout);
+
+//         /**
+//          * ✅ 重置页面(关键)
+//          */
+//         await page.goto("about:blank");
+
+//         await page.setContent(html, {
+//           waitUntil: "domcontentloaded",
+//         });
+
+//         /**
+//          * ✅ 等待渲染完成(带超时)
+//          */
+//         await Promise.race([
+//           page.waitForFunction(() => window.chartReady === true),
+//           page.waitForTimeout(30000),
+//         ]);
+
+//         /**
+//          * ✅ 检查错误
+//          */
+//         const hasError = await page.evaluate(() => {
+//           return window.chartError || false;
+//         });
+
+//         if (hasError) {
+//           throw new Error("Chart render failed in browser");
+//         }
+
+//         /**
+//          * ✅ 获取元素
+//          */
+//         const element = await page.$("#chart");
+
+//         if (!element) {
+//           throw new Error("Chart element not found");
+//         }
+
+//         /**
+//          * ✅ 截图
+//          */
+//         const buffer = await element.screenshot({
+//           type: "jpeg",
+//           quality: 90,
+//         });
+
+//         /**
+//          * ✅ 上传
+//          */
+//         const url = await uploadImage(buffer, bucketName, objectName);
+
+//         return url;
+//       } catch (err) {
+//         attempt++;
+
+//         console.error(
+//           `❌ renderChart error (attempt ${attempt}):`,
+//           err.message,
+//         );
+
+//         if (attempt > retry) {
+//           throw err;
+//         }
+
+//         // 👉 小延迟再重试(防抖)
+//         await new Promise((r) => setTimeout(r, 1000));
+//       } finally {
+//         try {
+//           if (page && !page.isClosed()) {
+//             await releasePage(page); // 只放回健康 page
+//           }
+//         } catch (err) {
+//           console.warn("page release failed, closing:", err);
+//           if (page && !page.isClosed()) await page.close();
+//         }
+//       }
+//     }
+//   });
+// }
+// src/server/utils/chartService/index.js
+import { getPage, releasePage } from "./browserPool.js";
+import { buildHTML } from "./chartRenderer.js";
+import { chartLimit } from "./limiter.js";
+import { initBrowserPool, closeBrowserPool } from "./browserPool.js";
+import { initPlotly } from "./plotlyLoader.js";
+import { uploadBufferToMinIO } from "../minioService.js";
+
+let initialized = false;
+
+/**
+ * 初始化 chart 服务
+ */
+export const initChartService = async () => {
+  if (initialized) return;
+  await initBrowserPool();
+  await initPlotly();
+  initialized = true;
+  console.log("🚀 Chart service ready");
+};
+
+/**
+ * 关闭 chart 服务
+ */
+export const shutdownChartService = async () => {
+  try {
+    await closeBrowserPool();
+    initialized = false;
+    console.log("🛑 Chart service shutdown complete");
+  } catch (err) {
+    console.error("❌ Shutdown error:", err);
+  }
+};
+
+/**
+ * 核心渲染函数(带重试 + MinIO 上传)
+ * @param {Array} traces
+ * @param {Object} layout
+ * @param {string} bucketName
+ * @param {string} objectName
+ * @param {number} retry
+ */
+export async function renderChart({
+  traces,
+  layout,
+  bucketName,
+  objectName,
+  retry = 2,
+}) {
+  return chartLimit(async () => {
+    let attempt = 0;
+    const timeoutMs = Number.isFinite(
+      Number.parseInt(process.env.CHART_RENDER_TIMEOUT_MS || "", 10),
+    )
+      ? Number.parseInt(process.env.CHART_RENDER_TIMEOUT_MS || "", 10)
+      : 120000;
+
+    while (attempt <= retry) {
+      const page = await getPage();
+
+      try {
+        const html = buildHTML(traces, layout);
+
+        // 重置页面
+        await page.goto("about:blank");
+        await page.setContent(html, {
+          waitUntil: "domcontentloaded",
+          timeout: timeoutMs,
+        });
+
+        // 等待渲染完成
+        await Promise.race([
+          page.waitForFunction(() => window.chartReady === true, {
+            timeout: timeoutMs,
+          }),
+          page.waitForTimeout(timeoutMs),
+        ]);
+
+        // 检查错误
+        const hasError = await page.evaluate(() => window.chartError || false);
+        if (hasError) throw new Error("Chart render failed in browser");
+
+        const element = await page.$("#chart");
+        if (!element) throw new Error("Chart element not found");
+
+        // 截图
+        const buffer = await element.screenshot({ type: "jpeg", quality: 90 });
+
+        // 上传到 MinIO
+        const url = await uploadBufferToMinIO(bucketName, objectName, buffer);
+        return url;
+      } catch (err) {
+        attempt++;
+        console.error(
+          `❌ renderChart error (attempt ${attempt}):`,
+          err.message,
+        );
+        if (attempt > retry) throw err;
+        await new Promise((r) => setTimeout(r, 1000));
+      } finally {
+        try {
+          if (page && !page.isClosed()) await releasePage(page);
+        } catch (err) {
+          console.warn("page release failed, closing:", err);
+          if (page && !page.isClosed()) await page.close();
+        }
+      }
+    }
+  });
+}

+ 11 - 0
downLoadServer/src/server/utils/chartService/limiter.js

@@ -0,0 +1,11 @@
+/*
+ * @Author: your name
+ * @Date: 2026-03-17 11:50:34
+ * @LastEditTime: 2026-03-17 11:50:35
+ * @LastEditors: bogon
+ * @Description: In User Settings Edit
+ * @FilePath: /performance-test/downLoadServer/src/server/utils/chartService/limiter.js
+ */
+import pLimit from "p-limit";
+
+export const chartLimit = pLimit(4);

+ 22 - 0
downLoadServer/src/server/utils/chartService/plotlyLoader.js

@@ -0,0 +1,22 @@
+import fs from "fs-extra";
+import path from "path";
+
+let plotlyContent = null;
+
+// 初始化 Plotly 内容
+export const initPlotly = async () => {
+  const plotlyPath = path.join(
+    process.cwd(),
+    "src/public/js/plotly-latest.min.js",
+  );
+  plotlyContent = await fs.readFile(plotlyPath, "utf-8");
+  console.log("✅ Plotly loaded");
+};
+
+// 获取 Plotly 内容
+export const getPlotly = () => {
+  if (!plotlyContent) {
+    throw new Error("Plotly not initialized, call initPlotly() first");
+  }
+  return plotlyContent;
+};

+ 37 - 0
downLoadServer/src/server/utils/chartService/uploadService.js

@@ -0,0 +1,37 @@
+/*
+ * @Author: your name
+ * @Date: 2026-03-17 13:23:29
+ * @LastEditTime: 2026-03-17 13:23:30
+ * @LastEditors: bogon
+ * @Description: In User Settings Edit
+ * @FilePath: /performance-test/downLoadServer/src/server/utils/chartService/uploadService1.js
+ */
+import FormData from "form-data";
+import axios from "axios";
+
+export const uploadBuffer = async (
+  buffer,
+  bucketName,
+  objectName,
+  extra = {},
+) => {
+  const formData = new FormData();
+
+  formData.append("file", buffer, "chart.jpg");
+  formData.append("bucketName", bucketName);
+  formData.append("objectName", objectName);
+
+  Object.keys(extra).forEach((key) => {
+    formData.append(key, extra[key]);
+  });
+
+  const res = await axios.post(
+    `${process.env.API_BASE_URL}/examples/upload`,
+    formData,
+    {
+      headers: formData.getHeaders(),
+    },
+  );
+
+  return res.data.url;
+};

+ 103 - 0
downLoadServer/src/server/utils/chartService/uploader.js

@@ -0,0 +1,103 @@
+// // /*
+// //  * @Author: your name
+// //  * @Date: 2026-03-17 11:50:22
+// //  * @LastEditTime: 2026-03-17 13:23:48
+// //  * @LastEditors: bogon
+// //  * @Description: In User Settings Edit
+// //  * @FilePath: /performance-test/downLoadServer/src/server/utils/chartService/uploader.js
+// //  */
+// // import axios from "axios";
+
+// // export async function uploadImage(filePath, bucketName, objectName) {
+// //   const response = await axios.post(
+// //     `${process.env.API_BASE_URL}/examples/upload`,
+// //     {
+// //       filePath,
+// //       bucketName,
+// //       objectName,
+// //     },
+// //   );
+
+// //   return response?.data?.url;
+// // }
+// // uploader.js
+// import axios from "axios";
+// import FormData from "form-data";
+
+// /**
+//  * 上传图片 buffer 到服务器
+//  * @param {Buffer} buffer - 生成的图表图片
+//  * @param {string} bucketName - 存储桶名称
+//  * @param {string} objectName - 文件名
+//  * @returns {Promise<string>} 返回上传后的 URL
+//  */
+// export const uploadImage = async (buffer, bucketName, objectName) => {
+//   try {
+//     const form = new FormData();
+
+//     // 添加文件字段
+//     form.append("file", buffer, {
+//       filename: objectName, // 上传文件名
+//       contentType: "image/jpeg", // 上传内容类型
+//     });
+
+//     // 如果服务端需要 bucketName,也可以放在表单字段里
+//     form.append("bucketName", bucketName);
+
+//     const response = await axios.post(
+//       "http://0.0.0.0:3001/examples/upload",
+//       form,
+//       {
+//         headers: {
+//           ...form.getHeaders(), // FormData 必须的 headers
+//         },
+//         maxBodyLength: Infinity, // 防止大文件被限制
+//       },
+//     );
+
+//     // 假设服务端返回 JSON 中包含 url 字段
+//     return response.data.url;
+//   } catch (err) {
+//     console.error("❌ Error uploading file to server:", err.message);
+//     throw err;
+//   }
+// };
+// src/server/utils/uploader.js
+import axios from "axios";
+import FormData from "form-data";
+
+/**
+ * 上传图片 buffer 到 HTTP 服务
+ * @param {Buffer} buffer - 生成的图表图片
+ * @param {string} bucketName - 存储桶名称
+ * @param {string} objectName - 文件名
+ * @returns {Promise<string>} 返回上传后的 URL
+ */
+export const uploadImage = async (buffer, bucketName, objectName) => {
+  try {
+    const form = new FormData();
+
+    form.append("file", buffer, {
+      filename: objectName,
+      contentType: "image/jpeg",
+    });
+
+    form.append("bucketName", bucketName);
+
+    const response = await axios.post(
+      "http://0.0.0.0:3001/examples/upload",
+      form,
+      {
+        headers: {
+          ...form.getHeaders(),
+        },
+        maxBodyLength: Infinity,
+      },
+    );
+
+    return response.data.url;
+  } catch (err) {
+    console.error("❌ Error uploading file to server:", err.message);
+    throw err;
+  }
+};

+ 15 - 14
downLoadServer/src/server/utils/chartsCom/3DDrawingChart.js

@@ -1,11 +1,3 @@
-/*
- * @Author: your name
- * @Date: 2025-04-14 16:09:13
- * @LastEditTime: 2026-03-17 09:30:20
- * @LastEditors: MacBookPro
- * @Description: In User Settings Edit
- * @FilePath: /downLoadServer/src/server/utils/chartsCom/3DDrawingChart.js
- */
 import puppeteer from "puppeteer";
 import fs from "fs-extra";
 import path from "path";
@@ -14,6 +6,11 @@ import axios from "axios";
 import { colorSchemes } from "../colors.js";
 export const generate3DDrawingChart = async (data, bucketName, objectName) => {
   try {
+    const timeoutMs = Number.isFinite(
+      Number.parseInt(process.env.CHART_RENDER_TIMEOUT_MS || "", 10),
+    )
+      ? Number.parseInt(process.env.CHART_RENDER_TIMEOUT_MS || "", 10)
+      : 500000;
     const colorSchemesItem = colorSchemes[0].colors;
     // 创建临时目录
     const tempDir = path.join(process.cwd(), "images");
@@ -39,10 +36,13 @@ export const generate3DDrawingChart = async (data, bucketName, objectName) => {
       executablePath: `${process.env.CHROME_PATH}`, // 根据系统改路径
 
       args: ["--no-sandbox", "--disable-setuid-sandbox"],
+      protocolTimeout: timeoutMs,
     });
 
     try {
       const page = await browser.newPage();
+      page.setDefaultTimeout(timeoutMs);
+      page.setDefaultNavigationTimeout(timeoutMs);
 
       // 准备图表数据
       const chartDataset = data.data[0]; // 修改为 data.chartData
@@ -86,6 +86,7 @@ export const generate3DDrawingChart = async (data, bucketName, objectName) => {
             fixedrange: true, // 防止缩放
             tickcolor: "black",
             tickangle: -10,
+            zeroline: false,
             title: {
               text: data.xaixs,
             },
@@ -104,6 +105,7 @@ export const generate3DDrawingChart = async (data, bucketName, objectName) => {
             showbackground: true,
             tickcolor: "black",
             tickangle: 25,
+            zeroline: false,
             title: {
               text: data.yaixs,
             },
@@ -119,6 +121,7 @@ export const generate3DDrawingChart = async (data, bucketName, objectName) => {
             tickcolor: "black",
             tickangle: -90,
             nticks: 3,
+            zeroline: false,
             title: {
               text: data.zaixs,
             },
@@ -180,8 +183,7 @@ export const generate3DDrawingChart = async (data, bucketName, objectName) => {
           <script>
             const traces = ${JSON.stringify(traces)};
             const layout = ${JSON.stringify(layout)};
-            Plotly.newPlot('chart', traces, layout, { responsive: true,displayModeBar: false,
-  staticPlot: true }).then(() => {
+            Plotly.newPlot('chart', traces, layout, { responsive: true }).then(() => {
               window.chartRendered = true; // 确保在图表渲染完成后设置
               
             }).catch((error) => {
@@ -193,15 +195,14 @@ export const generate3DDrawingChart = async (data, bucketName, objectName) => {
         `;
       // 设置页面内容
       await page.setContent(htmlContent, {
+        // 这里没有外部网络请求,使用 domcontentloaded 更稳定,避免 30s 默认 navigation timeout
         waitUntil: "domcontentloaded",
-        timeout: 0,
+        timeout: timeoutMs,
       });
 
-      await page.waitForTimeout(1000);
-
       // 等待图表渲染完成,延长超时时间
       await page.waitForFunction(() => window.chartRendered === true, {
-        timeout: 150000, // 延长到 120 秒
+        timeout: timeoutMs,
       });
 
       // 截图并保存到临时文件

+ 129 - 209
downLoadServer/src/server/utils/chartsCom/BarChart.js

@@ -1,215 +1,135 @@
-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";
-
-/**
- * 生成柱状图并上传
- * @param {Object} data - 图表数据
- * @returns {Promise<String>} - 返回图片URL
+/*
+ * 柱状图(服务化版本)
  */
-export const generateBarChart = async (data, bucketName, objectName) => {
-  try {
-    // 创建临时目录
-    const tempDir = path.join(process.cwd(), "images");
-    await fs.ensureDir(tempDir);
-    const tempFilePath = path.join(tempDir, `temp_chart_${Date.now()}.jpeg`);
-
-    // 获取 plotly.js 的绝对路径
-    const plotlyPath = path.join(
-      process.cwd(),
-      "src",
-      "public",
-      "js",
-      "plotly-latest.min.js",
-    );
-    const plotlyContent = await fs.readFile(plotlyPath, "utf-8");
-
-    // 创建浏览器实例
-    const browser = await puppeteer.launch({
-      headless: "new",
-      // 根据系统改路径
-      executablePath: `${process.env.CHROME_PATH}`, // 根据系统改路径
-
-      args: ["--no-sandbox", "--disable-setuid-sandbox"],
-    });
-
-    try {
-      const page = await browser.newPage();
-
-      // 准备图表数据
-      const chartDataset = data.data[0];
-      const trace = {
-        x: chartDataset.xData,
-        y: chartDataset.yData,
-        type: "bar",
-        marker: {
-          color: "#588CF0",
-        },
-        line: {
-          color: "#588CF0",
-        },
-        name: chartDataset.title || "数据",
-        hovertemplate: `${data.xaixs}: %{x} <br> ${data.yaixs}: %{y} <br>`,
-      };
-      const yValues = chartDataset.yData || [];
-      const minY = Math.min(...yValues, -5);
-      const maxY = Math.max(...yValues, 5);
-
-      // 准备布局配置
-      const layout = {
-        title: {
-          text: chartDataset.title,
-          font: {
-            size: 16,
-            weight: "bold",
-          },
-        },
-        xaxis: {
-          title: data.xaixs || "X轴",
-          gridcolor: "rgb(255,255,255)",
-          type: data.xaixs === "机组" ? "category" : undefined,
-          tickcolor: "rgb(255,255,255)",
-          backgroundcolor: "#e5ecf6",
-        },
-        yaxis: {
-          title: data.yaixs || "Y轴",
-          gridcolor: "rgb(255,255,255)",
-          tickcolor: "rgb(255,255,255)",
-          backgroundcolor: "#e5ecf6",
-          title_standoff: 100,
-          range:
-            data.data[0].title === "温度偏差"
-              ? [minY - 1, maxY + 1]
-              : undefined, // ✅ 手动设置 Y 轴范围
-        },
-        margin: {
-          l: 50,
-          r: 50,
-          t: 50,
-          b: 50,
-        },
-        plot_bgcolor: "#e5ecf6",
-        gridcolor: "#fff",
-        bgcolor: "#e5ecf6",
-        autosize: true,
-      };
-
-      // 如果 Y 轴是 "温度偏差",添加两条红色虚线
-      if (data.data[0].title === "温度偏差") {
-        layout.shapes = [
-          {
-            type: "line",
-            xref: "paper",
-            yref: "y",
-            x0: 0,
-            x1: 1,
-            y0: 5,
-            y1: 5,
-            line: {
-              color: "red",
-              width: 2,
-              dash: "dash",
-            },
-            hovertext: "上限: 5°C",
-            hoverinfo: "text",
-          },
-          {
-            type: "line",
-            xref: "paper",
-            yref: "y",
-            x0: 0,
-            x1: 1,
-            y0: -5,
-            y1: -5,
-            line: {
-              color: "red",
-              width: 2,
-              dash: "dash",
-            },
-            hovertext: "下限: -5°C",
-            hoverinfo: "text",
-          },
-        ];
-        layout.hovermode = "x unified";
-      }
+import { renderChart } from "../chartService/index.js";
 
-      // 如果是机组数据,设置刻度值
-      if (data.xaixs === "机组" || data.xaixs === "机组名称") {
-        layout.xaxis.tickvals = chartDataset.xData;
-        layout.xaxis.ticktext = chartDataset.xData;
-      }
-
-      // 创建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 trace = ${JSON.stringify(trace)};
-                const layout = ${JSON.stringify(layout)};
-                Plotly.newPlot('chart', [trace], layout,{
-  responsive: true,
-  displayModeBar: false,
-  staticPlot: true
-}).then(() => {
-                  window.chartRendered = true;
-                });
-              };
-            </script>
-          </body>
-        </html>
-      `;
-
-      // 设置页面内容
-      await page.setContent(htmlContent, {
-        waitUntil: "domcontentloaded",
-        timeout: 0,
-      });
-
-      await page.waitForTimeout(1000);
-
-      // 等待图表渲染完成
-      await page.waitForFunction(() => window.chartRendered === true, {
-        timeout: 60000,
-      });
-
-      // 截图并保存到临时文件
-      const chartElement = await page.$("#chart");
-      await chartElement.screenshot({
-        path: tempFilePath,
-        type: "jpeg",
-      });
+export const generateBarChart = async (data, bucketName, objectName) => {
+  /**
+   * =========================
+   * 1. 数据校验
+   * =========================
+   */
+  const chartDataset = data?.data?.[0];
+
+  if (!chartDataset) {
+    throw new Error("Invalid chart data");
+  }
 
-      // 上传图片到服务器
-      const formData = new FormData();
-      formData.append("file", fs.createReadStream(tempFilePath));
-      formData.append("type", "chart");
-      formData.append("engineCode", data.engineCode);
-      formData.append("analysisTypeCode", data.analysisTypeCode);
+  const xData = chartDataset.xData || [];
+  const yData = chartDataset.yData || [];
+
+  /**
+   * =========================
+   * 2. traces
+   * =========================
+   */
+  const traces = [
+    {
+      x: xData,
+      y: yData,
+      type: "bar",
+      marker: {
+        color: "#588CF0",
+      },
+      name: chartDataset.title || "数据",
+      hovertemplate: `${data.xaixs}: %{x}<br>${data.yaixs}: %{y}<extra></extra>`,
+    },
+  ];
+
+  /**
+   * =========================
+   * 3. layout
+   * =========================
+   */
+  const minY = Math.min(...yData, -5);
+  const maxY = Math.max(...yData, 5);
+
+  const layout = {
+    title: {
+      text: chartDataset.title,
+      font: { size: 16 },
+    },
+    xaxis: {
+      title: data.xaixs || "X轴",
+      type:
+        data.xaixs === "机组" || data.xaixs === "机组名称"
+          ? "category"
+          : undefined,
+      gridcolor: "rgb(255,255,255)",
+      tickcolor: "rgb(255,255,255)",
+      backgroundcolor: "#e5ecf6",
+      showbackground: true,
+      showline: true, // ✅ 显示 X 轴轴线
+      linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+      zeroline: false,
+    },
+    yaxis: {
+      title: data.yaixs || "Y轴",
+      gridcolor: "rgb(255,255,255)",
+      tickcolor: "rgb(255,255,255)",
+      backgroundcolor: "#e5ecf6",
+      showbackground: true,
+      showline: true, // ✅ 显示 X 轴轴线
+      linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+      zeroline: false,
+      range:
+        chartDataset.title === "温度偏差" ? [minY - 1, maxY + 1] : undefined,
+    },
+    margin: { l: 50, r: 50, t: 50, b: 50 },
+    showlegend: true,
+  };
+
+  /**
+   * =========================
+   * 4. 温度偏差(上下限线)
+   * =========================
+   */
+  if (chartDataset.title === "温度偏差") {
+    layout.shapes = [
+      {
+        type: "line",
+        xref: "paper",
+        yref: "y",
+        x0: 0,
+        x1: 1,
+        y0: 5,
+        y1: 5,
+        line: { color: "red", width: 2, dash: "dash" },
+      },
+      {
+        type: "line",
+        xref: "paper",
+        yref: "y",
+        x0: 0,
+        x1: 1,
+        y0: -5,
+        y1: -5,
+        line: { color: "red", width: 2, dash: "dash" },
+      },
+    ];
+
+    layout.hovermode = "x unified";
+  }
 
-      // 发送上传请求
-      const response = await axios.post(
-        `${process.env.API_BASE_URL}/examples/upload`,
-        { filePath: tempFilePath, bucketName, objectName },
-      );
-      return response?.data?.url;
-    } finally {
-      await browser.close();
-    }
-  } catch (error) {
-    // console.error("生成图表失败:", error);
-    throw error;
+  /**
+   * =========================
+   * 5. 机组类目处理
+   * =========================
+   */
+  if (data.xaixs === "机组" || data.xaixs === "机组名称") {
+    layout.xaxis.tickvals = xData;
+    layout.xaxis.ticktext = xData;
   }
+
+  /**
+   * =========================
+   * 6. 调用统一渲染
+   * =========================
+   */
+  return await renderChart({
+    traces,
+    layout,
+    bucketName,
+    objectName,
+  });
 };

+ 90 - 171
downLoadServer/src/server/utils/chartsCom/BoxLineCharts.js

@@ -1,186 +1,105 @@
 /*
  * @Author: your name
  * @Date: 2025-05-12 17:40:10
- * @LastEditTime: 2026-03-17 09:31:19
- * @LastEditors: MacBookPro
+ * @LastEditTime: 2026-03-18 09:18:23
+ * @LastEditors: bogon
  * @Description: In User Settings Edit
  * @FilePath: /downLoadServer/src/server/utils/chartsCom/BoxLineCharts.js
  */
-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";
 
-/**
- * 生成柱状图并上传
- * @param {Object} data - 图表数据
- * @returns {Promise<String>} - 返回图片URL
+/*
+ * 箱线图(终极稳定版)
  */
+import { renderChart } from "../chartService/index.js";
 
+/**
+ * 生成箱线图并上传
+ */
 export const generateBoxLineChart = async (data, bucketName, objectName) => {
-  try {
-    // 创建临时目录
-    const tempDir = path.join(process.cwd(), "images");
-    await fs.ensureDir(tempDir);
-    const tempFilePath = path.join(tempDir, `temp_chart_${Date.now()}.jpeg`);
-
-    // 获取 plotly.js 的绝对路径
-    const plotlyPath = path.join(
-      process.cwd(),
-      "src",
-      "public",
-      "js",
-      "plotly-latest.min.js",
-    );
-    const plotlyContent = await fs.readFile(plotlyPath, "utf-8");
-
-    // 创建浏览器实例
-    const browser = await puppeteer.launch({
-      headless: "new",
-      // 根据系统改路径
-      executablePath: `${process.env.CHROME_PATH}`, // 根据系统改路径
-
-      args: ["--no-sandbox", "--disable-setuid-sandbox"],
-    });
+  const chartDataset = data.data[0];
+
+  // ✅ 数据过滤(保留你的逻辑)
+  const filteredData = filterData(chartDataset);
+
+  // ✅ 主箱线图
+  const trace = {
+    x: filteredData.xData,
+    y: filteredData.yData,
+    type: "box",
+    marker: {
+      color: "#263649",
+    },
+    line: {
+      width: 0.8,
+    },
+    boxpoints: false,
+    boxmean: false,
+    name: filteredData.title,
+    fillcolor: "#458EF7",
+    hovertemplate: `${data.xaixs}: %{x} <br> ${data.yaixs}: %{y} <br>`,
+  };
 
-    try {
-      const page = await browser.newPage();
-
-      // 准备图表数据
-      const chartDataset = data.data[0];
-
-      // 过滤数据
-      const filteredData = filterData(chartDataset);
-
-      // 绘制箱线图
-      const trace = {
-        x: filteredData.xData,
-        y: filteredData.yData,
-        type: "box",
-        marker: {
-          color: "#263649",
-        },
-        line: {
-          width: 0.8,
-        },
-        boxpoints: false,
-        boxmean: false,
-        name: filteredData.title,
-        fillcolor: "#458EF7",
-        hovertemplate: `${data.xaixs}: %{x} <br> ${data.yaixs}: %{y} <br>`,
-      };
-
-      let trace2 = {};
-      if (filteredData.medians && filteredData.medians.x.length > 0) {
-        trace2 = {
-          x: filteredData.medians.x,
-          y: filteredData.medians.y,
-          mode: "markers",
-          marker: {
-            color: "#406DAB",
-            size: 3,
-          },
-          name: `${filteredData.title} - 中位点`,
-          type: "scatter",
-        };
-      }
-      // 准备布局配置
-      const layout = {
-        title: {
-          text: filteredData.title,
-          font: {
-            size: 16,
-            weight: "bold",
-          },
-        },
-        xaxis: {
-          title: data.xaixs || "X轴",
-          gridcolor: "rgb(255,255,255)",
-          type: data.xaixs === "机组" ? "category" : undefined,
-          tickcolor: "rgb(255,255,255)",
-          backgroundcolor: "#e5ecf6",
-        },
-        yaxis: {
-          title: data.yaixs || "Y轴",
-          gridcolor: "rgb(255,255,255)",
-          tickcolor: "rgb(255,255,255)",
-          backgroundcolor: "#e5ecf6",
-        },
-        showlegend: false,
-        plot_bgcolor: "#e5ecf6",
-        gridcolor: "#fff",
-        bgcolor: "#e5ecf6",
-      };
-
-      // 创建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 trace = ${JSON.stringify(trace)};
-                  const layout = ${JSON.stringify(layout)};
-                  Plotly.newPlot('chart', [trace, ${JSON.stringify(
-                    trace2,
-                  )}], layout,{
-  responsive: true,
-  displayModeBar: false,
-  staticPlot: true
-}).then(() => {
-                    window.chartRendered = true;
-                  });
-                };
-              </script>
-            </body>
-          </html>
-        `;
-
-      // 设置页面内容
-      await page.setContent(htmlContent, {
-        waitUntil: "domcontentloaded",
-        timeout: 0,
-      });
-
-      await page.waitForTimeout(1000);
-
-      // 等待图表渲染完成
-      await page.waitForFunction(() => window.chartRendered === true, {
-        timeout: 60000,
-      });
-
-      // 截图并保存到临时文件
-      const chartElement = await page.$("#chart");
-      await chartElement.screenshot({
-        path: tempFilePath,
-        type: "jpeg",
-      });
-
-      // 上传图片到服务器
-      const formData = new FormData();
-      formData.append("file", fs.createReadStream(tempFilePath));
-      const response = await axios.post(
-        `${process.env.API_BASE_URL}/examples/upload`,
-        { filePath: tempFilePath, bucketName, objectName },
-      );
-      return response?.data?.url;
-    } finally {
-      await browser.close();
-    }
-  } catch (error) {
-    console.error("生成图表失败:", error);
-    throw error;
+  // ✅ 中位点(可选)
+  const traces = [trace];
+
+  if (filteredData.medians?.x?.length) {
+    traces.push({
+      x: filteredData.medians.x,
+      y: filteredData.medians.y,
+      mode: "markers",
+      marker: {
+        color: "#406DAB",
+        size: 3,
+      },
+      name: `${filteredData.title} - 中位点`,
+      type: "scatter",
+    });
   }
+
+  // ✅ layout
+  const layout = {
+    title: {
+      text: filteredData.title,
+      font: {
+        size: 16,
+        weight: "bold",
+      },
+    },
+    xaxis: {
+      title: data.xaixs || "X轴",
+      gridcolor: "#fff",
+      type: data.xaixs === "机组" ? "category" : undefined,
+      gridcolor: "rgb(255,255,255)",
+      tickcolor: "rgb(255,255,255)",
+      backgroundcolor: "#e5ecf6",
+      showbackground: true,
+      showline: true, // ✅ 显示 X 轴轴线
+      linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+      zeroline: false,
+    },
+    yaxis: {
+      title: data.yaixs || "Y轴",
+      gridcolor: "#fff",
+      gridcolor: "rgb(255,255,255)",
+      tickcolor: "rgb(255,255,255)",
+      backgroundcolor: "#e5ecf6",
+      showbackground: true,
+      showline: true, // ✅ 显示 X 轴轴线
+      linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+      zeroline: false,
+    },
+    showlegend: false,
+    plot_bgcolor: "#e5ecf6",
+    paper_bgcolor: "#e5ecf6",
+  };
+
+  // ✅ 🚀 统一走 chartService
+  return await renderChart({
+    traces,
+    layout,
+    bucketName,
+    objectName,
+  });
 };
 
 // 过滤数据的函数

+ 72 - 116
downLoadServer/src/server/utils/chartsCom/BoxMarkersCharts.js

@@ -1,46 +1,55 @@
 /*
  * @Author: your name
  * @Date: 2025-05-13 11:02:32
- * @LastEditTime: 2026-03-17 09:31:26
- * @LastEditors: MacBookPro
+ * @LastEditTime: 2026-03-18 09:11:01
+ * @LastEditors: bogon
  * @Description: In User Settings Edit
  * @FilePath: /downLoadServer/src/server/utils/chartsCom/BoxMarkersCharts.js
  */
-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";
 
-// 判断时间戳是否在选择的日期范围内
-const isInDateRange = (timestamp) => {
-  const [startDate, endDate] = [];
+/**
+ * 判断时间范围
+ */
+const isInDateRange = (timestamp, startDate, endDate) => {
   if (!startDate || !endDate) return true;
 
-  const date = new Date(timestamp);
-  return date >= new Date(startDate) && date <= new Date(endDate);
+  const t = new Date(timestamp).getTime();
+  return t >= new Date(startDate).getTime() && t <= new Date(endDate).getTime();
 };
 
-// 过滤数据
-const filterData = (group) => {
+/**
+ * 过滤数据(修复版)
+ */
+const filterData = (group, options = {}) => {
+  const { startDate, endDate } = options;
+
   const filteredXData = [];
   const filteredYData = [];
   const filteredMedians = group.medians ? { x: [], y: [] } : null;
 
-  group.medians.x.forEach((timestamp, index) => {
-    if (isInDateRange(timestamp)) {
-      filteredMedians.x.push(timestamp);
-      filteredMedians.y.push(group.medians.y[index]);
-    }
-  });
+  // ✅ medians 安全处理
+  if (group.medians?.x?.length) {
+    group.medians.x.forEach((timestamp, index) => {
+      if (isInDateRange(timestamp, startDate, endDate)) {
+        filteredMedians.x.push(timestamp);
+        filteredMedians.y.push(group.medians.y[index]);
+      }
+    });
+  }
 
+  // ✅ 主数据
   group.xData.forEach((timestamp, index) => {
-    if (isInDateRange(timestamp)) {
+    if (isInDateRange(timestamp, startDate, endDate)) {
       filteredXData.push(timestamp);
       filteredYData.push(group.yData[index]);
     }
   });
+
   return {
     ...group,
     xData: filteredXData,
@@ -49,7 +58,9 @@ const filterData = (group) => {
   };
 };
 
-// 绘制箱线图
+/**
+ * 生成箱线图(带中位点)
+ */
 export const generateBoxMarkersCharts = async (
   data,
   bucketName,
@@ -57,21 +68,18 @@ export const generateBoxMarkersCharts = async (
 ) => {
   const { data: chartData, xaixs, yaixs, analysisTypeCode } = data;
 
-  const filteredData = chartData.map((group) => filterData(group));
+  // ✅ 支持时间过滤
+  const filteredData = chartData.map((group) =>
+    filterData(group, {
+      startDate: data.startDate,
+      endDate: data.endDate,
+    }),
+  );
 
   const traces = [];
-  const medianMarkers = [];
-  // 获取 plotly.js 的绝对路径
-  const plotlyPath = path.join(
-    process.cwd(),
-    "src",
-    "public",
-    "js",
-    "plotly-3.0.1.min.js",
-  );
-  const plotlyContent = await fs.readFile(plotlyPath, "utf-8");
 
   filteredData.forEach((group) => {
+    // ✅ 箱线图
     traces.push({
       x: group.xData,
       y: group.yData,
@@ -85,123 +93,71 @@ export const generateBoxMarkersCharts = async (
       hovertemplate:
         `<b>${xaixs}</b>: %{x}<br>` +
         `<b>最大值</b>: %{upperfence}<br>` +
-        `<b>上四分位数 (Q3)</b>: %{q3}<br>` +
-        `<b>中位数 (Median)</b>: %{median}<br>` +
-        `<b>下四分位数 (Q1)</b>: %{q1}<br>` +
+        `<b>Q3</b>: %{q3}<br>` +
+        `<b>中位数</b>: %{median}<br>` +
+        `<b>Q1</b>: %{q1}<br>` +
         `<b>最小值</b>: %{lowerfence}<br>` +
         `<b>均值</b>: %{mean}<br>` +
         `<extra></extra>`,
     });
 
-    if (group.medians && group.medians.x.length > 0) {
-      medianMarkers.push({
+    // ✅ 中位点 scatter
+    if (group.medians?.x?.length) {
+      traces.push({
         x: group.medians.x,
         y: group.medians.y,
         mode: "markers",
         marker: { color: "#f00", size: 3 },
         name: `${group.title} - 中位点`,
         type: "scatter",
-        hovertemplate: `<b>${xaixs}</b>: %{x} <br> <b>${yaixs}</b>: %{y} <br><b>中位点</b>: %{y}<br><extra></extra>`,
+        hovertemplate:
+          `<b>${xaixs}</b>: %{x}<br>` +
+          `<b>${yaixs}</b>: %{y}<br>` +
+          `<b>中位点</b>: %{y}<br>` +
+          `<extra></extra>`,
       });
     }
   });
 
+  // ✅ layout(更干净)
   const layout = {
     title: {
-      text: analysisTypeCode + chartData[0].engineName,
-      font: { size: 16, weight: "bold" },
+      text: analysisTypeCode + (chartData[0]?.engineName || ""),
+      font: { size: 16 },
     },
     xaxis: {
       title: xaixs,
       type: "date",
       tickformat: "%Y-%m-%d",
+      gridcolor: "#fff",
       gridcolor: "rgb(255,255,255)",
       tickcolor: "rgb(255,255,255)",
       backgroundcolor: "#e5ecf6",
+      showbackground: true,
+      showline: true, // ✅ 显示 X 轴轴线
+      linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+      zeroline: false,
     },
     yaxis: {
       title: yaixs,
       gridcolor: "rgb(255,255,255)",
       tickcolor: "rgb(255,255,255)",
       backgroundcolor: "#e5ecf6",
+      showbackground: true,
+      showline: true, // ✅ 显示 X 轴轴线
+      linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+      zeroline: false,
     },
     plot_bgcolor: "#e5ecf6",
+    paper_bgcolor: "#e5ecf6",
     showlegend: true,
   };
 
-  // 使用 Puppeteer 生成图表的截图
-  const browser = await puppeteer.launch({
-    headless: "new",
-    // 根据系统改路径
-    executablePath: `${process.env.CHROME_PATH}`, // 根据系统改路径
-    args: ["--no-sandbox", "--disable-setuid-sandbox"],
+  // ✅ 🚀 核心:统一走 chartService
+  return await renderChart({
+    traces,
+    layout,
+    bucketName,
+    objectName,
   });
-
-  try {
-    const page = await browser.newPage();
-    const htmlContent = `
-        <!DOCTYPE html>
-        <html>
-          <head>
-           <meta charset="UTF-8">
-           <script>${plotlyContent}</script>
-            <style>
-              body { margin: 0; }
-              #chart { width: 800px; height: 600px; }
-            </style>
-          </head>
-          <body>
-            <div id="chart"></div>
-            <script>
-              window.onload = function() {
-                const traces = ${JSON.stringify([...traces, ...medianMarkers])};
-                const layout = ${JSON.stringify(layout)};
-                Plotly.newPlot('chart', traces, layout,{
-  responsive: true,
-  displayModeBar: false,
-  staticPlot: true
-}).then(() => {
-                  window.chartRendered = true;
-                });
-              };
-            </script>
-          </body>
-        </html>
-      `;
-
-    await page.setContent(htmlContent, {
-      waitUntil: "domcontentloaded",
-      timeout: 0,
-    });
-
-    await page.waitForTimeout(1000);
-    await page.waitForFunction(() => window.chartRendered === true, {
-      timeout: 60000,
-    });
-
-    // 截图并保存到临时文件
-    const tempFilePath = path.join(
-      process.cwd(),
-      "images",
-      `chart_${Date.now()}.jpeg`,
-    );
-    const chartElement = await page.$("#chart");
-    await chartElement.screenshot({ path: tempFilePath, type: "jpeg" });
-
-    // 上传图片到服务器
-    const formData = new FormData();
-    formData.append("file", fs.createReadStream(tempFilePath));
-    const response = await axios.post(
-      `${process.env.API_BASE_URL}/examples/upload`,
-      {
-        filePath: tempFilePath,
-        bucketName,
-        objectName,
-      },
-    );
-
-    return response?.data?.url;
-  } finally {
-    await browser.close();
-  }
 };

+ 97 - 219
downLoadServer/src/server/utils/chartsCom/ColorbarInitTwoDmarkersChart.js

@@ -1,231 +1,109 @@
 /*
- * @Author: your name
- * @Date: 2025-04-28 10:27:00
- * @LastEditTime: 2026-03-17 09:31:41
- * @LastEditors: MacBookPro
- * @Description: In User Settings Edit
- * @FilePath: /downLoadServer/src/server/utils/chartsCom/ColorbarInitTwoDmarkersChart.js
+ * 2D 散点图(带 colorbar)- 终极稳定版
  */
-import puppeteer from "puppeteer";
-import fs from "fs-extra";
-import path from "path";
-import FormData from "form-data";
+import { renderChart } from "../chartService/index.js";
 import { colorSchemes } from "../colors.js";
-import axios from "axios";
 
 export const generateColorbarInitTwoDmarkersChart = async (
   data,
   bucketName,
   objectName,
 ) => {
-  try {
-    const colorsBar = colorSchemes[4].colors;
-    // 创建临时目录
-    const tempDir = path.join(process.cwd(), "images");
-    await fs.ensureDir(tempDir);
-    const tempFilePath = path.join(
-      tempDir,
-      `temp_scatter_chart_${Date.now()}.jpeg`,
-    );
-
-    // 获取 plotly.js 的绝对路径
-    const plotlyPath = path.join(
-      process.cwd(),
-      "src",
-      "public",
-      "js",
-      "plotly-3.0.1.min.js",
-    );
-    const plotlyContent = await fs.readFile(plotlyPath, "utf-8");
-
-    // 创建浏览器实例
-    const browser = await puppeteer.launch({
-      headless: "new",
-      // 根据系统改路径
-      executablePath: `${process.env.CHROME_PATH}`, // 根据系统改路径
-
-      args: ["--no-sandbox", "--disable-setuid-sandbox"],
-    });
-
-    try {
-      const page = await browser.newPage();
-
-      // 提取散点数据
-      const scatterData = data.data && data.data[0];
-
-      // 如果有 colorbar 数据
-      const uniqueTimeLabels =
-        scatterData.colorbar &&
-        scatterData.colorbar.length === scatterData.xData.length
-          ? [...new Set(scatterData.colorbar)] // 从 colorbar 中提取唯一的标签
-          : [...new Set(scatterData.yData)]; // 如果没有 colorbar,使用 data.color
-
-      const ticktext = uniqueTimeLabels.map((dateStr) => dateStr); // 格式化为标签
-      const tickvals = uniqueTimeLabels.map((label, index) => index + 1); // 设置 tick 值
-      const timeMapping = uniqueTimeLabels.reduce((acc, curr, index) => {
-        acc[curr] = index + 1;
-        return acc;
-      }, {});
-      // 获取 colorbar 的最小值和最大值来计算比例值
-      const minValue = Math.min(...new Set(uniqueTimeLabels));
-      const maxValue = Math.max(...new Set(uniqueTimeLabels));
-      const colorStops = [
-        colorsBar[0],
-        colorsBar[4],
-        colorsBar[8],
-        colorsBar[12],
-      ];
-
-      // 计算渐变比例
-      const colors = colorStops.map((color, index) => {
-        const proportion = index / (colorStops.length - 1); // 计算比例值 (0, 1/3, 2/3, 1)
-        return [proportion, color]; // 创建比例-颜色映射
-      });
-      // 计算颜色值映射
-      let colorValues = [];
-      if (
-        scatterData.colorbar &&
-        scatterData.colorbar.length === scatterData.xData.length
-      ) {
-        colorValues = scatterData.colorbar.map((date) => timeMapping[date]);
-      } else {
-        colorValues = scatterData.yData.map((date) => timeMapping[date]);
-      }
-
-      // 绘制 2D 散点图
-      const trace = {
-        x: scatterData.xData,
-        y: scatterData.yData,
-        mode: "markers",
-        type: "scattergl", // 使用 scattergl 提高性能
-        text: scatterData.engineName, // 提示文本
-        marker: {
-          color: scatterData.colorbar, // 根据 colorbar 映射的数字设置颜色
-          colorscale: colors, // 使用自定义颜色比例
-          colorbar: scatterData.colorbar
-            ? { title: { text: scatterData.colorbartitle || "" } }
-            : undefined, // 如果有 colorbar 显示,否则不显示
-          size: 4, // 点的大小
-          line: {
-            color: "#fff", // 让边框颜色和点颜色相同
-            width: 0.3, // 设置边框宽度
-          },
-        },
-      };
-
-      // 设置 hovertemplate
-      if (scatterData.colorbar) {
-        trace.customdata = scatterData.colorbar || scatterData.color;
-      }
-      if (scatterData.colorbartitle === "密度") {
-        trace.marker.cmin = 0;
-        trace.marker.cmax = maxValue;
-      } else if (scatterData.colorbartitle === "百分比(%)") {
-        trace.marker.cmin = 0;
-        trace.marker.cmax = 100;
-      }
-      // 图表布局
-      const layout = {
-        title: {
-          text: scatterData.title,
-          font: {
-            size: 16,
-            weight: "bold",
-          },
-        },
-        xaxis: {
-          title: {
-            text: data.xaixs,
-          },
-          gridcolor: "rgb(255,255,255)", // 网格线颜色
-          tickcolor: "rgb(255,255,255)",
-          backgroundcolor: "#e5ecf6",
-          showbackground: true, // 显示背景
-        },
-        yaxis: {
-          title: {
-            text: data.yaixs,
-          },
-          gridcolor: "rgb(255,255,255)", // 网格线颜色
-          tickcolor: "rgb(255,255,255)",
-          backgroundcolor: "#e5ecf6",
-          showbackground: true, // 显示背景
-        },
-        showlegend: false,
-        plot_bgcolor: "#e5ecf6",
-        gridcolor: "#fff", // 设置网格线颜色
-      };
-      if (data.xaixs === "时间") {
-        layout.xaxis.type = "date";
-        layout.xaxis.tickformat = "%Y-%m-%d";
-      }
-      // 准备 HTML 内容
-      const htmlContent = `
-        <!DOCTYPE html>
-        <html>
-        <head>
-          <meta charset="UTF-8">
-          <title>2D 散点图</title>
-          <script>${plotlyContent}</script>
-        </head>
-        <body>
-          <div id="chart" style="width: 100%; height: 600px"></div>
-          <script>
-            const traces = [${JSON.stringify(trace)}];
-            const layout = ${JSON.stringify(layout)};
-            Plotly.newPlot('chart', traces, layout, {
-  responsive: true,
-  displayModeBar: false,
-  staticPlot: true
-}).then(() => {
-              window.chartRendered = true; // 确保在图表渲染完成后设置
-              console.log("图表渲染完成");
-            }).catch((error) => {
-              console.error("图表渲染错误:", error); // 捕获渲染错误
-            });
-          </script>
-        </body>
-        </html>
-      `;
-
-      // 设置页面内容
-      await page.setContent(htmlContent, {
-        waitUntil: "domcontentloaded",
-        timeout: 0,
-      });
-
-      await page.waitForTimeout(1000);
-
-      // 等待图表渲染完成,延长超时时间
-      await page.waitForFunction(() => window.chartRendered === true, {
-        timeout: 150000, // 延长到 150 秒
-      });
-
-      // 截图并保存到临时文件
-      const chartElement = await page.$("#chart");
-      await chartElement.screenshot({
-        path: tempFilePath,
-        type: "jpeg",
-      });
+  const colorsBar = colorSchemes[4].colors;
+
+  const scatterData = data.data?.[0];
+  if (!scatterData) throw new Error("scatterData missing");
+
+  // ✅ 构造颜色刻度(更合理)
+  const colorStops = [colorsBar[0], colorsBar[4], colorsBar[8], colorsBar[12]];
+
+  const colorscale = colorStops.map((color, index) => [
+    index / (colorStops.length - 1),
+    color,
+  ]);
+
+  // ✅ colorbar 数据
+  const hasColorbar =
+    scatterData.colorbar &&
+    scatterData.colorbar.length === scatterData.xData.length;
+
+  const colorValues = hasColorbar ? scatterData.colorbar : scatterData.yData;
+
+  // ✅ trace
+  const trace = {
+    x: scatterData.xData,
+    y: scatterData.yData,
+    mode: "markers",
+    type: "scattergl",
+    text: scatterData.engineName,
+
+    marker: {
+      color: colorValues,
+      colorscale,
+      size: 4,
+      line: {
+        color: "#fff",
+        width: 0.3,
+      },
+
+      // ✅ colorbar 显示
+      colorbar: hasColorbar
+        ? { title: { text: scatterData.colorbartitle || "" } }
+        : undefined,
+    },
+  };
+
+  // ✅ 特殊范围控制(修复版)
+  if (scatterData.colorbartitle === "密度") {
+    trace.marker.cmin = 0;
+    trace.marker.cmax = Math.max(...colorValues);
+  } else if (scatterData.colorbartitle === "百分比(%)") {
+    trace.marker.cmin = 0;
+    trace.marker.cmax = 100;
+  }
 
-      // 上传图片到服务器
-      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;
-    } catch (error) {
-      console.error("生成2D散点图失败:", error);
-      throw error;
-    } finally {
-      await browser.close();
-    }
-  } catch (error) {
-    console.error("生成2D散点图失败:", error);
-    throw error;
+  // ✅ layout
+  const layout = {
+    title: {
+      text: scatterData.title,
+      font: { size: 16, weight: "bold" },
+    },
+    xaxis: {
+      title: data.xaixs,
+      gridcolor: "rgb(255,255,255)",
+      tickcolor: "rgb(255,255,255)",
+      backgroundcolor: "#e5ecf6",
+      showbackground: true,
+      showline: true, // ✅ 显示 X 轴轴线
+      linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+      zeroline: false,
+    },
+    yaxis: {
+      title: data.yaixs,
+      gridcolor: "rgb(255,255,255)",
+      tickcolor: "rgb(255,255,255)",
+      backgroundcolor: "#e5ecf6",
+      showbackground: true,
+      showline: true, // ✅ 显示 X 轴轴线
+      linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+      zeroline: false,
+    },
+    showlegend: false,
+    plot_bgcolor: "#e5ecf6",
+    paper_bgcolor: "#e5ecf6",
+  };
+
+  // ✅ 时间轴处理
+  if (data.xaixs === "时间") {
+    layout.xaxis.type = "date";
+    layout.xaxis.tickformat = "%Y-%m-%d";
   }
+
+  // ✅ 🚀 核心:统一走 chartService
+  return await renderChart({
+    traces: [trace],
+    layout,
+    bucketName,
+    objectName,
+  });
 };

+ 85 - 176
downLoadServer/src/server/utils/chartsCom/FaultAll.js

@@ -1,9 +1,7 @@
-import puppeteer from "puppeteer";
-import fs from "fs-extra";
-import path from "path";
-import FormData from "form-data";
-import axios from "axios"; // 导入 axios
-import { colorSchemes } from "../colors.js";
+/*
+ * 全场故障图(柱状 + 折线双轴)- 终极稳定版
+ */
+import { renderChart } from "../chartService/index.js";
 
 export const getFaultAllCharts = async (
   data,
@@ -11,180 +9,91 @@ export const getFaultAllCharts = async (
   objectName,
   analysisTypeCode,
 ) => {
-  // 获取 plotly.js 的绝对路径
-  const plotlyPath = path.join(
-    process.cwd(),
-    "src",
-    "public",
-    "js",
-    "plotly-3.0.1.min.js",
-  );
-  const plotlyContent = await fs.readFile(plotlyPath, "utf-8");
-  // console.log(data, "全场故障数据");
-  try {
-    // 提取故障类型、故障次数和故障时长
-    const faultTypes = data.map((item) => item.fault_detail);
-    const faultCounts = data.map((item) => item.count);
-    const faultDurations = data.map((item) => item.fault_time_sum);
-
-    // 故障次数的柱状图数据(左侧 Y 轴)
-    const barTrace = {
-      x: faultTypes.slice(0, 10),
-      y: faultCounts.slice(0, 10),
-      type: "bar",
-      marker: { color: "#64ADC2" }, // 蓝色柱状图
-      name: "故障次数",
-      hovertemplate: `故障类型: %{x} <br> 故障次数: %{y} 次<br>`,
-    };
-
-    // 故障时长的折线图数据(右侧 Y 轴)
-    const lineTrace = {
-      x: faultTypes.slice(0, 10),
-      y: faultDurations.slice(0, 10),
-      type: "scatter",
-      mode: "lines+markers", // 线性图 + 点标记
-      line: { color: "#1A295D" }, // 红色折线
-      name: "故障时长",
-      yaxis: "y2", // 使用第二个 Y 轴(右侧)
-      hovertemplate: `故障类型: %{x} <br> 故障时长: %{y} 小时 <br>`,
-    };
+  if (!Array.isArray(data) || data.length === 0) {
+    throw new Error("fault data empty");
+  }
 
-    // 布局配置,设置双 Y 轴
-    const layout = {
-      title: {
-        text: "全场故障次数与时长分析Top10",
-        font: {
-          size: 16, // 设置标题字体大小(默认 16)
-          weight: "bold",
-        },
-      },
-      xaxis: {
-        title: {
-          text: "故障类型",
-        },
+  // ✅ 排序(真正 Top10)
+  const sorted = [...data].sort((a, b) => b.count - a.count).slice(0, 10);
 
-        tickangle: 30,
-        tickmode: "array",
-        tickvals: faultTypes.slice(0, 10),
-        tickfont: { size: 12 },
-        gridcolor: "rgb(255,255,255)",
-        tickcolor: "rgb(255,255,255)",
-        backgroundcolor: "#e5ecf6",
-      },
-      yaxis: {
-        title: {
-          text: "故障次数",
-        },
-        titlefont: { color: "#64ADC2" },
-        tickfont: { color: "#64ADC2" },
-        side: "left", // 左侧的 Y 轴
-        showline: true,
-        linecolor: "#64ADC2",
-        gridcolor: "rgb(255,255,255)",
-        tickcolor: "rgb(255,255,255)",
-        backgroundcolor: "#e5ecf6",
-      },
-      yaxis2: {
-        title: {
-          text: "故障时长 (小时)",
-        },
-        titlefont: { color: "#1A295D" },
-        tickfont: { color: "#1A295D" },
-        overlaying: "y", // 在第一个 Y 轴上方绘制
-        side: "right", // 右侧的 Y 轴
-        position: 1, // 调整右侧轴的位置
-        showline: true,
-        linecolor: "#1A295D", // 设置右侧轴线颜色
-      },
-      barmode: "group", // 柱状图分组
-      plot_bgcolor: "#e5ecf6",
-      gridcolor: "#fff",
-      bgcolor: "#e5ecf6", // 设置背景颜色
-      showlegend: false,
-      margin: {
-        t: 80, // 上边距
-        b: 150, // 下边距,给 X 轴标签更多空间
-      },
-    };
+  const faultTypes = sorted.map((item) => item.fault_detail);
+  const faultCounts = sorted.map((item) => item.count);
+  const faultDurations = sorted.map((item) => item.fault_time_sum);
 
-    // 创建临时目录
-    const tempDir = path.join(process.cwd(), "images");
-    await fs.ensureDir(tempDir);
-    const tempFilePath = path.join(
-      tempDir,
-      `temp_fault_all_chart_${Date.now()}.jpeg`,
-    );
+  // ✅ 柱状图(左轴)
+  const barTrace = {
+    x: faultTypes,
+    y: faultCounts,
+    type: "bar",
+    marker: { color: "#64ADC2" },
+    name: "故障次数",
+    hovertemplate: `故障类型: %{x}<br>故障次数: %{y} 次<br>`,
+  };
 
-    // 使用 Puppeteer 生成图表的截图
-    const browser = await puppeteer.launch({
-      headless: "new",
-      // 根据系统改路径
-      executablePath: `${process.env.CHROME_PATH}`, // 根据系统改路径
+  // ✅ 折线图(右轴)
+  const lineTrace = {
+    x: faultTypes,
+    y: faultDurations,
+    type: "scatter",
+    mode: "lines+markers",
+    line: { color: "#1A295D" },
+    name: "故障时长",
+    yaxis: "y2",
+    hovertemplate: `故障类型: %{x}<br>故障时长: %{y} 小时<br>`,
+  };
 
-      args: ["--no-sandbox", "--disable-setuid-sandbox"],
-    });
-    try {
-      const page = await browser.newPage();
-      const htmlContent = `
-        <!DOCTYPE html>
-        <html>
-          <head>
-            <meta charset="UTF-8">
-            <title>全场故障</title>
-            <script>${plotlyContent}</script>
-            <style>
-              body { margin: 0; }
-              #chart { width: 800px; height: 600px; }
-            </style>
-          </head>
-          <body>
-            <div id="chart"></div>
-            <script>
-              window.onload = function() {
-                Plotly.newPlot('chart', [${JSON.stringify(
-                  barTrace,
-                )}, ${JSON.stringify(lineTrace)}], ${JSON.stringify(layout)},{
-  responsive: true,
-  displayModeBar: false,
-  staticPlot: true
-}).then(() => {
-                  window.chartRendered = true;
-                });
-              };
-            </script>
-          </body>
-        </html>
-      `;
-      await page.setContent(htmlContent, {
-        waitUntil: "domcontentloaded",
-        timeout: 0,
-      });
+  // ✅ layout(优化版)
+  const layout = {
+    title: {
+      text: "全场故障次数与时长分析 Top10",
+      font: { size: 16 },
+    },
+    xaxis: {
+      title: "故障类型",
+      tickangle: 30,
+      tickfont: { size: 12 },
+      gridcolor: "#fff",
+      gridcolor: "rgb(255,255,255)",
+      tickcolor: "rgb(255,255,255)",
+      backgroundcolor: "#e5ecf6",
+      showbackground: true,
+      showline: true, // ✅ 显示 X 轴轴线
+      linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+      zeroline: false,
+    },
+    yaxis: {
+      title: "故障次数",
+      side: "left",
+      showline: true,
+      linecolor: "#64ADC2",
+      tickfont: { color: "#64ADC2" },
+      gridcolor: "#fff",
+      zeroline: false,
+    },
+    yaxis2: {
+      title: "故障时长 (小时)",
+      overlaying: "y",
+      side: "right",
+      showline: true,
+      linecolor: "#1A295D",
+      tickfont: { color: "#1A295D" },
+      zeroline: false,
+    },
+    barmode: "group",
+    plot_bgcolor: "#e5ecf6",
+    paper_bgcolor: "#e5ecf6",
+    showlegend: false,
+    margin: {
+      t: 80,
+      b: 120,
+    },
+  };
 
-      await page.waitForTimeout(1000);
-      await page.waitForFunction(() => window.chartRendered === true, {
-        timeout: 60000,
-      });
-      // 截图并保存到临时文件
-      const chartElement = await page.$("#chart");
-      await chartElement.screenshot({ path: tempFilePath, type: "jpeg" });
-      // 上传图片到服务器
-      const formData = new FormData();
-      formData.append("file", fs.createReadStream(tempFilePath));
-      const response = await axios.post(
-        `${process.env.API_BASE_URL}/examples/upload`,
-        {
-          filePath: tempFilePath,
-          bucketName,
-          objectName,
-        },
-      );
-      return response?.data?.url;
-    } catch (error) {
-      console.error("生成图表失败:", error);
-    } finally {
-      await browser.close();
-    }
-  } catch (error) {
-    console.error("发生错误:", error);
-  }
+  // ✅ 🚀 核心:统一走 chartService
+  return await renderChart({
+    traces: [barTrace, lineTrace],
+    layout,
+    bucketName,
+    objectName,
+  });
 };

+ 86 - 161
downLoadServer/src/server/utils/chartsCom/FaultUnit.js

@@ -1,8 +1,7 @@
-import puppeteer from "puppeteer";
-import fs from "fs-extra";
-import path from "path";
-import FormData from "form-data";
-import axios from "axios"; // 导入 axios
+/*
+ * 机组故障气泡图(终极稳定版)
+ */
+import { renderChart } from "../chartService/index.js";
 
 export const getFaultUnitCharts = async (
   data,
@@ -10,169 +9,95 @@ export const getFaultUnitCharts = async (
   objectName,
   analysisTypeCode,
 ) => {
-  try {
-    // 获取 plotly.js 的绝对路径
-    const plotlyPath = path.join(
-      process.cwd(),
-      "src",
-      "public",
-      "js",
-      "plotly-3.0.1.min.js",
-    );
-    const plotlyContent = await fs.readFile(plotlyPath, "utf-8");
+  if (!Array.isArray(data) || data.length === 0) {
+    throw new Error("fault unit data empty");
+  }
 
-    // 步骤 1:预处理
-    const combined = data.map((item) => ({
+  // ✅ 数据预处理
+  const combined = data
+    .map((item) => ({
       name: item.wind_turbine_name,
-      count: item.count,
-      durationHour: (item.fault_time / 3600).toFixed(2), // 秒转小时
-    }));
+      count: Number(item.count) || 0,
+      durationHour: Number((item.fault_time || 0) / 3600),
+    }))
+    .sort((a, b) => a.name.localeCompare(b.name));
 
-    // 步骤 2:按 name 排序(字典序)
-    combined.sort((a, b) => a.name.localeCompare(b.name));
+  const names = combined.map((d) => d.name);
+  const durations = combined.map((d) => d.durationHour);
+  const counts = combined.map((d) => d.count);
 
-    // 步骤 3:归一化 count 用于 size
-    const rawSizes = combined.map((d) => d.count);
-    const minSize = 8;
-    const maxSize = 30;
-    const sizeRange = maxSize - minSize;
-    const minValue = Math.min(...rawSizes);
-    const maxValue = Math.max(...rawSizes);
-    const normalizedSizes = rawSizes.map((val) => {
-      if (maxValue === minValue) return (minSize + maxSize) / 2;
-      return ((val - minValue) / (maxValue - minValue)) * sizeRange + minSize;
-    });
+  // ✅ 气泡大小归一化
+  const minSize = 8;
+  const maxSize = 30;
 
-    // 步骤 4:生成 plotly 数据
-    const bubbleData = combined.map((d, i) => ({
-      x: [d.name],
-      y: [d.durationHour],
-      mode: "markers",
-      type: "scatter",
-      name: d.name,
-      marker: {
-        size: normalizedSizes[i],
-        sizemode: "area",
-        sizeref: 1,
-        sizemin: 4,
-        showscale: false,
-      },
-      hovertemplate: `机组: ${d.name}<br>故障时长: ${d.durationHour} 小时<br>故障次数: ${d.count} 次<extra></extra>`,
-    }));
+  const minVal = Math.min(...counts);
+  const maxVal = Math.max(...counts);
 
-    const layout = {
-      title: {
-        text: "机组故障时长与故障次数分析",
-        font: { size: 16, weight: "bold" },
-      },
-      xaxis: {
-        title: "故障机组",
-        type: "category",
-        tickangle: 30,
-        tickfont: { size: 12 },
-        gridcolor: "rgb(255,255,255)",
-        tickcolor: "rgb(255,255,255)",
-        backgroundcolor: "#e5ecf6",
-        showbackground: true,
-      },
-      yaxis: {
-        title: "故障时长(小时)",
-        tickfont: { size: 12 },
-        gridcolor: "rgb(255,255,255)",
-        tickcolor: "rgb(255,255,255)",
-        backgroundcolor: "#e5ecf6",
-        showbackground: true,
-      },
-      plot_bgcolor: "#e5ecf6",
-      gridcolor: "#fff",
-      margin: { t: 80, b: 120 },
-      showlegend: true,
-      legendgroup: "same",
-      legend: {
-        itemsizing: "constant",
-        font: {
-          size: 12,
-        },
-      },
-    };
+  const sizes = counts.map((val) => {
+    if (maxVal === minVal) return (minSize + maxSize) / 2;
+    return ((val - minVal) / (maxVal - minVal)) * (maxSize - minSize) + minSize;
+  });
 
-    // 创建临时目录
-    const tempDir = path.join(process.cwd(), "images");
-    await fs.ensureDir(tempDir);
-    const tempFilePath = path.join(
-      tempDir,
-      `temp_fault_unit_chart_${Date.now()}.jpeg`,
-    );
+  // ✅ 🚀 单 trace(性能关键)
+  const trace = {
+    x: names,
+    y: durations,
+    mode: "markers",
+    type: "scatter",
+    marker: {
+      size: sizes,
+      sizemode: "area",
+      sizemin: 4,
+    },
+    text: combined.map(
+      (d) =>
+        `机组: ${d.name}<br>故障时长: ${d.durationHour.toFixed(
+          2,
+        )} 小时<br>故障次数: ${d.count} 次`,
+    ),
+    hovertemplate: "%{text}<extra></extra>",
+  };
 
-    // 使用 Puppeteer 生成图表的截图
-    const browser = await puppeteer.launch({
-      headless: "new",
-      executablePath: `${process.env.CHROME_PATH}`, // 根据系统改路径
-      args: ["--no-sandbox", "--disable-setuid-sandbox"],
-    });
-    try {
-      const page = await browser.newPage();
-      const htmlContent = `
-        <!DOCTYPE html>
-        <html>
-          <head>
-            <meta charset="UTF-8">
-            <title>机组故障</title>
-            <script>${plotlyContent}</script>
-            <style>
-              body { margin: 0; }
-              #chart { width: 800px; height: 600px; }
-            </style>
-          </head>
-          <body>
-            <div id="chart"></div>
-            <script>
-              window.onload = function() {
-                Plotly.newPlot('chart', ${JSON.stringify(
-                  bubbleData,
-                )}, ${JSON.stringify(layout)},{
-  responsive: true,
-  displayModeBar: false,
-  staticPlot: true
-}).then(() => {
-                  window.chartRendered = true;
-                });
-              };
-            </script>
-          </body>
-        </html>
-      `;
-      await page.setContent(htmlContent, {
-        waitUntil: "domcontentloaded",
-        timeout: 0,
-      });
+  const layout = {
+    title: {
+      text: "机组故障时长与故障次数分析",
+      font: { size: 16 },
+    },
+    xaxis: {
+      title: "故障机组",
+      type: "category",
+      tickangle: 30,
+      tickfont: { size: 12 },
+      gridcolor: "rgb(255,255,255)",
+      tickcolor: "rgb(255,255,255)",
+      backgroundcolor: "#e5ecf6",
+      showbackground: true,
+      showline: true, // ✅ 显示 X 轴轴线
+      zeroline: false,
+      linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+    },
+    yaxis: {
+      title: "故障时长(小时)",
+      tickfont: { size: 12 },
+      gridcolor: "rgb(255,255,255)",
+      tickcolor: "rgb(255,255,255)",
+      backgroundcolor: "#e5ecf6",
+      showbackground: true,
+      zeroline: false,
+      showline: true, // ✅ 显示 X 轴轴线
+      linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+    },
+    plot_bgcolor: "#e5ecf6",
+    paper_bgcolor: "#e5ecf6",
+    margin: { t: 80, b: 120 },
+    showlegend: false, // ✅ 单 trace 不需要 legend
+  };
 
-      await page.waitForTimeout(1000);
-      await page.waitForFunction(() => window.chartRendered === true, {
-        timeout: 60000,
-      });
-      // 截图并保存到临时文件
-      const chartElement = await page.$("#chart");
-      await chartElement.screenshot({ path: tempFilePath, type: "jpeg" });
-      // 上传图片到服务器
-      const formData = new FormData();
-      formData.append("file", fs.createReadStream(tempFilePath));
-      const response = await axios.post(
-        `${process.env.API_BASE_URL}/examples/upload`,
-        {
-          filePath: tempFilePath,
-          bucketName,
-          objectName,
-        },
-      );
-      // console.log("上传成功:", response.data);
-      return response?.data?.url;
-    } catch (error) {
-      console.error("生成图表失败:", error);
-    } finally {
-      await browser.close();
-    }
-  } catch (error) {
-    console.error("发生错误:", error);
-  }
+  // ✅ 🚀 统一出口
+  return await renderChart({
+    traces: [trace],
+    layout,
+    bucketName,
+    objectName,
+  });
 };

+ 115 - 242
downLoadServer/src/server/utils/chartsCom/GeneratorTemperature.js

@@ -1,262 +1,135 @@
 /*
- * @Author: your name
- * @Date: 2025-05-13 14:33:50
- * @LastEditTime: 2026-03-17 09:32:38
- * @LastEditors: MacBookPro
- * @Description: In User Settings Edit
- * @FilePath: /downLoadServer/src/server/utils/chartsCom/GeneratorTemperature.js
+ * 发电机温度图(终极稳定版)
  */
-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";
 
 export const generateGeneratorTemperature = async (
   data,
   bucketName,
   objectName,
 ) => {
-  try {
-    const typeLine = ["solid", "solid", "dot", "dot", "dash", "solid"];
-    // 创建临时目录
-    const tempDir = path.join(process.cwd(), "images");
-    await fs.ensureDir(tempDir);
-    const tempFilePath = path.join(tempDir, `temp_chart_${Date.now()}.jpeg`);
-
-    // 获取 plotly.js 的绝对路径
-    const plotlyPath = path.join(
-      process.cwd(),
-      "src",
-      "public",
-      "js",
-      "plotly-latest.min.js",
-    );
-    const plotlyContent = await fs.readFile(plotlyPath, "utf-8");
-    const colorDefault = [
-      "#0000F5",
-      "#377E21",
-      "#0000F5",
-      "#377E21",
-      "#000000",
-      "#F2A93B",
-    ];
-    // 创建浏览器实例
-    const browser = await puppeteer.launch({
-      headless: "new",
-      // 根据系统改路径
-      executablePath: `${process.env.CHROME_PATH}`, // 根据系统改路径
-
-      args: ["--no-sandbox", "--disable-setuid-sandbox"],
-    });
+  if (!data || !Array.isArray(data.data)) {
+    throw new Error("generator temperature data invalid");
+  }
 
-    try {
-      const page = await browser.newPage();
+  const typeLine = ["solid", "solid", "dot", "dot", "dash", "solid"];
 
-      // 准备图表数据
-      const chartDataset = data.data;
-      const chartData = []; // 用于存储图表配置
+  const colorDefault = [
+    "#0000F5",
+    "#377E21",
+    "#0000F5",
+    "#377E21",
+    "#000000",
+    "#F2A93B",
+  ];
 
-      chartDataset.forEach((turbine, index) => {
-        const chartConfig = {
-          x: turbine.xData,
-          y: turbine.yData,
-          name: turbine.Name,
-          line: {
-            dash: typeLine[index % colorDefault.length],
-            color:
-              data.color1 && data.color1.length > 0
-                ? data.color1[index % data.color1.length]
-                : colorDefault[index % colorDefault.length],
-          },
-          marker: {
-            color:
-              data.color1 && data.color1.length > 0
-                ? data.color1[index % data.color1.length]
-                : colorDefault[index % colorDefault.length],
-          },
-          hovertemplate: `${data.xaixs}: %{x} <br> ${data.yaixs}: %{y} <br>`,
-        };
+  const chartDataset = data.data;
 
-        if (data.chartType === "line") {
-          chartConfig.fill = "none";
-          chartConfig.mode = "lines";
-        } else if (data.chartType === "bar") {
-          chartConfig.fill = "tonexty";
-        }
+  // ✅ traces 构建
+  const traces = chartDataset.map((turbine, index) => {
+    const color =
+      data.color1 && data.color1.length > 0
+        ? data.color1[index % data.color1.length]
+        : colorDefault[index % colorDefault.length];
 
-        chartData.push(chartConfig);
-      });
+    const base = {
+      x: turbine.xData || [],
+      y: turbine.yData || [],
+      name: turbine.Name || `series-${index}`,
+      hovertemplate: `${data.xaixs || "X"}: %{x}<br>${
+        data.yaixs || "Y"
+      }: %{y}<br>`,
+      marker: { color },
+    };
 
-      const layout = {
-        title: {
-          text: "发电机-轴承温度偏差:" + data.turbineName,
-          // text: data.title,
-          font: {
-            size: 16,
-            weight: "bold",
-          },
-        },
-        xaxis: {
-          title: data.xaixs || "X轴",
-          gridcolor: "rgb(255,255,255)",
-          tickcolor: "rgb(255,255,255)",
-          backgroundcolor: "#e5ecf6",
-        },
-        yaxis: {
-          title: data.yaixs || "Y轴",
-          gridcolor: "rgb(255,255,255)",
-          tickcolor: "rgb(255,255,255)",
-          backgroundcolor: "#e5ecf6",
-        },
-        margin: {
-          l: 50,
-          r: 50,
-          t: 50,
-          b: 50,
-        },
-        plot_bgcolor: "#e5ecf6",
-        gridcolor: "#fff",
-        bgcolor: "#e5ecf6",
-        autosize: true,
-        barmode: data.chartType === "bar" ? "stack" : "group",
-        shapes: [
-          {
-            type: "line",
-            xref: "paper",
-            x0: 0,
-            x1: 1,
-            yref: "y",
-            y0: 15,
-            y1: 15,
-            line: {
-              color: "red",
-              width: 2,
-              dash: "dash",
-            },
-          },
-          {
-            type: "line",
-            xref: "paper",
-            x0: 0,
-            x1: 1,
-            yref: "y",
-            y0: -15,
-            y1: -15,
-            line: {
-              color: "red",
-              width: 2,
-              dash: "dash",
-            },
-          },
-          {
-            type: "line",
-            xref: "paper",
-            x0: 0,
-            x1: 1,
-            yref: "y",
-            y0: 5,
-            y1: 5,
-            line: {
-              color: "#F9DD70",
-              width: 2,
-              dash: "dash",
-            },
-          },
-          {
-            type: "line",
-            xref: "paper",
-            x0: 0,
-            x1: 1,
-            yref: "y",
-            y0: -5,
-            y1: -5,
-            line: {
-              color: "#F9DD70",
-              width: 2,
-              dash: "dash",
-            },
-          },
-          {
-            type: "line",
-            xref: "paper",
-            x0: 0,
-            x1: 1,
-            yref: "y",
-            y0: 0,
-            y1: 0,
-            line: {
-              color: "#fff", // 设置为黑色
-              width: 2,
-            },
-          },
-        ],
+    if (data.chartType === "bar") {
+      return {
+        ...base,
+        type: "bar",
       };
+    }
 
-      // 创建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() {
-                    Plotly.newPlot('chart', ${JSON.stringify(
-                      chartData,
-                    )}, ${JSON.stringify(layout)},{
-  responsive: true,
-  displayModeBar: false,
-  staticPlot: true
-}).then(() => {
-                      window.chartRendered = true;
-                    });
-                  };
-                </script>
-              </body>
-            </html>
-          `;
-
-      // 设置页面内容
-      await page.setContent(htmlContent, {
-        waitUntil: "domcontentloaded",
-        timeout: 0,
-      });
-
-      await page.waitForTimeout(1000);
+    // 默认 line
+    return {
+      ...base,
+      type: "scatter",
+      mode: "lines",
+      line: {
+        dash: typeLine[index % typeLine.length],
+        color,
+      },
+    };
+  });
 
-      // 等待图表渲染完成
-      await page.waitForFunction(() => window.chartRendered === true, {
-        timeout: 60000,
-      });
+  // ✅ layout
+  const layout = {
+    title: {
+      text: `发电机-轴承温度偏差: ${data.turbineName || ""}`,
+      font: { size: 16 },
+    },
+    xaxis: {
+      title: data.xaixs || "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: data.yaixs || "Y轴",
+      gridcolor: "rgb(255,255,255)",
+      tickcolor: "rgb(255,255,255)",
+      backgroundcolor: "#e5ecf6",
+      showbackground: true,
+      showline: true, // ✅ 显示 X 轴轴线
+      linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+      zeroline: false,
+    },
+    plot_bgcolor: "#e5ecf6",
+    paper_bgcolor: "#e5ecf6",
+    margin: {
+      l: 50,
+      r: 50,
+      t: 60,
+      b: 50,
+    },
+    barmode: data.chartType === "bar" ? "stack" : "group",
 
-      // 截图并保存到临时文件
-      const chartElement = await page.$("#chart");
-      await chartElement.screenshot({
-        path: tempFilePath,
-        type: "jpeg",
-      });
+    // ✅ 阈值线(核心保留)
+    shapes: [
+      createThresholdLine(15, "red"),
+      createThresholdLine(-15, "red"),
+      createThresholdLine(5, "#F9DD70"),
+      createThresholdLine(-5, "#F9DD70"),
+      createThresholdLine(0, "#fff"),
+    ],
+  };
 
-      // 上传图片到服务器
-      const formData = new FormData();
-      formData.append("file", fs.createReadStream(tempFilePath));
-      const response = await axios.post(
-        `${process.env.API_BASE_URL}/examples/upload`,
-        { filePath: tempFilePath, bucketName, objectName },
-      );
-      return response?.data?.url;
-    } finally {
-      await browser.close();
-    }
-  } catch (error) {
-    console.error("生成图表失败:", error);
-    throw error;
-  }
+  // ✅ 统一出口
+  return await renderChart({
+    traces,
+    layout,
+    bucketName,
+    objectName,
+  });
 };
+
+// ✅ 抽离:阈值线生成器(非常推荐)
+function createThresholdLine(y, color) {
+  return {
+    type: "line",
+    xref: "paper",
+    x0: 0,
+    x1: 1,
+    yref: "y",
+    y0: y,
+    y1: y,
+    line: {
+      color,
+      width: 2,
+      dash: y === 0 ? "solid" : "dash",
+    },
+  };
+}

+ 93 - 169
downLoadServer/src/server/utils/chartsCom/HeatmapCharts.js

@@ -1,187 +1,111 @@
 /*
- * @Author: your name
- * @Date: 2025-04-14 11:15:35
- * @LastEditTime: 2026-03-17 09:32:48
- * @LastEditors: MacBookPro
- * @Description: In User Settings Edit
- * @FilePath: /performance-test/downLoadServer/src/server/utils/chartsCom/HeatmapCharts.js
+ * 热力图(终极稳定版)
  */
+import { renderChart } from "../chartService/index.js";
 
-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";
 export const generateHeatmapChart = async (data, bucketName, objectName) => {
-  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) || data.data.length === 0) {
+    throw new Error("heatmap data invalid");
+  }
 
-    // 获取 plotly.js 的绝对路径
-    const plotlyPath = path.join(
-      process.cwd(),
-      "src",
-      "public",
-      "js",
-      "plotly-3.0.1.min.js",
-    );
-    const plotlyContent = await fs.readFile(plotlyPath, "utf-8");
+  const chartDataset = data.data[0];
 
-    // 创建浏览器实例
-    const browser = await puppeteer.launch({
-      headless: "new",
-      // 根据系统改路径
-      executablePath: `${process.env.CHROME_PATH}`, // 根据系统改路径
+  const xData = chartDataset.xData || [];
+  const yData = chartDataset.yData || [];
+  const zData = chartDataset.ZData || [];
 
-      args: ["--no-sandbox", "--disable-setuid-sandbox"],
-    });
+  // ✅ 校验二维数组(很关键)
+  if (!Array.isArray(zData) || !Array.isArray(zData[0])) {
+    throw new Error("ZData must be 2D array");
+  }
 
-    try {
-      const page = await browser.newPage();
+  // ✅ 数据量控制(防止内存炸)
+  const MAX_SIZE = 200; // 200x200 已经很大了
+  if (zData.length > MAX_SIZE || zData[0].length > MAX_SIZE) {
+    console.warn("heatmap too large, may affect performance");
+  }
 
-      // 准备图表数据
-      const chartDataset = data.data[0];
+  // ✅ 是否显示文本(大数据关闭)
+  const showText = zData.length * zData[0].length <= 2500;
 
-      const trace = {
-        type: "heatmap",
-        x: chartDataset.xData,
-        y: chartDataset.yData,
-        z: chartDataset.ZData,
-        zmin: 0, // 最小值
-        zmax: 100, // 最大值
-        colorscale: [
-          [0, "#E1ECC5"], // 0% - 50%
-          [0.5, "#E1ECC5"], // 50%
-          [0.5, "#9BC8C1"], // 50% - 85%
-          [0.85, "#9BC8C1"], // 85%
-          [0.85, "#5783B3"], // 85% - 100%
-          [1, "#5783B3"], // 100%
-        ],
-        text: chartDataset.ZData.map((row) =>
-          row.map((value) => value.toString()),
-        ),
-        hoverinfo: "text", // Hover时显示文本
-        showscale: true, // 显示颜色条
-        texttemplate: "%{text}", // 在热图块上显示z值
-        xgap: 2, // 设置水平方向格子之间的间距
-        ygap: 2, // 设置垂直方向格子之间的间距
-      };
+  const trace = {
+    type: "heatmap",
+    x: xData,
+    y: yData,
+    z: zData,
 
-      // 准备布局配置
-      const layout = {
-        showlegend: false,
-        autosize: true,
-        title: {
-          text: chartDataset.title,
-          font: {
-            size: 16,
-            weight: "bold",
-          },
-        },
-        xaxis: {
-          title: data.xaixs || "X轴",
-          tickvals: chartDataset.xData,
-          ticktext: chartDataset.xData,
-          tickmode: "array",
-          tickfont: {
-            size: 12,
-          },
-        },
-        yaxis: {
-          title: data.yaixs || "Y轴",
-          tickvals: chartDataset.yData,
-          ticktext: chartDataset.yData,
-          tickmode: "array",
-          tickfont: {
-            size: 12,
-          },
-        },
-        plot_bgcolor: "white",
-        gridcolor: "#d3d3d3", // 设置网格线颜色
-      };
+    zmin: 0,
+    zmax: 100,
 
-      // 准备 HTML 内容
-      const htmlContent = `
-        <!DOCTYPE html>
-        <html>
-        <head>
-          <meta charset="UTF-8">
-          <title>热力图</title>
-          <script>${plotlyContent}</script>
-        </head>
-        <body>
-          <div id="chart" style="width: 100%; height: 450px"></div>
-          <script>
-            window.chartRendered = false;
-            const trace = ${JSON.stringify(trace)};
-            const layout = ${JSON.stringify(layout)};
-            Plotly.newPlot('chart', [trace], layout, {
-  responsive: true,
-  displayModeBar: false,
-  staticPlot: true
-}).then(() => {
-              window.chartRendered = true;
-            }); 
-          </script>
-        </body>
-        </html>
-      `;
-      // 设置页面内容
-      await page.setContent(htmlContent, {
-        waitUntil: "domcontentloaded",
-        timeout: 0,
-      });
+    colorscale: [
+      [0, "#E1ECC5"],
+      [0.5, "#E1ECC5"],
+      [0.5, "#9BC8C1"],
+      [0.85, "#9BC8C1"],
+      [0.85, "#5783B3"],
+      [1, "#5783B3"],
+    ],
 
-      await page.waitForTimeout(1000);
+    // ✅ 只在小数据量显示文字
+    ...(showText && {
+      text: zData.map((row) => row.map((v) => String(v))),
+      texttemplate: "%{text}",
+    }),
 
-      // 等待图表渲染完成,延长超时时间
-      await page.waitForFunction(() => window.chartRendered === true, {
-        timeout: 120000, // 延长到 120 秒
-      });
+    hovertemplate: "X: %{x}<br>Y: %{y}<br>值: %{z}<extra></extra>",
 
-      // 截图并保存到临时文件
-      const chartElement = await page.$("#chart");
-      await chartElement.screenshot({
-        path: tempFilePath,
-        type: "jpeg",
-      });
+    showscale: true,
+    xgap: 2,
+    ygap: 2,
+  };
 
-      // 上传图片到服务器
-      const formData = new FormData();
-      formData.append("file", fs.createReadStream(tempFilePath));
-      //   const response = await axios.post(
-      //     "http://10.10.10.11:8080/upload",
-      //     formData,
-      //     {
-      //       headers: {
-      //         ...formData.getHeaders(),
-      //       },
-      //     }
-      //   );
+  const layout = {
+    title: {
+      text: chartDataset.title || "热力图",
+      font: { size: 16 },
+    },
+    xaxis: {
+      title: data.xaixs || "X轴",
+      tickmode: "array",
+      tickvals: xData,
+      ticktext: xData,
+      tickfont: { size: 12 },
+      gridcolor: "rgb(255,255,255)",
+      tickcolor: "rgb(255,255,255)",
+      backgroundcolor: "#e5ecf6",
+      showbackground: true,
+      zeroline: false,
+      showline: true, // ✅ 显示 X 轴轴线
+      linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+    },
+    yaxis: {
+      title: data.yaixs || "Y轴",
+      tickmode: "array",
+      tickvals: yData,
+      ticktext: yData,
+      tickfont: { size: 12 },
+      gridcolor: "rgb(255,255,255)",
+      tickcolor: "rgb(255,255,255)",
+      backgroundcolor: "#e5ecf6",
+      showbackground: true,
+      zeroline: false,
+      showline: true, // ✅ 显示 X 轴轴线
+      linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+    },
+    plot_bgcolor: "#ffffff",
+    paper_bgcolor: "#ffffff",
+    margin: {
+      t: 60,
+      b: 60,
+    },
+    showlegend: false,
+  };
 
-      // 返回图片URL
-      //   return response.data.data;
-      // return formData;
-      // 发送上传请求
-      const response = await axios.post(
-        `${process.env.API_BASE_URL}/examples/upload`,
-        { filePath: tempFilePath, bucketName, objectName },
-      );
-      return response?.data?.url;
-    } catch (error) {
-      // console.error("生成热力图失败:", error);
-      throw error;
-    } finally {
-      await browser.close();
-    }
-  } catch (error) {
-    // console.error("生成热力图失败:", error);
-    throw error;
-  }
+  // ✅ 🚀 统一出口
+  return await renderChart({
+    traces: [trace],
+    layout,
+    bucketName,
+    objectName,
+  });
 };

+ 93 - 151
downLoadServer/src/server/utils/chartsCom/PlotlyCharts.js

@@ -1,87 +1,114 @@
-/*
- * @Author: your name
- * @Date: 2025-05-15 15:22:19
- * @LastEditTime: 2026-03-17 09:33:25
- * @LastEditors: MacBookPro
- * @Description: In User Settings Edit
- * @FilePath: /downLoadServer/src/server/utils/chartsCom/PlotlyCharts.js
+import { renderChart } from "../chartService/index.js";
+import { colorSchemes } from "../colors.js";
+
+/**
+ * 数据过滤(可选)
  */
+const filterData = (group, startDate, endDate) => {
+  if (!startDate || !endDate) return group;
 
-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 { text } from "stream/consumers";
+  const xData = [];
+  const yData = [];
+
+  group.xData.forEach((x, i) => {
+    const date = new Date(x);
+    if (date >= new Date(startDate) && date <= new Date(endDate)) {
+      xData.push(x);
+      yData.push(group.yData[i]);
+    }
+  });
+
+  return { ...group, xData, yData };
+};
 
 /**
- * 生成折线图并上传
- * @param {Object} options - 配置选项
- * @param {string} options.fileAddr - 数据文件地址
- * @param {string[]} options.color1 - 颜色数组1
- * @param {string[]} options.colors - 颜色数组2
- * @returns {Promise<String>} - 返回图片URL
+ * ✅ 终极版 Plotly 图表生成
  */
 export const generatePlotlyCharts = async (
   chartData,
   bucketName,
   objectName,
+  startDate,
+  endDate,
 ) => {
-  const colorSchemesItem = colorSchemes[0].colors;
-  const tempDir = path.join(process.cwd(), "images");
-  const tempFilePath = path.join(tempDir, `temp_line_chart_${Date.now()}.jpeg`);
-  let browser;
-
   try {
-    // 构建数据
-    const data = [];
-    const newData = chartData.data.filter(
-      (item) => item.enginName !== "合同功率曲线",
-    );
+    const colorSchemesItem = colorSchemes[0].colors;
 
-    newData.forEach((turbine, index) => {
-      data.push({
-        x: turbine.xData,
-        y: turbine.yData,
-        name: turbine.enginName,
-        mode: "lines",
-        connectgaps: false,
-        line: { color: colorSchemesItem[index % colorSchemesItem.length] },
-        marker: { color: colorSchemesItem[index % colorSchemesItem.length] },
-      });
+    // ✅ 1. 分离数据(更安全)
+    const normalData = [];
+    let contractLine = null;
+
+    chartData.data.forEach((item) => {
+      if (item.enginName === "合同功率曲线") {
+        contractLine = item;
+      } else {
+        normalData.push(item);
+      }
     });
 
-    if (
-      chartData.data[chartData.data.length - 1] &&
-      chartData.data[chartData.data.length - 1].enginName === "合同功率曲线" &&
-      chartData.data[chartData.data.length - 1].yData.length > 0
-    ) {
-      data.push({
-        x: chartData.data[chartData.data.length - 1].xData,
-        y: chartData.data[chartData.data.length - 1].yData,
+    // ✅ 2. 数据过滤(你之前缺失的)
+    const filteredData = normalData.map((group) =>
+      filterData(group, startDate, endDate),
+    );
+
+    // ✅ 3. 构建 traces
+    const traces = filteredData.map((turbine, index) => ({
+      x: turbine.xData,
+      y: turbine.yData,
+      name: turbine.enginName,
+      type: "scatter",
+      mode: "lines",
+      connectgaps: false,
+      line: {
+        color: colorSchemesItem[index % colorSchemesItem.length],
+      },
+      marker: {
+        color: colorSchemesItem[index % colorSchemesItem.length],
+      },
+    }));
+
+    // ✅ 4. 合同功率曲线(更稳)
+    if (contractLine && contractLine.yData?.length) {
+      traces.push({
+        x: contractLine.xData,
+        y: contractLine.yData,
+        type: "scatter",
         mode: "lines+markers",
         name: "合同功率曲线",
-        line: {
-          color: "red",
-          width: 1,
-        },
+        line: { color: "red", width: 1 },
         marker: { color: "red", size: 4 },
       });
     }
 
+    // ✅ 5. layout
     const layout = {
       title: {
         text: `风速功率曲线分析${chartData.engineTypeName}机型`,
-        font: { size: 16, weight: "bold" },
+        font: { size: 16 },
+      },
+      xaxis: {
+        title: "风速(m/s)",
+        gridcolor: "rgb(255,255,255)",
+        tickcolor: "rgb(255,255,255)",
+        backgroundcolor: "#e5ecf6",
+        showbackground: true,
+        showline: true, // ✅ 显示 X 轴轴线
+        zeroline: false,
+        linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+      },
+      yaxis: {
+        title: "功率(kW)",
+        gridcolor: "rgb(255,255,255)",
+        tickcolor: "rgb(255,255,255)",
+        backgroundcolor: "#e5ecf6",
+        showbackground: true,
+        showline: true, // ✅ 显示 X 轴轴线
+        zeroline: false,
+        linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
       },
-      xaxis: { title: { text: "风速(m / s)" || "X轴" }, gridcolor: "#fff" },
-      yaxis: { title: { text: "功率(kW)" || "Y轴" }, gridcolor: "#fff" },
       margin: { l: 50, r: 50, t: 50, b: 50 },
       plot_bgcolor: "#e5ecf6",
-      bgcolor: "#e5ecf6",
-      autosize: true,
-      barmode: "group",
+      paper_bgcolor: "#e5ecf6",
       legend: {
         orientation: "h",
         xanchor: "center",
@@ -90,102 +117,17 @@ export const generatePlotlyCharts = async (
       },
     };
 
-    await fs.ensureDir(tempDir);
-    const plotlyPath = path.join(
-      process.cwd(),
-      "src",
-      "public",
-      "js",
-      "plotly-latest.min.js",
-    );
-    const plotlyContent = await fs.readFile(plotlyPath, "utf-8");
-
-    browser = await puppeteer.launch({
-      headless: "new",
-      // 根据系统改路径
-      executablePath: `${process.env.CHROME_PATH}`, // 根据系统改路径
-
-      args: ["--no-sandbox", "--disable-setuid-sandbox"],
-    });
-
-    const page = await browser.newPage();
-
-    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(data)};
-              const layout = ${JSON.stringify(layout)};
-              Plotly.newPlot('chart', data, layout, {
-  responsive: true,
-  displayModeBar: false,
-  staticPlot: true
-}).then(() => {
-                window.chartRendered = true;
-              });
-            };
-          </script>
-        </body>
-      </html>`;
-
-    await page.setContent(htmlContent, {
-      waitUntil: "domcontentloaded",
-      timeout: 0,
-    });
-
-    await page.waitForTimeout(1000);
-
-    await page.waitForFunction(() => window.chartRendered === true, {
-      timeout: 60000,
+    // ✅ 6. 统一渲染(核心优化点🔥)
+    const url = await renderChart({
+      traces,
+      layout,
+      bucketName,
+      objectName,
     });
 
-    const chartElement = await page.$("#chart");
-    await chartElement.screenshot({ path: tempFilePath, type: "jpeg" });
-
-    // ✅ 上传前判断文件是否存在
-    if (!(await fs.pathExists(tempFilePath))) {
-      throw new Error("图表截图文件未生成");
-    }
-
-    // ✅ 发起上传
-    try {
-      const response = await axios.post(
-        `${process.env.API_BASE_URL}/examples/upload`,
-        {
-          filePath: tempFilePath,
-          bucketName,
-          objectName,
-        },
-      );
-
-      const imageUrl = response?.data?.url;
-
-      return imageUrl;
-    } catch (uploadError) {
-      throw uploadError;
-    } finally {
-      // ✅ 上传后安全删除
-      try {
-        if (await fs.pathExists(tempFilePath)) {
-          await fs.unlink(tempFilePath);
-        }
-      } catch (deleteError) {
-        console.warn("⚠️ 删除临时文件失败:", deleteError.message);
-      }
-    }
+    return url;
   } catch (error) {
-    console.error("❌ 生成折线图失败:", error.message);
+    console.error("❌ 生成图表失败:", error);
     throw error;
-  } finally {
-    if (browser) {
-      await browser.close();
-    }
   }
 };

+ 11 - 16
downLoadServer/src/server/utils/chartsCom/PlotlyChartsFen.js

@@ -1,11 +1,3 @@
-/*
- * @Author: your name
- * @Date: 2025-05-23 17:19:20
- * @LastEditTime: 2026-03-17 09:33:54
- * @LastEditors: MacBookPro
- * @Description: In User Settings Edit
- * @FilePath: /downLoadServer/src/server/utils/chartsCom/PlotlyChartsFen.js
- */
 import puppeteer from "puppeteer";
 import fs from "fs-extra";
 import path from "path";
@@ -127,12 +119,20 @@ export const generatePlotlyChartsFen = async (
           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,
@@ -169,10 +169,8 @@ export const generatePlotlyChartsFen = async (
                 const data = ${JSON.stringify(finalData)};
                 const layout = ${JSON.stringify(layout)};
                 Plotly.newPlot('chart', data, layout, {
-  responsive: true,
-  displayModeBar: false,
-  staticPlot: true
-}).then(() => {
+                  responsive: true
+                }).then(() => {
                   window.chartRendered = true;
                 });
               };
@@ -183,12 +181,9 @@ export const generatePlotlyChartsFen = async (
 
       // 设置页面内容
       await page.setContent(htmlContent, {
-        waitUntil: "domcontentloaded",
-        timeout: 0,
+        waitUntil: "networkidle0",
       });
 
-      await page.waitForTimeout(1000);
-
       // 等待图表渲染完成
       await page.waitForFunction(() => window.chartRendered === true, {
         timeout: 60000,

+ 13 - 17
downLoadServer/src/server/utils/chartsCom/Time3DChart.js

@@ -1,11 +1,3 @@
-/*
- * @Author: your name
- * @Date: 2025-04-14 17:49:33
- * @LastEditTime: 2026-03-17 09:34:32
- * @LastEditors: MacBookPro
- * @Description: In User Settings Edit
- * @FilePath: /performance-test/downLoadServer/src/server/utils/chartsCom/Time3DChart.js
- */
 import puppeteer from "puppeteer";
 import fs from "fs-extra";
 import path from "path";
@@ -21,6 +13,11 @@ const formatDate = (dateString) => {
 };
 export const generateTime3DChart = async (data, bucketName, objectName) => {
   try {
+    const timeoutMs = Number.isFinite(
+      Number.parseInt(process.env.CHART_RENDER_TIMEOUT_MS || "", 10),
+    )
+      ? Number.parseInt(process.env.CHART_RENDER_TIMEOUT_MS || "", 10)
+      : 500000;
     // 创建临时目录
     const tempDir = path.join(process.cwd(), "images");
     await fs.ensureDir(tempDir);
@@ -49,10 +46,13 @@ export const generateTime3DChart = async (data, bucketName, objectName) => {
         "--disable-setuid-sandbox",
         "--window-size=1920,1080",
       ],
+      protocolTimeout: timeoutMs,
     });
 
     try {
       const page = await browser.newPage();
+      page.setDefaultTimeout(timeoutMs);
+      page.setDefaultNavigationTimeout(timeoutMs);
       // 准备图表数据
       const uniqueMonths = Array.from(
         new Set(data.data[0].yData.map((date) => formatDate(date))),
@@ -101,6 +101,7 @@ export const generateTime3DChart = async (data, bucketName, objectName) => {
               text: data.xaixs,
               standoff: 100,
             },
+            zeroline: false,
           },
           yaxis: {
             type: "category", // 让 Y 轴按类别均匀分布
@@ -118,6 +119,7 @@ export const generateTime3DChart = async (data, bucketName, objectName) => {
             tickcolor: "black",
             zeroline: false,
             tickangle: 25,
+            zeroline: false,
             title: {
               text: data.yaixs,
             },
@@ -195,11 +197,7 @@ export const generateTime3DChart = async (data, bucketName, objectName) => {
           <script>
             const traces = ${JSON.stringify(traces)};
             const layout = ${JSON.stringify(layout)};
-            Plotly.newPlot('chart', traces, layout, {
-  responsive: true,
-  displayModeBar: false,
-  staticPlot: true
-}).then(() => {
+            Plotly.newPlot('chart', traces, layout, { responsive: true }).then(() => {
               window.chartRendered = true; // 确保在图表渲染完成后设置
               
             }).catch((error) => {
@@ -212,14 +210,12 @@ export const generateTime3DChart = async (data, bucketName, objectName) => {
       // 设置页面内容
       await page.setContent(htmlContent, {
         waitUntil: "domcontentloaded",
-        timeout: 0,
+        timeout: timeoutMs,
       });
 
-      await page.waitForTimeout(1000);
-
       // 等待图表渲染完成,延长超时时间
       await page.waitForFunction(() => window.chartRendered === true, {
-        timeout: 150000, // 延长到 120 秒
+        timeout: timeoutMs,
       });
 
       // 截图并保存到临时文件

+ 111 - 204
downLoadServer/src/server/utils/chartsCom/TwoDMarkersChart.js

@@ -1,18 +1,5 @@
-/*
- * @Author: your name
- * @Date: 2025-04-25 16:19:33
- * @LastEditTime: 2026-03-17 09:34:40
- * @LastEditors: MacBookPro
- * @Description: In User Settings Edit
- * @FilePath: /downLoadServer/src/server/utils/chartsCom/TwoDMarkersChart.js
- */
-
-import puppeteer from "puppeteer";
-import fs from "fs-extra";
-import path from "path";
-import FormData from "form-data";
+import { renderChart } from "../chartService/index.js";
 import { colorSchemes } from "../colors.js";
-import axios from "axios";
 
 export const generateTwoDMarkersChart = async (
   data,
@@ -20,199 +7,119 @@ export const generateTwoDMarkersChart = async (
   objectName,
 ) => {
   try {
-    // 创建临时目录
-    const tempDir = path.join(process.cwd(), "images");
-    await fs.ensureDir(tempDir);
-    const tempFilePath = path.join(
-      tempDir,
-      `temp_scatter_chart_${Date.now()}.jpeg`,
-    );
-
-    // 获取 plotly.js 的绝对路径
-    const plotlyPath = path.join(
-      process.cwd(),
-      "src",
-      "public",
-      "js",
-      "plotly-3.0.1.min.js",
-    );
-    const plotlyContent = await fs.readFile(plotlyPath, "utf-8");
-
-    // 创建浏览器实例
-    const browser = await puppeteer.launch({
-      headless: "new",
-      // 根据系统改路径
-      executablePath: `${process.env.CHROME_PATH}`, // 根据系统改路径
-
-      args: ["--no-sandbox", "--disable-setuid-sandbox"],
-    });
+    const chartDataset = data.data?.[0];
+    if (!chartDataset) {
+      throw new Error("2D散点数据为空");
+    }
 
-    try {
-      const page = await browser.newPage();
-
-      // 准备图表数据
-      const chartDataset = data.data[0];
-      const uniqueTimeLabels =
-        chartDataset.colorbar &&
-        chartDataset.colorbar.length === chartDataset.xData.length
-          ? [...new Set(chartDataset.colorbar)]
-          : [...new Set(chartDataset.yData)];
-
-      const ticktext = uniqueTimeLabels.map((label) => label);
-      const tickvals = uniqueTimeLabels.map((_, index) => index + 1);
-      const timeMapping = uniqueTimeLabels.reduce((acc, curr, index) => {
-        acc[curr] = index + 1;
-        return acc;
-      }, {});
-
-      // 获取 colorbar 的最小值和最大值来计算比例值
-      const minValue = Math.min(...new Set(uniqueTimeLabels));
-      const maxValue = Math.max(...new Set(uniqueTimeLabels));
-      const colorStops = [
-        colorSchemes[0].colors[0],
-        colorSchemes[0].colors[4],
-        colorSchemes[0].colors[8],
-        colorSchemes[0].colors[12],
-      ];
-      // 计算渐变比例
-      const colors = colorStops.map((color, index) => {
-        const proportion = index / (colorStops.length - 1); // 计算比例值 (0, 1/3, 2/3, 1)
-        return [proportion, color]; // 创建比例-颜色映射
-      });
-      // 确保 colors 至少有 2 种颜色,否则使用默认颜色
-      if (colors.length < 2) {
-        colors.push([1, colorStops[colorStops.length - 1] || "#1B2973"]);
-      }
-
-      // 计算颜色值映射
-      let colorValues =
-        chartDataset.colorbar &&
-        chartDataset.colorbar.length === chartDataset.xData.length
-          ? chartDataset.colorbar.map((date) => timeMapping[date])
-          : chartDataset.yData.map((date) => timeMapping[date]);
-
-      // 绘制 2D 散点图
-      const trace = {
-        x: chartDataset.xData,
-        y: chartDataset.yData,
-        mode: "markers",
-        type: "scattergl", // 使用 scattergl 提高性能
-        text: chartDataset.engineName, // 提示文本
-        marker: {
-          color: colorValues,
-          colorscale: [
-            [0, "#F9FDD2"],
-            [0.15, "#E9F6BD"],
-            [0.3, "#C2E3B9"],
-            [0.45, "#8AC8BE"],
-            [0.6, "#5CA8BF"],
-            [0.75, "#407DB3"],
-            [0.9, "#2E4C9A"],
-            [1, "#1B2973"],
-          ],
-          size: new Array(chartDataset.xData.length).fill(6), // 点的大小
-        },
-      };
-
-      // 图表布局
-      const layout = {
-        title: {
-          text: chartDataset.title,
-          font: {
-            size: 16,
-            weight: "bold",
-          },
-        },
-        xaxis: {
-          title: {
-            text: data.xaixs,
-          },
-          gridcolor: "rgb(255,255,255)",
-          tickcolor: "rgb(255,255,255)",
-          backgroundcolor: "#e5ecf6",
-          showbackground: true,
-        },
-        yaxis: {
-          title: {
-            text: data.yaixs,
-          },
-          gridcolor: "rgb(255,255,255)",
-          tickcolor: "rgb(255,255,255)",
-          backgroundcolor: "#e5ecf6",
-          showbackground: true,
-        },
-        // showlegend: true,
-        plot_bgcolor: "#e5ecf6",
-        gridcolor: "#fff",
-      };
-
-      // 准备 HTML 内容
-      const htmlContent = `
-        <!DOCTYPE html>
-        <html>
-        <head>
-          <meta charset="UTF-8">
-          <title>2D 散点图</title>
-          <script>${plotlyContent}</script>
-        </head>
-        <body>
-          <div id="chart" style="width: 100%; height: 600px"></div>
-          <script>
-            const traces = [${JSON.stringify(trace)}];
-            const layout = ${JSON.stringify(layout)};
-            Plotly.newPlot('chart', traces, layout, {
-  responsive: true,
-  displayModeBar: false,
-  staticPlot: true
-}).then(() => {
-              window.chartRendered = true; // 确保在图表渲染完成后设置
-              console.log("图表渲染完成");
-            }).catch((error) => {
-              console.error("图表渲染错误:", error); // 捕获渲染错误
-            });
-          </script>
-        </body>
-        </html>
-      `;
-
-      // 设置页面内容
-      await page.setContent(htmlContent, {
-        waitUntil: "domcontentloaded",
-        timeout: 0,
-      });
-
-      await page.waitForTimeout(1000);
-
-      // 等待图表渲染完成,延长超时时间
-      await page.waitForFunction(() => window.chartRendered === true, {
-        timeout: 150000, // 延长到 150 秒
-      });
-
-      // 截图并保存到临时文件
-      const chartElement = await page.$("#chart");
-      await chartElement.screenshot({
-        path: tempFilePath,
-        type: "jpeg",
-      });
-
-      // 上传图片到服务器
-      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;
-    } catch (error) {
-      console.error("生成2D散点图失败:", error);
-      throw error;
-    } finally {
-      await browser.close();
+    const { xData, yData, title, engineName } = chartDataset;
+
+    // ✅ 1. color 数据源(统一逻辑)
+    const rawColorData =
+      chartDataset.colorbar?.length === xData.length
+        ? chartDataset.colorbar
+        : yData;
+
+    // ✅ 2. 唯一标签
+    const uniqueLabels = [...new Set(rawColorData)];
+
+    const tickvals = uniqueLabels.map((_, i) => i + 1);
+    const ticktext = uniqueLabels;
+
+    const mapping = uniqueLabels.reduce((acc, cur, i) => {
+      acc[cur] = i + 1;
+      return acc;
+    }, {});
+
+    // ✅ 3. 颜色渐变(安全版)
+    const colors = colorSchemes?.[0]?.colors || [];
+
+    const safePick = (i) => colors[i] || colors[0] || "#1B2973";
+
+    const colorStops = [safePick(0), safePick(4), safePick(8), safePick(12)];
+
+    const colorscale = colorStops.map((c, i) => [
+      i / (colorStops.length - 1),
+      c,
+    ]);
+
+    if (colorscale.length < 2) {
+      colorscale.push([1, safePick(0)]);
     }
+
+    // ✅ 4. 映射颜色值
+    const colorValues = rawColorData.map((v) => mapping[v]);
+
+    // ⚠️ 性能优化(点很多时很关键🔥)
+    const pointCount = xData.length;
+    const markerSize = pointCount > 5000 ? 3 : 6;
+
+    // ✅ 5. trace
+    const trace = {
+      x: xData,
+      y: yData,
+      type: "scattergl",
+      mode: "markers",
+      name: engineName || "散点图",
+      marker: {
+        color: colorValues,
+        colorscale,
+        size: markerSize,
+        colorbar: {
+          title: { text: chartDataset.colorbartitle || "分类" },
+          tickvals,
+          ticktext,
+        },
+      },
+      customdata: rawColorData,
+      hovertemplate:
+        `${data.xaixs}: %{x}<br>` +
+        `${data.yaixs}: %{y}<br>` +
+        `分类: %{customdata}<extra></extra>`,
+    };
+
+    // ✅ 6. layout(简化+统一风格)
+    const layout = {
+      title: {
+        text: title || "2D散点图",
+        font: { size: 16 },
+      },
+      xaxis: {
+        title: data.xaixs,
+        gridcolor: "rgb(255,255,255)",
+        tickcolor: "rgb(255,255,255)",
+        backgroundcolor: "#e5ecf6",
+        showbackground: true,
+        showline: true, // ✅ 显示 X 轴轴线
+        zeroline: false,
+        linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+      },
+      yaxis: {
+        title: data.yaixs,
+        gridcolor: "rgb(255,255,255)",
+        tickcolor: "rgb(255,255,255)",
+        backgroundcolor: "#e5ecf6",
+        showbackground: true,
+        showline: true, // ✅ 显示 X 轴轴线
+        zeroline: false,
+        linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+      },
+      showlegend: false,
+      plot_bgcolor: "#e5ecf6",
+      paper_bgcolor: "#e5ecf6",
+    };
+
+    // ✅ 7. 渲染
+    const url = await renderChart({
+      traces: [trace],
+      layout,
+      bucketName,
+      objectName,
+    });
+
+    return url;
   } catch (error) {
-    console.error("生成2D散点图失败:", error);
+    console.error("❌ 生成2D散点图失败:", error);
     throw error;
   }
 };

+ 132 - 195
downLoadServer/src/server/utils/chartsCom/TwoDMarkersChart1.js

@@ -1,9 +1,5 @@
-import puppeteer from "puppeteer";
-import fs from "fs-extra";
-import path from "path";
-import FormData from "form-data";
+import { renderChart } from "../chartService/index.js";
 import { colorSchemes } from "../colors.js";
-import axios from "axios";
 
 export const generateTwoDMarkersChart1 = async (
   data,
@@ -11,210 +7,151 @@ export const generateTwoDMarkersChart1 = async (
   objectName,
 ) => {
   try {
-    // 创建临时目录
-    const tempDir = path.join(process.cwd(), "images");
-    await fs.ensureDir(tempDir);
-    const tempFilePath = path.join(
-      tempDir,
-      `temp_scatter_chart_${Date.now()}.jpeg`,
+    // ✅ 1. 数据拆分(修复 enginName 拼写问题🔥)
+    const scatterData = data.data.find(
+      (item) => item.engineName !== "合同功率曲线",
     );
 
-    // 获取 plotly.js 的绝对路径
-    const plotlyPath = path.join(
-      process.cwd(),
-      "src",
-      "public",
-      "js",
-      "plotly-3.0.1.min.js",
+    const lineData = data.data.find(
+      (item) =>
+        item.engineName === "合同功率曲线" || item.enginName === "合同功率曲线",
     );
-    const plotlyContent = await fs.readFile(plotlyPath, "utf-8");
 
-    // 创建浏览器实例
-    const browser = await puppeteer.launch({
-      headless: "new",
-      // 根据系统改路径
-      executablePath: `${process.env.CHROME_PATH}`, // 根据系统改路径
+    if (!scatterData) {
+      throw new Error("scatterData 不存在");
+    }
+
+    // ✅ 2. 颜色数据源
+    const rawColorData =
+      scatterData.colorbar?.length === scatterData.xData.length
+        ? scatterData.colorbar
+        : scatterData.color;
+
+    const uniqueLabels = [...new Set(rawColorData)];
 
-      args: ["--no-sandbox", "--disable-setuid-sandbox"],
+    const tickvals = uniqueLabels.map((_, i) => i + 1);
+
+    const ticktext = uniqueLabels.map((dateStr) => {
+      const date = new Date(dateStr);
+      if (isNaN(date)) return dateStr; // 防止非法时间🔥
+      const y = date.getFullYear();
+      const m = String(date.getMonth() + 1).padStart(2, "0");
+      return `${y}-${m}`;
     });
 
-    try {
-      const page = await browser.newPage();
-
-      // 提取散点数据和线数据
-      const scatterData = data.data.filter(
-        (item) => item.engineName !== "合同功率曲线",
-      )[0]; // 点数据
-
-      const lineData = data.data.filter(
-        (item) =>
-          item.engineName === "合同功率曲线" ||
-          item.enginName === "合同功率曲线",
-      )[0]; // 线数据
-
-      // 提取唯一时间标签,并计算 tickvals 和 ticktext
-      const uniqueTimeLabels = scatterData.colorbar
-        ? [...new Set(scatterData.colorbar)]
-        : [...new Set(scatterData.color)];
-      const tickvals = uniqueTimeLabels.map((_, index) => index + 1);
-      const ticktext = uniqueTimeLabels.map((dateStr) => {
-        const date = new Date(dateStr);
-        return date.toLocaleDateString("en-CA", {
-          year: "numeric",
-          month: "2-digit",
-        });
-      });
+    const mapping = uniqueLabels.reduce((acc, cur, i) => {
+      acc[cur] = i + 1;
+      return acc;
+    }, {});
 
-      const timeMapping = uniqueTimeLabels.reduce((acc, curr, index) => {
-        acc[curr] = index + 1;
-        return acc;
-      }, {});
-
-      // 计算颜色值映射
-      let colorValues = scatterData.colorbar
-        ? scatterData.colorbar.map((date) => timeMapping[date])
-        : scatterData.color.map((date) => timeMapping[date]);
-
-      // 绘制散点图
-      const scatterTrace = {
-        x: scatterData.xData,
-        y: scatterData.yData,
-        mode: "markers",
-        type: "scattergl", // 使用 scattergl 提高性能
-        text: scatterData.engineName, // 提示文本
-        marker: {
-          color: colorValues,
-          colorscale: [
-            [0, "#F9FDD2"],
-            [0.15, "#E9F6BD"],
-            [0.3, "#C2E3B9"],
-            [0.45, "#8AC8BE"],
-            [0.6, "#5CA8BF"],
-            [0.75, "#407DB3"],
-            [0.9, "#2E4C9A"],
-            [1, "#1B2973"],
-          ],
-          size: new Array(scatterData.xData.length).fill(6), // 点的大小
-        },
-        hovertemplate: `${data.xaixs}: %{x} <br> ${data.yaixs}: %{y} <br> 时间: %{customdata}<extra></extra>`,
-        customdata: scatterData.colorbar || scatterData.color, // 将格式化后的时间存入 customdata
-      };
-
-      // 绘制线图
-      let lineTrace = {};
-      if (lineData) {
-        lineTrace = {
-          x: lineData.xData,
-          y: lineData.yData,
-          mode: "lines+markers", // 线和点同时显示
-          type: "scattergl", // 使用 scattergl 类型
-          text: lineData.engineName, // 提示文本
-          line: {
-            color: "red", // 线条颜色
-          },
-        };
-      }
-      // 图表布局
-      const layout = {
-        title: {
-          text: scatterData.title,
-          font: {
-            size: 16,
-            weight: "bold",
-          },
-        },
-        xaxis: {
-          title: {
-            text: data.xaixs,
-          },
-          gridcolor: "rgb(255,255,255)",
-          tickcolor: "rgb(255,255,255)",
-          backgroundcolor: "#e5ecf6",
-          showbackground: true,
-        },
-        yaxis: {
-          title: {
-            text: data.yaixs,
-          },
-          gridcolor: "rgb(255,255,255)",
-          tickcolor: "rgb(255,255,255)",
-          backgroundcolor: "#e5ecf6",
-          showbackground: true,
-        },
-        showlegend: false,
-        plot_bgcolor: "#e5ecf6",
-        gridcolor: "#fff",
-      };
-
-      // 准备 HTML 内容
-      const htmlContent = `
-        <!DOCTYPE html>
-        <html>
-        <head>
-          <meta charset="UTF-8">
-          <title>2D 散点图</title>
-          <script>${plotlyContent}</script>
-        </head>
-        <body>
-          <div id="chart" style="width: 100%; height: 600px"></div>
-          <script>
-            const traces = [${JSON.stringify(scatterTrace)}${
-        lineTrace ? `, ${JSON.stringify(lineTrace)}` : ""
-      }];
-            const layout = ${JSON.stringify(layout)};
-            Plotly.newPlot('chart', traces, layout, {
-  responsive: true,
-  displayModeBar: false,
-  staticPlot: true
-}).then(() => {
-              window.chartRendered = true; // 确保在图表渲染完成后设置
-              console.log("图表渲染完成");
-            }).catch((error) => {
-              console.error("图表渲染错误:", error); // 捕获渲染错误
-            });
-          </script>
-        </body>
-        </html>
-      `;
-
-      // 设置页面内容
-      await page.setContent(htmlContent, {
-        waitUntil: "domcontentloaded",
-        timeout: 0,
-      });
+    // ✅ 3. 颜色渐变(安全版)
+    const colors = colorSchemes?.[0]?.colors || [];
 
-      await page.waitForTimeout(1000);
+    const safePick = (i) => colors[i] || colors[0] || "#1B2973";
 
-      // 等待图表渲染完成,延长超时时间
-      await page.waitForFunction(() => window.chartRendered === true, {
-        timeout: 150000, // 延长到 150 秒
-      });
+    const colorStops = [safePick(0), safePick(4), safePick(8), safePick(12)];
 
-      // 截图并保存到临时文件
-      const chartElement = await page.$("#chart");
-      await chartElement.screenshot({
-        path: tempFilePath,
-        type: "jpeg",
-      });
+    const colorscale = colorStops.map((c, i) => [
+      i / (colorStops.length - 1),
+      c,
+    ]);
 
-      // 上传图片到服务器
-      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;
-    } catch (error) {
-      console.error("生成2D散点图失败:", error);
-      throw error;
-    } finally {
-      await browser.close();
+    if (colorscale.length < 2) {
+      colorscale.push([1, safePick(0)]);
+    }
+
+    // ✅ 4. 映射颜色
+    const colorValues = rawColorData.map((v) => mapping[v]);
+
+    // ⚠️ 性能优化
+    const pointCount = scatterData.xData.length;
+    const markerSize = pointCount > 5000 ? 3 : 6;
+
+    // ✅ 5. scatter trace
+    const scatterTrace = {
+      x: scatterData.xData,
+      y: scatterData.yData,
+      type: "scattergl",
+      mode: "markers",
+      name: scatterData.engineName,
+      marker: {
+        color: colorValues,
+        colorscale,
+        size: markerSize,
+        colorbar: {
+          title: { text: scatterData.colorbartitle || "时间" },
+          tickvals,
+          ticktext,
+        },
+        line: {
+          color: "#fff",
+          width: 0.3,
+        },
+      },
+      customdata: rawColorData,
+      hovertemplate:
+        `${data.xaixs}: %{x}<br>` +
+        `${data.yaixs}: %{y}<br>` +
+        `时间: %{customdata}<extra></extra>`,
+    };
+
+    // ✅ 6. 合同曲线(优化:避免空对象加入🔥)
+    const traces = [scatterTrace];
+
+    if (lineData?.xData?.length && lineData?.yData?.length) {
+      traces.push({
+        x: lineData.xData,
+        y: lineData.yData,
+        type: "scatter",
+        mode: "lines+markers",
+        name: "合同功率曲线",
+        line: { color: "red" },
+        marker: { color: "red", size: 4 },
+      });
     }
+
+    // ✅ 7. layout
+    const layout = {
+      title: {
+        text: scatterData.title || "散点图",
+        font: { size: 16 },
+      },
+      xaxis: {
+        title: data.xaixs,
+        gridcolor: "rgb(255,255,255)",
+        tickcolor: "rgb(255,255,255)",
+        backgroundcolor: "#e5ecf6",
+        showbackground: true,
+        showline: true, // ✅ 显示 X 轴轴线
+        zeroline: false,
+        linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+      },
+      yaxis: {
+        title: data.yaixs,
+        gridcolor: "rgb(255,255,255)",
+        tickcolor: "rgb(255,255,255)",
+        backgroundcolor: "#e5ecf6",
+        showbackground: true,
+        showline: true, // ✅ 显示 X 轴轴线
+        zeroline: false,
+        linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+      },
+      showlegend: false,
+      plot_bgcolor: "#e5ecf6",
+      paper_bgcolor: "#e5ecf6",
+    };
+
+    // ✅ 8. 渲染
+    const url = await renderChart({
+      traces,
+      layout,
+      bucketName,
+      objectName,
+    });
+
+    return url;
   } catch (error) {
-    console.error("生成2D散点图失败:", error);
+    console.error("❌ 生成2D散点图失败:", error);
     throw error;
   }
 };

+ 49 - 139
downLoadServer/src/server/utils/chartsCom/WindRoseChart.js

@@ -1,8 +1,4 @@
-import puppeteer from "puppeteer";
-import fs from "fs-extra";
-import path from "path";
-import FormData from "form-data";
-import axios from "axios"; // 导入 axios
+import { renderChart } from "../chartService/index.js"; // 用你的统一渲染服务
 import { colorSchemes } from "../colors.js";
 
 export const getWindRoseChart = async (
@@ -12,185 +8,99 @@ export const getWindRoseChart = async (
   analysisTypeCode,
 ) => {
   try {
-    // 获取 plotly.js 的绝对路径
-    const plotlyPath = path.join(
-      process.cwd(),
-      "src",
-      "public",
-      "js",
-      "plotly-3.0.1.min.js",
-    );
-    const plotlyContent = await fs.readFile(plotlyPath, "utf-8");
+    const windRoseList = data.data;
 
-    const windRoseList = data.data; // 确保数据源一致
-    // 创建临时目录
-    const tempDir = path.join(process.cwd(), "images");
-    await fs.ensureDir(tempDir);
-    const tempFilePath = path.join(
-      tempDir,
-      `temp_wind_rose_chart_${Date.now()}.jpeg`,
-    );
-
-    // 定义风向的 16 等分
+    // ===== 数据处理 =====
     const windDirections = Array.from({ length: 16 }, (_, i) => i * 22.5);
+
     const windSpeedRanges = new Set();
     const counts = {};
 
-    // 从数据中提取 windSpeedRange 和动态生成 speedLabels
     windRoseList.forEach((engine) => {
       engine.windRoseData.forEach((item) => {
         windSpeedRanges.add(item.windSpeedRange);
       });
     });
 
-    const speedLabels = Array.from(windSpeedRanges).sort(); // 动态范围值
-    const colors = colorSchemes[0].colors; // 生成颜色
+    const speedLabels = Array.from(windSpeedRanges).sort();
+
+    const colors = colorSchemes[0].colors;
     const colorscale = {};
+
     speedLabels.forEach((label, index) => {
       colorscale[label] = colors[(index % colors.length) + 4];
+      counts[label] = Array(windDirections.length).fill(0);
     });
 
-    // 初始化 counts 对象
-    speedLabels.forEach((speedLabel) => {
-      counts[speedLabel] = Array(windDirections.length).fill(0); // 确保初始化为 0
-    });
-
-    // 数据聚合
+    // ===== 数据聚合 =====
     windRoseList.forEach((engine) => {
       engine.windRoseData.forEach((item) => {
         const { windDirection, windSpeedRange } = item;
+
         const index = windDirections.findIndex(
           (dir, i) =>
-            windDirection >= dir && windDirection < windDirections[i + 1],
+            windDirection >= dir &&
+            windDirection < (windDirections[i + 1] ?? 360),
         );
 
-        if (index !== -1 && counts[windSpeedRange]) {
-          counts[windSpeedRange][index] += item.frequency; // 聚合频率
+        if (index !== -1) {
+          counts[windSpeedRange][index] += item.frequency;
         }
       });
     });
 
-    // 构建 traces
-    const traces = speedLabels.map((speedLabel) => {
-      const percentage = counts[speedLabel];
-
-      return {
-        r: percentage,
-        theta: windDirections,
-        name: speedLabel,
-        type: "barpolar",
-        marker: {
-          color: colorscale[speedLabel],
-          line: {
-            color: "white",
-            width: 1,
-          },
+    // ===== traces =====
+    const traces = speedLabels.map((speedLabel) => ({
+      r: counts[speedLabel],
+      theta: windDirections,
+      name: speedLabel,
+      type: "barpolar",
+      marker: {
+        color: colorscale[speedLabel],
+        line: {
+          color: "white",
+          width: 1,
         },
-        hovertemplate: `频率: %{r} <br> 风向: %{theta} <br> <extra></extra>`, // 修改为与 Vue 文件一致
-      };
-    });
+      },
+      hovertemplate: "频率: %{r}<br>风向: %{theta}<extra></extra>",
+    }));
 
-    // 图表布局
+    // ===== layout =====
     const layout = {
       title: {
-        text: `风向玫瑰图 - ${windRoseList[0]?.enginName} `, // 修改为与 Vue 文件一致
-        font: {
-          size: 16,
-          weight: "bold",
-        },
+        text: `风向玫瑰图 - ${windRoseList[0]?.enginName || ""}`,
+        font: { size: 16 },
       },
       plot_bgcolor: "#e5ecf6",
       polar: {
         bgcolor: "#e5ecf6",
         radialaxis: {
-          title: { text: data?.axes?.radial || "频率百分比(%)" }, // 确保与 Vue 文件一致
-          gridcolor: "rgb(255,255,255)",
-          showgrid: true,
-          linecolor: "rgb(255,255,255)",
+          title: {
+            text: data?.axes?.radial || "频率百分比(%)",
+          },
+          gridcolor: "#fff",
         },
         angularaxis: {
-          title: { text: "风向" }, // 确保与 Vue 文件一致
+          title: { text: "风向" },
           tickvals: windDirections,
-          ticktext: windDirections.map((item) => item), // 设置为空字符串以隐藏单位
-          gridcolor: "rgb(255,255,255)",
-          tickcolor: "rgb(255,255,255)",
-          linecolor: "rgb(255,255,255)",
+          ticktext: windDirections,
         },
       },
       showlegend: true,
-      legend: { title: { text: "风速范围" } }, // 确保与 Vue 文件一致
+      legend: {
+        title: { text: "风速范围" },
+      },
     };
 
-    // 使用 Puppeteer 生成图表的截图
-    const browser = await puppeteer.launch({
-      headless: "new",
-      // 根据系统改路径
-      executablePath: `${process.env.CHROME_PATH}`, // 根据系统改路径
-
-      args: ["--no-sandbox", "--disable-setuid-sandbox"],
+    // ===== 统一渲染(核心!)=====
+    return await renderChart({
+      traces,
+      layout,
+      bucketName,
+      objectName,
     });
-    try {
-      const page = await browser.newPage();
-      const htmlContent = `
-        <!DOCTYPE html>
-        <html>
-          <head>
-            <meta charset="UTF-8">
-            <title>玫瑰图</title>
-            <script>${plotlyContent}</script>
-            <style>
-              body { margin: 0; }
-              #chart { width: 800px; height: 600px; }
-            </style>
-          </head>
-          <body>
-            <div id="chart"></div>
-            <script>
-              window.onload = function() {
-                Plotly.newPlot('chart', ${JSON.stringify(
-                  traces,
-                )}, ${JSON.stringify(layout)},{
-  responsive: true,
-  displayModeBar: false,
-  staticPlot: true
-}).then(() => {
-                  window.chartRendered = true;
-                });
-              };
-            </script>
-          </body>
-        </html>
-      `;
-      await page.setContent(htmlContent, {
-        waitUntil: "domcontentloaded",
-        timeout: 0,
-      });
-
-      await page.waitForTimeout(1000);
-      await page.waitForFunction(() => window.chartRendered === true, {
-        timeout: 60000,
-      });
-      // 截图并保存到临时文件
-      const chartElement = await page.$("#chart");
-      await chartElement.screenshot({ path: tempFilePath, type: "jpeg" });
-      // 上传图片到服务器
-      const formData = new FormData();
-      formData.append("file", fs.createReadStream(tempFilePath));
-      const response = await axios.post(
-        `${process.env.API_BASE_URL}/examples/upload`,
-        {
-          filePath: tempFilePath,
-          bucketName,
-          objectName,
-        },
-      );
-      return response?.data?.url;
-    } catch (error) {
-      console.error("生成图表失败:", error);
-    } finally {
-      await browser.close();
-    }
   } catch (error) {
-    console.error("发生错误:", error);
+    console.error("风玫瑰图生成失败:", error);
+    throw error;
   }
 };

+ 56 - 157
downLoadServer/src/server/utils/chartsCom/YewErrorBarChart.js

@@ -1,9 +1,4 @@
-import puppeteer from "puppeteer";
-import fs from "fs-extra";
-import path from "path";
-import FormData from "form-data";
-import axios from "axios"; // 导入 axios
-import { colorSchemes } from "../colors.js";
+import { renderChart } from "../chartService/index.js";
 
 export const getYewErrorBarChart = async (
   data,
@@ -12,181 +7,85 @@ export const getYewErrorBarChart = async (
   analysisTypeCode,
 ) => {
   try {
-    // 提取机组编号和偏航误差值
-    const xData = data.map((item) => item.engine_name); // 机组编号
-    const yData = data.map((item) => item.yaw_error1); // 偏航误差值
+    // ===== 数据处理 =====
+    const xData = data.map((item) => item.engine_name || "-");
+    const yData = data.map((item) => Number(item.yaw_error1 || 0));
 
-    // 为每个数据点分配颜色
-    const colors = yData.map((value) => {
-      if (value <= 3) {
-        return "#8AC8BE"; // (0, 3] 蓝色
-      } else if (value <= 5) {
-        return "#407DB3"; // (3, 5] 绿色
+    // ===== 按区间拆成多个 trace(关键优化点!)=====
+    const ranges = {
+      low: { name: "(0, 3]", color: "#8AC8BE", x: [], y: [] },
+      mid: { name: "(3, 5]", color: "#407DB3", x: [], y: [] },
+      high: { name: "(5, ∞]", color: "#1B2973", x: [], y: [] },
+    };
+
+    xData.forEach((x, i) => {
+      const y = yData[i];
+
+      if (y <= 3) {
+        ranges.low.x.push(x);
+        ranges.low.y.push(y);
+      } else if (y <= 5) {
+        ranges.mid.x.push(x);
+        ranges.mid.y.push(y);
       } else {
-        return "#1B2973"; // (5, ∞] 红色
+        ranges.high.x.push(x);
+        ranges.high.y.push(y);
       }
     });
 
-    const trace = {
-      x: xData, // 横坐标数据
-      y: yData, // 纵坐标数据
-      type: "bar", // 当前图表类型
+    // ===== traces(每个区间一个 trace)=====
+    const traces = Object.values(ranges).map((range) => ({
+      x: range.x,
+      y: range.y,
+      type: "bar",
+      name: range.name,
       marker: {
-        color: colors, // 每个点的颜色
+        color: range.color,
       },
-      name: "偏航误差值", // 图例名称
-    };
-    // 创建虚拟的 trace 以便显示图例
-    const legendTrace1 = {
-      x: [null],
-      y: [null],
-      name: "(0, 3]",
-      mode: "markers",
-      marker: { color: "#8AC8BE", size: 10 },
-    };
-    const legendTrace2 = {
-      x: [null],
-      y: [null],
-      name: "(3, 5]",
-      mode: "markers",
-      marker: { color: "#407DB3", size: 10 },
-    };
-    const legendTrace3 = {
-      x: [null],
-      y: [null],
-      name: "(5, ∞]",
-      mode: "markers",
-      marker: { color: "#1B2973", size: 10 },
-    };
+      hovertemplate: "机组: %{x}<br>偏航误差: %{y}<extra></extra>",
+    }));
+
+    // ===== layout =====
     const layout = {
       title: {
-        text: "机组静态偏航误差值分布", // 图表标题
-        font: {
-          size: 16, // 设置标题字体大小(默认 16)
-          weight: "bold",
-        },
+        text: "机组静态偏航误差值分布",
+        font: { size: 16 },
       },
       xaxis: {
-        title: {
-          text: "机组",
-        }, // 横坐标标题
-        tickmode: "array",
-        tickvals: xData, // 设置刻度值(机组编号)
-        ticktext: xData, // 设置刻度文本(机组编号)
+        title: { text: "机组" },
         gridcolor: "rgb(255,255,255)",
         tickcolor: "rgb(255,255,255)",
         backgroundcolor: "#e5ecf6",
+        showbackground: true,
+        showline: true, // ✅ 显示 X 轴轴线
+        zeroline: false,
+        linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
       },
       yaxis: {
-        title: {
-          text: "静态偏航误差值(度)",
-        }, // 纵坐标标题
+        title: { text: "静态偏航误差值(度)" },
         gridcolor: "rgb(255,255,255)",
         tickcolor: "rgb(255,255,255)",
         backgroundcolor: "#e5ecf6",
+        showbackground: true,
+        zeroline: false,
+        showline: true, // ✅ 显示 X 轴轴线
+        linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
       },
-      margin: {
-        l: 50,
-        r: 50,
-        t: 50,
-        b: 50,
-      },
+      margin: { l: 50, r: 50, t: 50, b: 50 },
       plot_bgcolor: "#e5ecf6",
-      gridcolor: "#fff",
-      bgcolor: "#e5ecf6", // 设置背景颜色
-      autosize: true, // 开启自适应
-      showlegend: true, // 显示图例
+      showlegend: true,
+      barmode: "group", // 或 "stack" 看你需求
     };
 
-    // 创建临时目录
-    const tempDir = path.join(process.cwd(), "images");
-    await fs.ensureDir(tempDir);
-    const tempFilePath = path.join(
-      tempDir,
-      `temp_yew_error_bar_chart_${Date.now()}.jpeg`,
-    );
-
-    // 获取 plotly.js 的绝对路径
-    const plotlyPath = path.join(
-      process.cwd(),
-      "src",
-      "public",
-      "js",
-      "plotly-3.0.1.min.js",
-    );
-    const plotlyContent = await fs.readFile(plotlyPath, "utf-8");
-
-    // 使用 Puppeteer 生成图表的截图
-    const browser = await puppeteer.launch({
-      headless: "new",
-      // 根据系统改路径
-      executablePath: `${process.env.CHROME_PATH}`, // 根据系统改路径
-
-      args: ["--no-sandbox", "--disable-setuid-sandbox"],
+    // ===== 核心统一渲染 =====
+    return await renderChart({
+      traces,
+      layout,
+      bucketName,
+      objectName,
     });
-    try {
-      const page = await browser.newPage();
-      const traces = [trace, legendTrace1, legendTrace2, legendTrace3];
-      const htmlContent = `
-        <!DOCTYPE html>
-        <html>
-          <head>
-            <meta charset="UTF-8">
-            <title>机组静态偏航误差值</title>
-            <script>${plotlyContent}</script>
-            <style>
-              body { margin: 0; }
-              #chart { width: 800px; height: 600px; }
-            </style>
-          </head>
-          <body>
-            <div id="chart"></div>
-            <script>
-              window.onload = function() {
-                Plotly.newPlot('chart', ${JSON.stringify(
-                  traces,
-                )}, ${JSON.stringify(layout)},{
-  responsive: true,
-  displayModeBar: false,
-  staticPlot: true
-}).then(() => {
-                  window.chartRendered = true;
-                });
-              };
-            </script>
-          </body>
-        </html>
-      `;
-      await page.setContent(htmlContent, {
-        waitUntil: "domcontentloaded",
-        timeout: 0,
-      });
-
-      await page.waitForTimeout(1000);
-      await page.waitForFunction(() => window.chartRendered === true, {
-        timeout: 60000,
-      });
-      // 截图并保存到临时文件
-      const chartElement = await page.$("#chart");
-      await chartElement.screenshot({ path: tempFilePath, type: "jpeg" });
-      // 上传图片到服务器
-      const formData = new FormData();
-      formData.append("file", fs.createReadStream(tempFilePath));
-      const response = await axios.post(
-        `${process.env.API_BASE_URL}/examples/upload`,
-        {
-          filePath: tempFilePath,
-          bucketName,
-          objectName,
-        },
-      );
-      return response?.data?.url;
-    } catch (error) {
-      console.error("生成图表失败:", error);
-    } finally {
-      await browser.close();
-    }
   } catch (error) {
-    console.error("发生错误:", error);
+    console.error("偏航误差柱状图生成失败:", error);
+    throw error;
   }
 };

+ 103 - 194
downLoadServer/src/server/utils/chartsCom/lineAndChildLine.js

@@ -1,210 +1,119 @@
-import puppeteer from "puppeteer";
-import fs from "fs-extra";
-import path from "path";
-import FormData from "form-data";
+/*
+ * 多折线 + 子曲线(终极稳定版)
+ */
+import { renderChart } from "../chartService/index.js";
 import { colorSchemes } from "../colors.js";
-import axios from "axios";
 
-/**
- * 生成折线图并上传
- * @param {Object} options - 配置选项
- * @param {string} options.fileAddr - 数据文件地址
- * @param {string[]} options.color1 - 颜色数组1
- * @param {string[]} options.colors - 颜色数组2
- * @returns {Promise<String>} - 返回图片URL
- */
 export const generateLineAndChildLine = async (
   chartData,
   bucketName,
   objectName,
 ) => {
-  try {
-    const colorSchemesItem = colorSchemes[0].colors;
-    // 获取数据
-    // 准备图表数据
-    const data = [];
-    const newData =
-      chartData.analysisTypeCode === "风电机组叶尖速比和风速分析"
-        ? chartData &&
-          chartData.data &&
-          JSON.parse(JSON.stringify(chartData.data)).sort((a, b) => {
-            return a.engineName.localeCompare(b.engineName);
-          })
-        : JSON.parse(JSON.stringify(chartData.data));
+  if (!chartData || !Array.isArray(chartData.data)) {
+    throw new Error("line chart data invalid");
+  }
 
-    newData.forEach((turbine, index) => {
-      const chartConfig = {
-        x: turbine.xData,
-        y: turbine.yData,
-        name: turbine.engineName,
-        mode: "lines",
-        line: {
-          color: colorSchemesItem[index % colorSchemesItem.length],
-        },
-        marker: {
-          color: colorSchemesItem[index % colorSchemesItem.length],
-        },
-        hovertemplate:
-          `${chartData.xaixs}:` +
-          ` %{x} <br> ` +
-          `${chartData.yaixs}:` +
-          "%{y} <br>",
-      };
-      if (chartData.yaixs === "概率密度函数") {
-        chartConfig.line.color = colorSchemesItem[7];
-      }
-      data.push(chartConfig);
-    });
+  const colorSchemesItem = colorSchemes[0].colors || [];
+
+  // ✅ 排序(避免深拷贝)
+  let dataset = [...chartData.data];
+
+  if (chartData.analysisTypeCode === "风电机组叶尖速比和风速分析") {
+    dataset.sort((a, b) =>
+      (a.engineName || "").localeCompare(b.engineName || ""),
+    );
+  }
 
-    // 准备布局配置
-    const layout = {
-      title: {
-        text: chartData.title || chartData.data[0].title,
-        font: {
-          size: 16,
-          weight: "bold",
-        },
+  // ✅ 主折线
+  const traces = dataset.map((turbine, index) => {
+    const color =
+      chartData.yaixs === "概率密度函数"
+        ? colorSchemesItem[7]
+        : colorSchemesItem[index % colorSchemesItem.length];
+
+    return {
+      x: turbine.xData || [],
+      y: turbine.yData || [],
+      type: "scatter",
+      mode: "lines",
+      name: turbine.engineName || `series-${index}`,
+      line: {
+        color,
       },
-      xaxis: {
-        title: chartData.xaixs || "X轴",
-        gridcolor: "rgb(255,255,255)",
-        tickcolor: "rgb(255,255,255)",
-        backgroundcolor: "#e5ecf6",
+      marker: {
+        color,
       },
-      yaxis: {
-        title: chartData.yaixs || "Y轴",
-        gridcolor: "rgb(255,255,255)",
-        tickcolor: "rgb(255,255,255)",
-        backgroundcolor: "#e5ecf6",
+      hovertemplate: `${chartData.xaixs || "X"}: %{x}<br>${
+        chartData.yaixs || "Y"
+      }: %{y}<br>`,
+    };
+  });
+
+  // ✅ 子曲线(合同曲线)
+  if (
+    Array.isArray(chartData.contract_Cp_curve_yData) &&
+    chartData.contract_Cp_curve_yData.length > 0
+  ) {
+    traces.push({
+      x: chartData.contract_Cp_curve_xData || [],
+      y: chartData.contract_Cp_curve_yData || [],
+      type: "scatter",
+      mode: "lines+markers",
+      name: "合同功率曲线",
+      line: {
+        color: "red",
+        width: 1,
       },
-      margin: {
-        l: 50,
-        r: 50,
-        t: 50,
-        b: 50,
+      marker: {
+        color: "red",
+        size: 4,
       },
-      plot_bgcolor: "#e5ecf6",
-      gridcolor: "#fff",
-      bgcolor: "#e5ecf6",
-      autosize: true,
-      barmode: "group",
-    };
-
-    if (
-      chartData.contract_Cp_curve_yData &&
-      chartData.contract_Cp_curve_yData.length > 0
-    ) {
-      data.push({
-        x: chartData.contract_Cp_curve_xData,
-        y: chartData.contract_Cp_curve_yData,
-        mode: "lines+markers",
-        name: "合同功率曲线",
-        line: {
-          color: "red",
-          width: 1,
-        },
-        marker: { color: "red", size: 4 },
-      });
-    }
-
-    // 创建临时目录
-    const tempDir = path.join(process.cwd(), "images");
-    await fs.ensureDir(tempDir);
-    const tempFilePath = path.join(
-      tempDir,
-      `temp_line_chart_${Date.now()}.jpeg`,
-    );
-
-    // 获取 plotly.js 的绝对路径
-    const plotlyPath = path.join(
-      process.cwd(),
-      "src",
-      "public",
-      "js",
-      "plotly-latest.min.js",
-    );
-    const plotlyContent = await fs.readFile(plotlyPath, "utf-8");
-
-    // 创建浏览器实例
-    const browser = await puppeteer.launch({
-      headless: "new",
-      // 根据系统改路径
-      executablePath: `${process.env.CHROME_PATH}`, // 根据系统改路径
-
-      args: ["--no-sandbox", "--disable-setuid-sandbox"],
     });
-
-    try {
-      const page = await browser.newPage();
-
-      // 创建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(data)};
-                        const layout = ${JSON.stringify(layout)};
-                        Plotly.newPlot('chart', data, layout, {
-  responsive: true,
-  displayModeBar: false,
-  staticPlot: true
-}).then(() => {
-                          window.chartRendered = true;
-                        });
-                      };
-                    </script>
-                  </body>
-                </html>
-            `;
-
-      // 设置页面内容
-      await page.setContent(htmlContent, {
-        waitUntil: "domcontentloaded",
-        timeout: 0,
-      });
-
-      await page.waitForTimeout(1000);
-
-      // 等待图表渲染完成
-      await page.waitForFunction(() => window.chartRendered === true, {
-        timeout: 60000,
-      });
-
-      // 截图并保存到临时文件
-      const chartElement = await page.$("#chart");
-      await chartElement.screenshot({
-        path: tempFilePath,
-        type: "jpeg",
-      });
-
-      // 上传图片到服务器
-      const formData = new FormData();
-      formData.append("file", fs.createReadStream(tempFilePath));
-      formData.append("type", "chart");
-      // 这里假设需要从 chartData 中获取 engineCode 和 analysisTypeCode,根据实际情况调整
-      formData.append("engineCode", chartData.engineCode || "");
-      formData.append("analysisTypeCode", chartData.analysisTypeCode || "");
-
-      // 发送上传请求
-      const response = await axios.post(
-        `${process.env.API_BASE_URL}/examples/upload`,
-        { filePath: tempFilePath, bucketName, objectName },
-      );
-      return response?.data?.url;
-    } finally {
-      await browser.close();
-    }
-  } catch (error) {
-    throw error;
   }
+
+  // ✅ layout
+  const layout = {
+    title: {
+      text: chartData.title || chartData?.data?.[0]?.title || "",
+      font: { size: 16 },
+    },
+    xaxis: {
+      title: chartData.xaixs || "X轴",
+      gridcolor: "#fff",
+      gridcolor: "rgb(255,255,255)",
+      tickcolor: "rgb(255,255,255)",
+      backgroundcolor: "#e5ecf6",
+      showbackground: true,
+      showline: true, // ✅ 显示 X 轴轴线
+      zeroline: false,
+      linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+    },
+    yaxis: {
+      title: chartData.yaixs || "Y轴",
+      gridcolor: "rgb(255,255,255)",
+      tickcolor: "rgb(255,255,255)",
+      backgroundcolor: "#e5ecf6",
+      showbackground: true,
+      zeroline: false,
+      showline: true, // ✅ 显示 X 轴轴线
+      linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+    },
+    plot_bgcolor: "#e5ecf6",
+    paper_bgcolor: "#e5ecf6",
+    margin: {
+      l: 50,
+      r: 50,
+      t: 60,
+      b: 50,
+    },
+    showlegend: true,
+  };
+
+  // ✅ 🚀 统一出口
+  return await renderChart({
+    traces,
+    layout,
+    bucketName,
+    objectName,
+  });
 };

+ 125 - 176
downLoadServer/src/server/utils/chartsCom/lineChartsFen.js

@@ -1,203 +1,152 @@
-import puppeteer from "puppeteer";
-import fs from "fs-extra";
-import path from "path";
-import FormData from "form-data";
+import { renderChart } from "../chartService/index.js";
 import { colorSchemes } from "../colors.js";
-import axios from "axios";
 
 /**
- * 生成折线图并上传
- * @param {Object} data - 图表数据
- * @returns {Promise<String>} - 返回图片URL
+ * 过滤数据(你原来的逻辑,已增强健壮性)
+ */
+const filterData = (group, startDate, endDate) => {
+  const filteredXData = [];
+  const filteredYData = [];
+  const filteredMedians = group.medians ? { x: [], y: [] } : null;
+
+  const isDate = isDateType(group.xData);
+
+  group.xData.forEach((x, i) => {
+    if (!isDate || isInDateRange(x, startDate, endDate)) {
+      filteredXData.push(x);
+      filteredYData.push(group.yData[i]);
+
+      if (filteredMedians && group.medians?.x[i]) {
+        if (!isDate || isInDateRange(group.medians.x[i], startDate, endDate)) {
+          filteredMedians.x.push(group.medians.x[i]);
+          filteredMedians.y.push(group.medians.y[i]);
+        }
+      }
+    }
+  });
+
+  return {
+    ...group,
+    xData: filteredXData,
+    yData: filteredYData,
+    medians: filteredMedians,
+  };
+};
+
+const isDateType = (xData) => {
+  if (!xData || !xData.length) return false;
+  return !isNaN(Date.parse(xData[0]));
+};
+
+const isInDateRange = (timestamp, startDate, endDate) => {
+  if (!startDate || !endDate) return true;
+  const date = new Date(timestamp);
+  return date >= new Date(startDate) && date <= new Date(endDate);
+};
+
+/**
+ * ✅ 终极版折线图生成(接入 chartService)
  */
 export const generateLineChart = async (
   data,
   bucketName,
   objectName,
   fieldEngineCode,
+  startDate,
+  endDate,
 ) => {
   try {
-    // 创建临时目录
-    const tempDir = path.join(process.cwd(), "images");
-    await fs.ensureDir(tempDir);
-    const tempFilePath = path.join(
-      tempDir,
-      `temp_line_chart_${Date.now()}.jpeg`,
-    );
+    const colorSchemesItem = colorSchemes[0].colors;
 
-    // 获取 plotly.js 的绝对路径
-    const plotlyPath = path.join(
-      process.cwd(),
-      "src",
-      "public",
-      "js",
-      "plotly-latest.min.js",
-    );
-    const plotlyContent = await fs.readFile(plotlyPath, "utf-8");
+    // ✅ 1. 排序(目标机组最后)
+    const sortedData = [...data.data].sort((a, b) => {
+      const aIsTarget = a.engineCode === fieldEngineCode;
+      const bIsTarget = b.engineCode === fieldEngineCode;
 
-    // 创建浏览器实例
-    const browser = await puppeteer.launch({
-      headless: "new",
-      // 根据系统改路径
-      executablePath: `${process.env.CHROME_PATH}`, // 根据系统改路径
+      if (aIsTarget && !bIsTarget) return 1;
+      if (!aIsTarget && bIsTarget) return -1;
 
-      args: ["--no-sandbox", "--disable-setuid-sandbox"],
+      return a.engineName.localeCompare(b.engineName);
     });
 
-    try {
-      const page = await browser.newPage();
-
-      // 先整体排序:目标 engineCode 放最后,其他按 engineName 升序
-      const sortedData = [...data.data].sort((a, b) => {
-        const aIsTarget = a.engineCode === fieldEngineCode;
-        const bIsTarget = b.engineCode === fieldEngineCode;
-        // 如果 a 是目标、b 不是 => a 排后(返回 1)
-        if (aIsTarget && !bIsTarget) return 1;
-        // 如果 b 是目标、a 不是 => b 排后(返回 -1)
-        if (!aIsTarget && bIsTarget) return -1;
-        // 如果都不是目标项,按 engineName 升序排序
-        return a.engineName.localeCompare(b.engineName);
-      });
-      const finalData = [];
-      sortedData.forEach((turbine) => {
-        const color =
-          turbine.engineCode === fieldEngineCode ? "#406DAB" : "#D3D3D3";
-        const chartConfig = {
-          x: turbine.xData,
-          y: turbine.yData,
-          name: turbine.engineName,
-          mode: "lines",
-          fill: "none",
-          line: { color },
-          marker: { color },
-          hovertemplate: `${data.xaixs}: %{x} <br> ${data.yaixs}: %{y} <br>`,
-        };
-        finalData.push(chartConfig);
-      });
+    // ✅ 2. 过滤数据(你之前漏掉的逻辑)
+    const filteredData = sortedData.map((group) =>
+      filterData(group, startDate, endDate),
+    );
 
-      // 添加合同功率曲线
-      if (
-        data.contract_Cp_curve_yData &&
-        data.contract_Cp_curve_yData.length > 0
-      ) {
-        finalData.push({
-          x: data.contract_Cp_curve_xData,
-          y: data.contract_Cp_curve_yData,
-          mode: "lines+markers",
-          name: "合同功率曲线",
-          line: {
-            color: "red",
-            width: 1,
-          },
-          marker: { color: "red", size: 4 },
-        });
-      }
+    // ✅ 3. 构建 traces
+    const traces = filteredData.map((turbine, index) => {
+      const isTarget = turbine.engineCode === fieldEngineCode;
+
+      const color = isTarget
+        ? "#406DAB"
+        : colorSchemesItem[index % colorSchemesItem.length];
 
-      // 准备布局配置
-      const layout = {
-        title: {
-          text: data.title || "图表",
-          font: {
-            size: 16,
-            weight: "bold",
-          },
-        },
-        xaxis: {
-          title: data.xaixs || "X轴",
-          gridcolor: "rgb(255,255,255)",
-          tickcolor: "rgb(255,255,255)",
-          backgroundcolor: "#e5ecf6",
-        },
-        yaxis: {
-          title: data.yaixs || "Y轴",
-          gridcolor: "rgb(255,255,255)",
-          tickcolor: "rgb(255,255,255)",
-          backgroundcolor: "#e5ecf6",
-        },
-        margin: {
-          l: 50,
-          r: 50,
-          t: 50,
-          b: 50,
-        },
-        autosize: true,
-        plot_bgcolor: "#e5ecf6",
-        gridcolor: "#fff",
-        bgcolor: "#e5ecf6",
+      return {
+        x: turbine.xData,
+        y: turbine.yData,
+        name: turbine.engineName,
+        type: "scatter",
+        mode: "lines",
+        line: { color },
+        marker: { color },
+        hovertemplate: `${data.xaixs}: %{x}<br>${data.yaixs}: %{y}<extra></extra>`,
       };
+    });
 
-      // 创建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,
-  displayModeBar: false,
-  staticPlot: true
-}).then(() => {
-                  window.chartRendered = true;
-                });
-              };
-            </script>
-          </body>
-        </html>
-      `;
-
-      // 设置页面内容
-      await page.setContent(htmlContent, {
-        waitUntil: "domcontentloaded",
-        timeout: 0,
+    // ✅ 4. 合同曲线
+    if (data.contract_Cp_curve_yData?.length) {
+      traces.push({
+        x: data.contract_Cp_curve_xData,
+        y: data.contract_Cp_curve_yData,
+        type: "scatter",
+        mode: "lines+markers",
+        name: "合同功率曲线",
+        line: { color: "red", width: 1 },
+        marker: { color: "red", size: 4 },
       });
+    }
 
-      await page.waitForTimeout(1000);
-
-      // 等待图表渲染完成
-      await page.waitForFunction(() => window.chartRendered === true, {
-        timeout: 60000,
-      });
+    // ✅ 5. layout
+    const layout = {
+      title: {
+        text: data.title || "图表",
+        font: { size: 16 },
+      },
+      xaxis: {
+        title: data.xaixs || "X轴",
+        gridcolor: "rgb(255,255,255)",
+        tickcolor: "rgb(255,255,255)",
+        backgroundcolor: "#e5ecf6",
+        showbackground: true,
+        showline: true, // ✅ 显示 X 轴轴线
+        zeroline: false,
+        linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+      },
+      yaxis: {
+        title: data.yaixs || "Y轴",
+        gridcolor: "rgb(255,255,255)",
+        tickcolor: "rgb(255,255,255)",
+        backgroundcolor: "#e5ecf6",
+        showbackground: true,
+        zeroline: false,
+        showline: true, // ✅ 显示 X 轴轴线
+        linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+      },
+      margin: { l: 50, r: 50, t: 50, b: 50 },
+      plot_bgcolor: "#fff",
+      paper_bgcolor: "#fff",
+    };
 
-      // 截图并保存到临时文件
-      const chartElement = await page.$("#chart");
-      await chartElement.screenshot({
-        path: tempFilePath,
-        type: "jpeg",
-      });
+    // ✅ 6. 直接调用统一服务
+    const url = await renderChart({
+      traces,
+      layout,
+      bucketName,
+      objectName,
+    });
 
-      // 上传图片到服务器
-      // const formData = new FormData();
-      // formData.append("file", fs.createReadStream(tempFilePath));
-      // formData.append("type", "chart");
-      // formData.append("engineCode", data.engineCode);
-      // formData.append("analysisTypeCode", data.analysisTypeCode);
-      // 构造 formData 表单
-      const formData = new FormData();
-      formData.append("filePath", fs.createReadStream(tempFilePath)); // tempFilePath 是本地文件路径
-      formData.append("bucketName", bucketName);
-      formData.append("objectName", objectName);
-      // 删除临时文件
-
-      // 发送上传请求
-      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 url;
   } catch (error) {
     console.error("生成折线图失败:", error);
     throw error;

+ 120 - 252
downLoadServer/src/server/utils/chartsCom/powerMarkers2DCharts.js

@@ -1,17 +1,5 @@
-/*
- * @Author: your name
- * @Date: 2025-04-28 14:15:23
- * @LastEditTime: 2026-03-17 09:34:08
- * @LastEditors: MacBookPro
- * @Description: In User Settings Edit
- * @FilePath: /downLoadServer/src/server/utils/chartsCom/powerMarkers2DCharts.js
- */
-import puppeteer from "puppeteer";
-import fs from "fs-extra";
-import path from "path";
-import FormData from "form-data";
+import { renderChart } from "../chartService/index.js";
 import { colorSchemes } from "../colors.js";
-import axios from "axios"; // 导入 axios
 
 export const generatepowerMarkers2DCharts = async (
   data,
@@ -20,265 +8,145 @@ export const generatepowerMarkers2DCharts = async (
 ) => {
   try {
     const colorsBar = colorSchemes[0].colors;
-    // 创建临时目录
-    const tempDir = path.join(process.cwd(), "images");
-    await fs.ensureDir(tempDir);
-    const tempFilePath = path.join(
-      tempDir,
-      `temp_scatter_chart_${Date.now()}.jpeg`,
-    );
 
-    // 获取 plotly.js 的绝对路径
-    const plotlyPath = path.join(
-      process.cwd(),
-      "src",
-      "public",
-      "js",
-      "plotly-3.0.1.min.js",
+    // ✅ 1. 拆分数据
+    const scatterData = data.data.find(
+      (item) => item.engineName !== "合同功率曲线",
     );
-    const plotlyContent = await fs.readFile(plotlyPath, "utf-8");
-
-    // 创建浏览器实例
-    const browser = await puppeteer.launch({
-      headless: "new",
-      // 根据系统改路径
-      executablePath: `${process.env.CHROME_PATH}`, // 根据系统改路径
-
-      args: ["--no-sandbox", "--disable-setuid-sandbox"],
-    });
 
-    try {
-      const page = await browser.newPage();
-
-      // 提取散点数据和线数据
-      const scatterData = data.data.filter(
-        (item) => item.engineName !== "合同功率曲线",
-      )[0]; // 点数据
-
-      const lineData = data.data.filter(
-        (item) => item.engineName === "合同功率曲线",
-      )[0]; // 线数据
+    const lineData = data.data.find(
+      (item) => item.engineName === "合同功率曲线",
+    );
 
-      // 保存原始颜色和大小
-      const originalColors = [...scatterData.yData];
-      const originalSizes = new Array(scatterData.xData.length).fill(6); // 初始点大小
+    if (!scatterData) {
+      throw new Error("scatterData 不存在");
+    }
 
-      // 如果有 colorbar 数据
-      const uniqueTimeLabels =
-        scatterData.colorbar &&
-        scatterData.colorbar.length === scatterData.xData.length
-          ? [...new Set(scatterData.colorbar)] // 从 colorbar 中提取唯一的标签
-          : [...new Set(scatterData.color)]; // 如果没有 colorbar,使用 data.color
-      const ticktext = uniqueTimeLabels.map((dateStr) => {
-        const date = new Date(dateStr);
-        const year = date.getFullYear();
-        const month = String(date.getMonth() + 1).padStart(2, "0");
-        return `${year}-${month}`; // ✅ 保证格式为 yyyy-MM
-      });
+    // ✅ 2. 时间映射(colorbar 核心逻辑)
+    const rawColorData =
+      scatterData.colorbar?.length === scatterData.xData.length
+        ? scatterData.colorbar
+        : scatterData.color;
 
-      // const ticktext = uniqueTimeLabels.map((dateStr) => {
-      //   const date = new Date(dateStr);
-      //   return date.toLocaleDateString("en-CA", {
-      //     year: "numeric",
-      //     month: "2-digit",
-      //   }); // 格式化为 'yyyy-MM'
-      // }); // 使用格式化后的时间作为 ticktext
-      const tickvals = uniqueTimeLabels.map((label, index) => index + 1); // 设置 tick 值
-      const timeMapping = uniqueTimeLabels.reduce((acc, curr, index) => {
-        acc[curr] = index + 1;
-        return acc;
-      }, {});
+    const uniqueTimeLabels = [...new Set(rawColorData)];
 
-      // 获取 colorbar 的最小值和最大值来计算比例值
-      const minValue = Math.min(...tickvals);
-      const maxValue = Math.max(...tickvals);
+    const ticktext = uniqueTimeLabels.map((dateStr) => {
+      const date = new Date(dateStr);
+      const year = date.getFullYear();
+      const month = String(date.getMonth() + 1).padStart(2, "0");
+      return `${year}-${month}`;
+    });
 
-      // 仅取 colorsBar 的前 4 种颜色进行渐变
-      const colorStops = [
-        colorsBar[0],
-        colorsBar[4],
-        colorsBar[8],
-        colorsBar[12],
-      ];
+    const tickvals = uniqueTimeLabels.map((_, index) => index + 1);
 
-      // 计算渐变比例
-      const colors = colorStops.map((color, index) => {
-        const proportion = index / (colorStops.length - 1); // 计算比例值 (0, 1/3, 2/3, 1)
-        return [proportion, color]; // 生成颜色映射
-      });
+    const timeMapping = uniqueTimeLabels.reduce((acc, curr, index) => {
+      acc[curr] = index + 1;
+      return acc;
+    }, {});
 
-      // 确保至少有 2 个颜色,否则使用默认颜色
-      if (colors.length < 2) {
-        colors.push([1, colorStops[colorStops.length - 1] || "#1B2973"]);
-      }
+    // ✅ 3. 颜色渐变(优化容错)
+    const safePick = (i) => colorsBar[i] || colorsBar[0] || "#1B2973";
 
-      // 计算颜色值映射
-      let colorValues = [];
-      if (scatterData.colorbar) {
-        colorValues = scatterData.colorbar.map((date) => timeMapping[date]);
-      } else {
-        colorValues = scatterData.color.map((date) => timeMapping[date]);
-      }
+    const colorStops = [safePick(0), safePick(4), safePick(8), safePick(12)];
 
-      // 绘制散点图
-      const scatterTrace = {
-        x: scatterData.xData,
-        y: scatterData.yData,
-        mode: "markers", // 散点
-        type: "scattergl", // 使用散点图
-        text: scatterData.engineName, // 提示文本
-        marker: {
-          color: colorValues, // 使用时间数据来映射颜色
-          colorscale: colors
-            ? [...colors]
-            : [
-                [0, "#F9FDD2"],
-                [0.15, "#E9F6BD"],
-                [0.3, "#C2E3B9"],
-                [0.45, "#8AC8BE"],
-                [0.6, "#5CA8BF"],
-                [0.75, "#407DB3"],
-                [0.9, "#2E4C9A"],
-                [1, "#1B2973"],
-              ], // 默认颜色渐变
-          colorbar: {
-            title: { text: scatterData.colorbartitle || "月份" }, // 色标标题
-            tickvals: tickvals, // 设置刻度值
-            ticktext: ticktext, // 设置刻度文本
-            tickmode: "array", // 使用数组模式
-            // tickangle: -45, // 可选:调整刻度文本的角度
-          },
-          size: 4, // 点的大小
-          line: {
-            color: "#fff", // 让边框颜色和点颜色相同
-            width: 0.3, // 设置边框宽度
-          },
-        },
-        hovertemplate:
-          `${data.xaixs}: %{x} <br> ` +
-          `${data.yaixs}: %{y} <br> ` +
-          `时间: %{customdata}<extra></extra>`, // 在 hover 中显示格式化后的时间
-        customdata: scatterData.colorbar || scatterData.color, // 将格式化后的时间存入 customdata
-      };
+    const colorscale = colorStops.map((color, index) => {
+      const proportion = index / (colorStops.length - 1);
+      return [proportion, color];
+    });
 
-      // 绘制线图
-      let lineTrace = {};
-      if (lineData) {
-        lineTrace = {
-          x: lineData.xData, // 线数据的 xData
-          y: lineData.yData, // 线数据的 yData
-          mode: "lines+markers", // 线和点同时显示
-          type: "scattergl", // 使用 scattergl 类型
-          text: lineData.engineName, // 提示文本
-          line: {
-            color: "red", // 线条颜色
-          },
-        };
-      }
+    if (colorscale.length < 2) {
+      colorscale.push([1, safePick(0)]);
+    }
 
-      // 图表布局
-      const layout = {
-        title: {
-          text: scatterData.title,
-          font: {
-            size: 16,
-            weight: "bold",
-          },
-        },
-        xaxis: {
-          title: {
-            text: data.xaixs,
-          },
-          gridcolor: "rgb(255,255,255)", // 网格线颜色
-          tickcolor: "rgb(255,255,255)",
-          backgroundcolor: "#e5ecf6",
-          showbackground: true, // 显示背景
+    // ✅ 4. 映射颜色值
+    const colorValues = rawColorData.map((v) => timeMapping[v]);
+
+    // ✅ 5. scatter trace
+    const scatterTrace = {
+      x: scatterData.xData,
+      y: scatterData.yData,
+      type: "scattergl",
+      mode: "markers",
+      name: scatterData.engineName,
+      marker: {
+        color: colorValues,
+        colorscale,
+        size: 4,
+        colorbar: {
+          title: { text: scatterData.colorbartitle || "月份" },
+          tickvals,
+          ticktext,
+          tickmode: "array",
         },
-        yaxis: {
-          title: { text: data.yaixs },
-          gridcolor: "rgb(255,255,255)", // 网格线颜色
-          tickcolor: "rgb(255,255,255)",
-          backgroundcolor: "#e5ecf6",
-          showbackground: true, // 显示背景
+        line: {
+          color: "#fff",
+          width: 0.3,
         },
-        showlegend: false,
-        plot_bgcolor: "#e5ecf6",
-        gridcolor: "#fff", // 设置网格线颜色
-      };
-
-      // 准备 HTML 内容
-      const htmlContent = `
-        <!DOCTYPE html>
-        <html>
-        <head>
-          <meta charset="UTF-8">
-          <title>2D 散点图</title>
-          <script>${plotlyContent}</script>
-        </head>
-        <body>
-          <div id="chart" style="width: 100%; height: 600px"></div>
-          <script>
-            const traces = [${JSON.stringify(scatterTrace)}${
-        lineTrace ? `, ${JSON.stringify(lineTrace)}` : ""
-      }];
-            const layout = ${JSON.stringify(layout)};
-            Plotly.newPlot('chart', traces, layout, {
-  responsive: true,
-  displayModeBar: false,
-  staticPlot: true
-}).then(() => {
-              window.chartRendered = true; // 确保在图表渲染完成后设置
-              
-            }).catch((error) => {
-              console.error("图表渲染错误:", error); // 捕获渲染错误
-            });
-          </script>
-        </body>
-        </html>
-      `;
-
-      // 设置页面内容
-      await page.setContent(htmlContent, {
-        waitUntil: "domcontentloaded",
-        timeout: 0,
-      });
-
-      await page.waitForTimeout(1000);
-
-      // 等待图表渲染完成,延长超时时间
-      await page.waitForFunction(() => window.chartRendered === true, {
-        timeout: 150000, // 延长到 150 秒
+      },
+      customdata: rawColorData,
+      hovertemplate:
+        `${data.xaixs}: %{x}<br>` +
+        `${data.yaixs}: %{y}<br>` +
+        `时间: %{customdata}<extra></extra>`,
+    };
+
+    // ✅ 6. 合同曲线
+    const traces = [scatterTrace];
+
+    if (lineData?.yData?.length) {
+      traces.push({
+        x: lineData.xData,
+        y: lineData.yData,
+        type: "scatter",
+        mode: "lines+markers",
+        name: "合同功率曲线",
+        line: { color: "red" },
+        marker: { color: "red", size: 4 },
       });
+    }
 
-      // 截图并保存到临时文件
-      const chartElement = await page.$("#chart");
-      await chartElement.screenshot({
-        path: tempFilePath,
-        type: "jpeg",
-      });
+    // ✅ 7. layout
+    const layout = {
+      title: {
+        text: scatterData.title || "散点图",
+        font: { size: 16 },
+      },
+      xaxis: {
+        title: data.xaixs,
+        gridcolor: "rgb(255,255,255)",
+        tickcolor: "rgb(255,255,255)",
+        backgroundcolor: "#e5ecf6",
+        showbackground: true,
+        showline: true, // ✅ 显示 X 轴轴线
+        zeroline: false,
+        linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+      },
+      yaxis: {
+        title: data.yaixs,
+        gridcolor: "rgb(255,255,255)",
+        tickcolor: "rgb(255,255,255)",
+        backgroundcolor: "#e5ecf6",
+        showbackground: true,
+        showline: true, // ✅ 显示 X 轴轴线
+        zeroline: false,
+        linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
+      },
+      showlegend: false,
+      plot_bgcolor: "#e5ecf6",
+      paper_bgcolor: "#e5ecf6",
+    };
+
+    // ✅ 8. 统一渲染(核心🔥)
+    const url = await renderChart({
+      traces,
+      layout,
+      bucketName,
+      objectName,
+    });
 
-      // 上传图片到服务器
-      const formData = new FormData();
-      formData.append("file", fs.createReadStream(tempFilePath));
-      // 发送上传请求
-      //在这里需要传入风场编号-风场编号+分析编号-分析类型-manual-名称.png
-      const response = await axios.post(
-        `${process.env.API_BASE_URL}/examples/upload`,
-        {
-          filePath: tempFilePath,
-          bucketName, //桶名称
-          objectName, //在 MinIO 中的文件名
-        },
-      );
-      // return response.data; // 返回上传结果
-      return response?.data?.url;
-    } catch (error) {
-      throw error;
-    } finally {
-      await browser.close();
-    }
+    return url;
   } catch (error) {
-    console.error("生成2D散点图失败:", error);
+    console.error("❌ 生成2D散点图失败:", error);
     throw error;
   }
 };

+ 51 - 144
downLoadServer/src/server/utils/chartsCom/yawErrorBarSum.js

@@ -1,10 +1,4 @@
-import puppeteer from "puppeteer";
-import fs from "fs-extra";
-import path from "path";
-import FormData from "form-data";
-import axios from "axios"; // 导入 axios
-import { colorSchemes } from "../colors.js";
-import { text } from "stream/consumers";
+import { renderChart } from "../chartService/index.js";
 
 export const getYawErrorBarSumCharts = async (
   data,
@@ -13,165 +7,78 @@ export const getYawErrorBarSumCharts = async (
   analysisTypeCode,
 ) => {
   try {
-    // 提取静态偏航误差值和机组数量
-    let a = [];
-    let b = [];
-    let c = [];
-    data.map((item) => {
-      if (item["[0,3]"] != "0.0") {
-        a.push(item["[0,3]"]);
-      }
-      if (item["(3,5]"] != "0.0") {
-        b.push(item["(3,5]"]);
-      }
-      if (item["(5, )"] != "0.0") {
-        c.push(item["(5, )"]);
-      }
-    });
+    // ===== 数据处理(修复版)=====
+    let countA = 0;
+    let countB = 0;
+    let countC = 0;
 
-    const xData = ["(0,3]", "(3,5]", "(>5]"]; // 偏航误差区间
-    const yData = [a.length, b.length, c.length]; // 各区间的机组数量
+    data.forEach((item) => {
+      const v1 = Number(item["[0,3]"] || 0);
+      const v2 = Number(item["(3,5]"] || 0);
+      const v3 = Number(item["(5, )"] || 0);
 
-    // 每个柱子的颜色
-    const colors = [
-      "#8AC8BE", // (0, 3] 蓝色
-      "#407DB3", // (3, 5] 绿色
-      "#1B2973", // (5, ∞] 红色
-    ];
+      if (v1 !== 0) countA++;
+      if (v2 !== 0) countB++;
+      if (v3 !== 0) countC++;
+    });
+
+    const xData = ["(0,3]", "(3,5]", "(>5]"];
+    const yData = [countA, countB, countC];
 
-    const trace = {
-      x: xData, // 横坐标数据
-      y: yData, // 纵坐标数据
-      type: "bar", // 当前图表类型
-      marker: {
-        color: colors, // 为每个柱子分配不同的颜色
+    // ===== trace =====
+    const traces = [
+      {
+        x: xData,
+        y: yData,
+        type: "bar",
+        marker: {
+          color: ["#8AC8BE", "#407DB3", "#1B2973"],
+        },
+        hovertemplate: "偏航误差区间: %{x}<br>台数: %{y}<extra></extra>",
       },
-      // hovertemplate:
-      //   `偏航误差值:` + ` %{x} <br> ` + `台数:` + "%{y} <br> <extra></extra>",
-    };
+    ];
 
+    // ===== layout =====
     const layout = {
       title: {
         text: "静态偏航误差的绝对值机组台数分布情况",
-        font: {
-          size: 16,
-          weight: "bold",
-        },
-      }, // 图表标题
+        font: { size: 16 },
+      },
       xaxis: {
-        title: { text: "静态偏航误差值(度)" }, // 横坐标标题
+        title: { text: "静态偏航误差值(度)" },
+        gridcolor: "#fff",
         gridcolor: "rgb(255,255,255)",
         tickcolor: "rgb(255,255,255)",
         backgroundcolor: "#e5ecf6",
+        showbackground: true,
+        showline: true, // ✅ 显示 X 轴轴线
+        zeroline: false,
+        linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
       },
       yaxis: {
-        title: {
-          text: "台数",
-        }, // 纵坐标标题
+        title: { text: "台数" },
+        gridcolor: "#fff",
         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, // 开启自适应
-      // showlegend: true, // 显示图例
+      margin: { l: 50, r: 50, t: 50, b: 50 },
       plot_bgcolor: "#e5ecf6",
-      gridcolor: "#fff",
-      bgcolor: "#e5ecf6", // 设置背景颜色
     };
 
-    // 创建临时目录
-    const tempDir = path.join(process.cwd(), "images");
-    await fs.ensureDir(tempDir);
-    const tempFilePath = path.join(
-      tempDir,
-      `temp_yaw_error_bar_sum_chart_${Date.now()}.jpeg`,
-    );
-    // 获取 plotly.js 的绝对路径
-    const plotlyPath = path.join(
-      process.cwd(),
-      "src",
-      "public",
-      "js",
-      "plotly-3.0.1.min.js",
-    );
-    const plotlyContent = await fs.readFile(plotlyPath, "utf-8");
-
-    // 使用 Puppeteer 生成图表的截图
-    const browser = await puppeteer.launch({
-      headless: "new",
-      // 根据系统改路径
-      executablePath: `${process.env.CHROME_PATH}`, // 根据系统改路径
-
-      args: ["--no-sandbox", "--disable-setuid-sandbox"],
+    // ===== 统一渲染(关键)=====
+    return await renderChart({
+      traces,
+      layout,
+      bucketName,
+      objectName,
     });
-    try {
-      const page = await browser.newPage();
-      const htmlContent = `
-        <!DOCTYPE html>
-        <html>
-          <head>
-            <meta charset="UTF-8">
-            <title>静态偏航误差值</title>
-            <script>${plotlyContent}</script>
-            <style>
-              body { margin: 0; }
-              #chart { width: 800px; height: 600px; }
-            </style>
-          </head>
-          <body>
-            <div id="chart"></div>
-            <script>
-              window.onload = function() {
-                Plotly.newPlot('chart', [${JSON.stringify(
-                  trace,
-                )}], ${JSON.stringify(layout)},{
-  responsive: true,
-  displayModeBar: false,
-  staticPlot: true
-}).then(() => {
-                  window.chartRendered = true;
-                });
-              };
-            </script>
-          </body>
-        </html>
-      `;
-      await page.setContent(htmlContent, {
-        waitUntil: "domcontentloaded",
-        timeout: 0,
-      });
-
-      await page.waitForTimeout(1000);
-      await page.waitForFunction(() => window.chartRendered === true, {
-        timeout: 60000,
-      });
-      // 截图并保存到临时文件
-      const chartElement = await page.$("#chart");
-      await chartElement.screenshot({ path: tempFilePath, type: "jpeg" });
-      // 上传图片到服务器
-      const formData = new FormData();
-      formData.append("file", fs.createReadStream(tempFilePath));
-      const response = await axios.post(
-        `${process.env.API_BASE_URL}/examples/upload`,
-        {
-          filePath: tempFilePath,
-          bucketName,
-          objectName,
-        },
-      );
-      return response.data.url;
-    } catch (error) {
-      console.error("生成图表失败:", error);
-    } finally {
-      await browser.close();
-    }
   } catch (error) {
-    console.error("发生错误:", error);
+    console.error("偏航误差柱状图生成失败:", error);
+    throw error;
   }
 };

+ 73 - 167
downLoadServer/src/server/utils/chartsCom/yawErrorLine.js

@@ -1,17 +1,5 @@
-/*
- * @Author: your name
- * @Date: 2025-05-14 10:49:00
- * @LastEditTime: 2026-03-17 09:35:08
- * @LastEditors: MacBookPro
- * @Description: In User Settings Edit
- * @FilePath: /downLoadServer/src/server/utils/chartsCom/yawErrorLine.js
- */
-import puppeteer from "puppeteer";
-import fs from "fs-extra";
-import path from "path";
-import FormData from "form-data";
+import { renderChart } from "../chartService/index.js";
 import { colorSchemes } from "../colors.js";
-import axios from "axios";
 
 export const generateYawErrorLine = async (
   chartData,
@@ -19,183 +7,101 @@ export const generateYawErrorLine = async (
   objectName,
 ) => {
   try {
-    const data = [];
-    const colors = [...colorSchemes[0].colors]; // 生成颜色
-    // 获取 plotly.js 的绝对路径
-    const plotlyPath = path.join(
-      process.cwd(),
-      "src",
-      "public",
-      "js",
-      "plotly-3.0.1.min.js",
-    );
-    const plotlyContent = await fs.readFile(plotlyPath, "utf-8");
+    const traces = [];
+    const colors = [...colorSchemes[0].colors];
 
-    chartData &&
-      chartData.data &&
-      chartData.data.forEach((turbine, index) => {
-        // 判断图表类型,根据类型调整绘制方式
-        const chartConfig = {
-          x: turbine.xData, // X 数据
-          y: turbine.yData, // Y 数据
-          name: turbine.legend, // 使用机组名称
-          line: {
-            color: colors[index % colors.length], // 为每个机组分配不同的颜色
-          },
-          marker: {
-            color: colors[index % colors.length], // 为每个机组分配不同的颜色
-          },
-          hovertemplate:
-            `${chartData.xaixs}:` +
-            ` %{x} <br> ` +
-            `${chartData.yaixs}:` +
-            "%{y} <br> <extra></extra>",
-        };
+    // ===== 主数据 =====
+    chartData?.data?.forEach((turbine, index) => {
+      const baseTrace = {
+        x: turbine.xData || [],
+        y: turbine.yData || [],
+        name: turbine.legend || `机组${index + 1}`,
+        type: "scatter", // ✅ 必须加!
+        line: {
+          color: colors[index % colors.length],
+        },
+        marker: {
+          color: colors[index % colors.length],
+        },
+        hovertemplate:
+          `${chartData.xaixs || "X"}: %{x}<br>` +
+          `${chartData.yaixs || "Y"}: %{y}<extra></extra>`,
+      };
 
-        if (chartData.chartType === "line") {
-          chartConfig.mode = "lines+markers"; // 如果是折线图
-          chartConfig.fill = "none";
-        } else if (chartData.chartType === "bar") {
-          chartConfig.fill = "tonexty";
-        }
+      if (chartData.chartType === "line") {
+        baseTrace.mode = "lines+markers";
+      } else if (chartData.chartType === "area") {
+        baseTrace.mode = "lines";
+        baseTrace.fill = "tonexty"; // ✅ 面积图才用
+      }
 
-        data.push(chartConfig);
+      traces.push(baseTrace);
+    });
+
+    // ===== 合同功率曲线 =====
+    if (
+      chartData.contract_Cp_curve_yData?.length &&
+      chartData.contract_Cp_curve_xData?.length
+    ) {
+      traces.push({
+        x: chartData.contract_Cp_curve_xData,
+        y: chartData.contract_Cp_curve_yData,
+        type: "scatter",
+        mode: "lines+markers",
+        name: "合同功率曲线",
+        line: {
+          color: "red",
+          width: 1,
+        },
+        marker: {
+          color: "red",
+          size: 4,
+        },
       });
+    }
 
+    // ===== layout =====
     const layout = {
       title: {
-        text: chartData.data[0].title,
-        font: {
-          size: 16, // 设置标题字体大小(默认 16)
-          weight: "bold",
-        },
+        text: chartData?.data?.[0]?.title || "偏航误差图",
+        font: { size: 16 },
       },
       xaxis: {
+        title: chartData.xaixs || "X轴",
+        gridcolor: "#fff",
         gridcolor: "rgb(255,255,255)",
         tickcolor: "rgb(255,255,255)",
         backgroundcolor: "#e5ecf6",
-        title: chartData.xaixs || "X轴", // 横坐标标题
+        showbackground: true,
+        showline: true, // ✅ 显示 X 轴轴线
+        zeroline: false,
+        linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
       },
       yaxis: {
+        title: chartData.yaixs || "Y轴",
+        gridcolor: "#fff",
         gridcolor: "rgb(255,255,255)",
         tickcolor: "rgb(255,255,255)",
         backgroundcolor: "#e5ecf6",
-        title: chartData.yaixs || "Y轴", // 纵坐标标题
+        showbackground: true,
+        showline: true, // ✅ 显示 X 轴轴线
+        zeroline: false,
+        linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
       },
-      //   margin: {
-      //     l: 50,
-      //     r: 50,
-      //     t: 50,
-      //     b: 50,
-      //   },
       plot_bgcolor: "#e5ecf6",
-      gridcolor: "#fff",
-      bgcolor: "#e5ecf6", // 设置背景颜色
-      autosize: true, // 开启自适应
-      barmode: chartData.chartType === "bar" ? "stack" : "group", // 如果是柱状图则启用堆叠
+      barmode: chartData.chartType === "bar" ? "stack" : "group",
+      showlegend: true,
     };
 
-    if (
-      chartData.contract_Cp_curve_yData &&
-      chartData.contract_Cp_curve_yData.length > 0
-    ) {
-      data.push({
-        x: chartData.contract_Cp_curve_xData,
-        y: chartData.contract_Cp_curve_yData,
-        mode: "lines+markers",
-        name: "合同功率曲线",
-        line: {
-          color: "red",
-          width: 1, // 设置线条的宽度为1
-        },
-        marker: { color: "red", size: 4 },
-      });
-    }
-
-    // 使用 Puppeteer 生成图表的截图
-    const browser = await puppeteer.launch({
-      headless: "new",
-      // 根据系统改路径
-      executablePath: `${process.env.CHROME_PATH}`, // 根据系统改路径
-
-      args: ["--no-sandbox", "--disable-setuid-sandbox"],
+    // ===== 核心:统一渲染 =====
+    return await renderChart({
+      traces,
+      layout,
+      bucketName,
+      objectName,
     });
-
-    const tempDir = path.join(process.cwd(), "images");
-    await fs.ensureDir(tempDir);
-    const tempFilePath = path.join(
-      tempDir,
-      `temp_yaw_error_chart_${Date.now()}.jpeg`,
-    );
-
-    try {
-      const page = await browser.newPage();
-      const htmlContent = `
-        <!DOCTYPE html>
-        <html>
-          <head>
-            <meta charset="UTF-8">
-            <title>静态偏航折线分图</title>
-            <script>${plotlyContent}</script>
-            <style>
-              body { margin: 0; }
-              #chart { width: 800px; height: 600px; }
-            </style>
-          </head>
-          <body>
-            <div id="chart"></div>
-            <script>
-              window.onload = function() {
-                Plotly.newPlot('chart', ${JSON.stringify(
-                  data,
-                )}, ${JSON.stringify(layout)},{
-  responsive: true,
-  displayModeBar: false,
-  staticPlot: true
-}).then(() => {
-                  window.chartRendered = true;
-                });
-              };
-            </script>
-          </body>
-        </html>
-      `;
-
-      await page.setContent(htmlContent, {
-        waitUntil: "domcontentloaded",
-        timeout: 0,
-      });
-
-      await page.waitForTimeout(1000);
-      await page.waitForFunction(() => window.chartRendered === true, {
-        timeout: 60000,
-      });
-
-      // 截图并保存到临时文件
-      const chartElement = await page.$("#chart");
-      await chartElement.screenshot({ path: tempFilePath, type: "jpeg" });
-
-      // 上传图片到服务器
-      const formData = new FormData();
-      formData.append("file", fs.createReadStream(tempFilePath));
-      const response = await axios.post(
-        `${process.env.API_BASE_URL}/examples/upload`,
-        {
-          filePath: tempFilePath,
-          bucketName,
-          objectName,
-        },
-      );
-
-      return response?.data?.url;
-    } catch (error) {
-      console.error("生成折线图失败:", error);
-      throw error;
-    } finally {
-      await browser.close();
-    }
   } catch (error) {
-    console.error("生成折线图失败:", error);
+    console.error("偏航误差折线图生成失败:", error);
     throw error;
   }
 };

+ 97 - 31
downLoadServer/src/server/utils/minioService.js

@@ -1,40 +1,106 @@
-/*
- * @Author: your name
- * @Date: 2025-04-28 14:43:09
- * @LastEditTime: 2026-03-13 14:54:29
- * @LastEditors: milo-MacBook-Pro.local
- * @Description: In User Settings Edit
- * @FilePath: /downLoadServer/src/server/utils/minioService.js
- */
-// src/minioService.js
+// /*
+//  * @Author: your name
+//  * @Date: 2025-04-28 14:43:09
+//  * @LastEditTime: 2026-03-13 14:54:29
+//  * @LastEditors: milo-MacBook-Pro.local
+//  * @Description: In User Settings Edit
+//  * @FilePath: /downLoadServer/src/server/utils/minioService.js
+//  */
+// // src/minioService.js
+// import { Client } from "minio";
+
+// // 创建 MinIO 客户端
+// export const minioClient = new Client({
+//   endPoint: process.env.MINIO_ENDPOINT, // 从环境变量中获取 MinIO 服务器地址
+//   port: parseInt(process.env.MINIO_PORT, 10), // 从环境变量中获取 MinIO 端口
+//   useSSL: false, // 如果使用 HTTPS,请设置为 true
+//   accessKey: process.env.MINIO_ACCESS_KEY, // 从环境变量中获取 MinIO 访问密钥
+//   secretKey: process.env.MINIO_SECRET_KEY, // 从环境变量中获取 MinIO 秘密密钥
+//   // endPoint: "192.168.50.233", // 不需要包含 http://
+//   // port: 6900, // 确保端口正确
+//   // useSSL: false, // 如果使用 HTTPS,请设置为 true
+//   // accessKey: "haH1vePq7unSp4TG1One", // MinIO 访问密钥
+//   // secretKey: "idxO5SAjboUYERpDICgHgBoHX7bcYv355lMQANt6", // MinIO 秘密密钥
+// });
+
+// // 上传文件到 MinIO
+// export const uploadFileToMinIO = async (bucketName, filePath, objectName) => {
+//   try {
+//     // 检查桶是否存在,如果不存在则创建
+//     const bucketExists = await minioClient.bucketExists(bucketName);
+//     if (!bucketExists) {
+//       await minioClient.makeBucket(bucketName, "us-east-1"); // 使用适当的区域
+//       console.log(`Bucket ${bucketName} created successfully.`);
+//     }
+//     // 上传文件
+//     await minioClient.fPutObject(bucketName, objectName, filePath);
+//   } catch (error) {
+//     console.error("Error uploading file to MinIO:", error);
+//   }
+// };
+// src/server/utils/minioService.js
+import dotenv from "dotenv";
 import { Client } from "minio";
 
-// 创建 MinIO 客户端
+// 兜底:避免某些入口未提前加载环境变量
+dotenv.config();
+
+function normalizeEnvValue(value) {
+  if (value == null) return "";
+  // 常见误配:末尾多了逗号/空格(会导致 Authorization 头被解析成“缺字段”)
+  return String(value).trim().replace(/,+$/, "");
+}
+
+function getRequiredEnv(name) {
+  const v = normalizeEnvValue(process.env[name]);
+  if (!v) {
+    throw new Error(
+      `MinIO 配置缺失:环境变量 ${name} 为空。请检查 downLoadServer/.env 是否已加载`,
+    );
+  }
+  return v;
+}
+
+const MINIO_ENDPOINT = getRequiredEnv("MINIO_ENDPOINT");
+const MINIO_PORT_RAW = getRequiredEnv("MINIO_PORT");
+const MINIO_PORT = Number.parseInt(MINIO_PORT_RAW, 10);
+if (!Number.isFinite(MINIO_PORT)) {
+  throw new Error(`MinIO 配置错误:MINIO_PORT 非法 (${MINIO_PORT_RAW})`);
+}
+
+const MINIO_ACCESS_KEY = getRequiredEnv("MINIO_ACCESS_KEY");
+const MINIO_SECRET_KEY = getRequiredEnv("MINIO_SECRET_KEY");
+
 export const minioClient = new Client({
-  endPoint: process.env.MINIO_ENDPOINT, // 从环境变量中获取 MinIO 服务器地址
-  port: parseInt(process.env.MINIO_PORT, 10), // 从环境变量中获取 MinIO 端口
+  endPoint: MINIO_ENDPOINT, // 不要包含 http(s)://
+  port: MINIO_PORT,
   useSSL: false, // 如果使用 HTTPS,请设置为 true
-  accessKey: process.env.MINIO_ACCESS_KEY, // 从环境变量中获取 MinIO 访问密钥
-  secretKey: process.env.MINIO_SECRET_KEY, // 从环境变量中获取 MinIO 秘密密钥
-  // endPoint: "192.168.50.233", // 不需要包含 http://
-  // port: 6900, // 确保端口正确
-  // useSSL: false, // 如果使用 HTTPS,请设置为 true
-  // accessKey: "haH1vePq7unSp4TG1One", // MinIO 访问密钥
-  // secretKey: "idxO5SAjboUYERpDICgHgBoHX7bcYv355lMQANt6", // MinIO 秘密密钥
+  accessKey: MINIO_ACCESS_KEY,
+  secretKey: MINIO_SECRET_KEY,
 });
+/**
+ * 上传 Buffer 到 MinIO
+ * @param {string} bucketName
+ * @param {string} objectName
+ * @param {Buffer} buffer
+ * @returns {string} 返回可访问 URL
+ */
 
-// 上传文件到 MinIO
-export const uploadFileToMinIO = async (bucketName, filePath, objectName) => {
+export async function uploadBufferToMinIO(bucketName, objectName, buffer) {
   try {
-    // 检查桶是否存在,如果不存在则创建
-    const bucketExists = await minioClient.bucketExists(bucketName);
-    if (!bucketExists) {
-      await minioClient.makeBucket(bucketName, "us-east-1"); // 使用适当的区域
-      console.log(`Bucket ${bucketName} created successfully.`);
+    // 检查桶是否存在
+    const exists = await minioClient.bucketExists(bucketName);
+    if (!exists) {
+      await minioClient.makeBucket(bucketName);
     }
-    // 上传文件
-    await minioClient.fPutObject(bucketName, objectName, filePath);
-  } catch (error) {
-    console.error("Error uploading file to MinIO:", error);
+
+    // 上传
+    await minioClient.putObject(bucketName, objectName, buffer);
+
+    // 返回可访问 URL
+    return `http://${MINIO_ENDPOINT}:${MINIO_PORT}/${bucketName}/${objectName}`;
+  } catch (err) {
+    console.error("Upload buffer failed:", err);
+    throw err; // 上传失败直接抛出异常
   }
-};
+}

BIN
downLoadServer/temp-images/image_1773380356597_0.webp


BIN
downLoadServer/temp-images/image_1773380356597_1.webp


BIN
downLoadServer/temp-images/image_1773380356604_0.webp


BIN
downLoadServer/temp-images/image_1773381971053_0.webp


BIN
downLoadServer/temp-images/image_1773381971053_1.webp


BIN
downLoadServer/temp-images/image_1773381971056_0.webp


BIN
downLoadServer/temp-images/images_1773380356605.zip