PlotlyCharts.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. /*
  2. * @Author: your name
  3. * @Date: 2025-05-15 15:22:19
  4. * @LastEditTime: 2025-05-26 11:00:41
  5. * @LastEditors: bogon
  6. * @Description: In User Settings Edit
  7. * @FilePath: /downLoadServer/src/server/utils/chartsCom/PlotlyCharts.js
  8. */
  9. import puppeteer from "puppeteer";
  10. import fs from "fs-extra";
  11. import path from "path";
  12. import FormData from "form-data";
  13. import { colorSchemes } from "../colors.js";
  14. import axios from "axios";
  15. import { text } from "stream/consumers";
  16. /**
  17. * 生成折线图并上传
  18. * @param {Object} options - 配置选项
  19. * @param {string} options.fileAddr - 数据文件地址
  20. * @param {string[]} options.color1 - 颜色数组1
  21. * @param {string[]} options.colors - 颜色数组2
  22. * @returns {Promise<String>} - 返回图片URL
  23. */
  24. export const generatePlotlyCharts = async (
  25. chartData,
  26. bucketName,
  27. objectName
  28. ) => {
  29. const colorSchemesItem = colorSchemes[0].colors;
  30. const tempDir = path.join(process.cwd(), "images");
  31. const tempFilePath = path.join(tempDir, `temp_line_chart_${Date.now()}.jpeg`);
  32. let browser;
  33. try {
  34. // 构建数据
  35. const data = [];
  36. const newData = chartData.data.filter(
  37. (item) => item.enginName !== "合同功率曲线"
  38. );
  39. newData.forEach((turbine, index) => {
  40. data.push({
  41. x: turbine.xData,
  42. y: turbine.yData,
  43. name: turbine.enginName,
  44. mode: "lines",
  45. connectgaps: false,
  46. line: { color: colorSchemesItem[index % colorSchemesItem.length] },
  47. marker: { color: colorSchemesItem[index % colorSchemesItem.length] },
  48. });
  49. });
  50. if (
  51. chartData.data[chartData.data.length - 1] &&
  52. chartData.data[chartData.data.length - 1].enginName === "合同功率曲线" &&
  53. chartData.data[chartData.data.length - 1].yData.length > 0
  54. ) {
  55. data.push({
  56. x: chartData.data[chartData.data.length - 1].xData,
  57. y: chartData.data[chartData.data.length - 1].yData,
  58. mode: "lines+markers",
  59. name: "合同功率曲线",
  60. line: {
  61. color: "red",
  62. width: 1,
  63. },
  64. marker: { color: "red", size: 4 },
  65. });
  66. }
  67. const layout = {
  68. title: {
  69. text: `有功功率曲线分析${chartData.engineTypeName}`,
  70. font: { size: 16, weight: "bold" },
  71. },
  72. xaxis: { title: { text: "风速(m / s)" || "X轴" }, gridcolor: "#fff" },
  73. yaxis: { title: { text: "功率(kW)" || "Y轴" }, gridcolor: "#fff" },
  74. margin: { l: 50, r: 50, t: 50, b: 50 },
  75. plot_bgcolor: "#e5ecf6",
  76. bgcolor: "#e5ecf6",
  77. autosize: true,
  78. barmode: "group",
  79. legend: {
  80. orientation: "h",
  81. xanchor: "center",
  82. x: 0.5,
  83. y: -0.2,
  84. },
  85. };
  86. await fs.ensureDir(tempDir);
  87. const plotlyPath = path.join(
  88. process.cwd(),
  89. "src",
  90. "public",
  91. "js",
  92. "plotly-latest.min.js"
  93. );
  94. const plotlyContent = await fs.readFile(plotlyPath, "utf-8");
  95. browser = await puppeteer.launch({
  96. headless: "new",
  97. args: ["--no-sandbox", "--disable-setuid-sandbox"],
  98. });
  99. const page = await browser.newPage();
  100. const htmlContent = `
  101. <!DOCTYPE html>
  102. <html>
  103. <head>
  104. <script>${plotlyContent}</script>
  105. <style>body { margin: 0; } #chart { width: 800px; height: 600px; }</style>
  106. </head>
  107. <body>
  108. <div id="chart"></div>
  109. <script>
  110. window.onload = function () {
  111. const data = ${JSON.stringify(data)};
  112. const layout = ${JSON.stringify(layout)};
  113. Plotly.newPlot('chart', data, layout, { responsive: true }).then(() => {
  114. window.chartRendered = true;
  115. });
  116. };
  117. </script>
  118. </body>
  119. </html>`;
  120. await page.setContent(htmlContent, { waitUntil: "networkidle0" });
  121. await page.waitForFunction(() => window.chartRendered === true, {
  122. timeout: 60000,
  123. });
  124. const chartElement = await page.$("#chart");
  125. await chartElement.screenshot({ path: tempFilePath, type: "jpeg" });
  126. // ✅ 上传前判断文件是否存在
  127. if (!(await fs.pathExists(tempFilePath))) {
  128. console.error("❌ 图表文件未生成:", tempFilePath);
  129. throw new Error("图表截图文件未生成");
  130. }
  131. // ✅ 发起上传
  132. try {
  133. const response = await axios.post(
  134. `${process.env.API_BASE_URL}/examples/upload`,
  135. {
  136. filePath: tempFilePath,
  137. bucketName,
  138. objectName,
  139. }
  140. );
  141. const imageUrl = response?.data?.url;
  142. console.log("✅ 上传成功:", imageUrl);
  143. return imageUrl;
  144. } catch (uploadError) {
  145. console.error("❌ 上传失败:", uploadError.message);
  146. throw uploadError;
  147. } finally {
  148. // ✅ 上传后安全删除
  149. try {
  150. if (await fs.pathExists(tempFilePath)) {
  151. await fs.unlink(tempFilePath);
  152. console.log("🧹 临时文件已删除:", tempFilePath);
  153. }
  154. } catch (deleteError) {
  155. console.warn("⚠️ 删除临时文件失败:", deleteError.message);
  156. }
  157. }
  158. } catch (error) {
  159. console.error("❌ 生成折线图失败:", error.message);
  160. throw error;
  161. } finally {
  162. if (browser) {
  163. await browser.close();
  164. }
  165. }
  166. };