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