powerMarkers2DCharts.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. <!--
  2. * @Author: your name
  3. * @Date: 2024-09-11 14:32:12
  4. * @LastEditTime: 2025-02-20 16:03:24
  5. * @LastEditors: bogon
  6. * @Description: In User Settings Edit
  7. * @FilePath: /performance-test/src/views/performance/components/chartsCom/powerMarkers2DCharts.vue
  8. -->
  9. <!-- <template>
  10. <div> -->
  11. <!-- powerMarkers2DCharts
  12. <h1>逐月有功功率散点2D分析</h1> -->
  13. <!-- <h1>偏航控制策略异常检测 2D</h1> 目前没找到这个分析类型-->
  14. <template>
  15. <div style="min-height: 300px">
  16. <!-- 2D散点图 -->
  17. <template>
  18. <div style="display: flex; align-items: center; margin-top: 20px">
  19. <div style="margin-right: 20px; display: flex; align-items: center">
  20. <el-select
  21. size="small"
  22. v-model="color1"
  23. @change="updateChartColor"
  24. placeholder="选择配色方案"
  25. style="width: 200px"
  26. >
  27. <el-option
  28. v-for="(scheme, index) in colorSchemes"
  29. :key="index"
  30. :label="scheme.label"
  31. :value="scheme.colors"
  32. :style="getOptionStyle(scheme.colors)"
  33. ></el-option>
  34. </el-select>
  35. </div>
  36. </div>
  37. <!-- 点大小控制 -->
  38. <div style="display: flex; align-items: center">
  39. <!-- <span style="margin-right: 10px">点大小</span> -->
  40. <el-slider
  41. v-model="pointSize"
  42. :min="3"
  43. :max="15"
  44. :step="1"
  45. label="点的大小"
  46. show-stops
  47. style="width: 150px"
  48. @change="updateChartColor"
  49. ></el-slider>
  50. </div>
  51. <div
  52. v-loading="loading"
  53. :ref="`plotlyChart-${index}`"
  54. style="height: 400px"
  55. >
  56. <el-empty v-if="isError" description="请求失败"></el-empty>
  57. </div>
  58. </template>
  59. </div>
  60. </template>
  61. <script>
  62. import Plotly from "plotly.js-dist";
  63. import axios from "axios";
  64. import { myMixin } from "@/mixins/chartRequestMixin";
  65. import { colorSchemes } from "@/views/overview/js/colors";
  66. export default {
  67. mixins: [myMixin],
  68. props: {
  69. fileAddr: {
  70. default: "",
  71. type: String,
  72. },
  73. index: {
  74. type: String,
  75. },
  76. },
  77. data() {
  78. return {
  79. pointSize: 5, // 默认点大小
  80. chartData: {},
  81. chartType: "scatter", // 默认显示散点图
  82. color1: colorSchemes[0].colors, // 默认颜色
  83. selectedPoints: [],
  84. originalColors: [],
  85. originalSizes: [],
  86. // 配色方案列表(每个方案是一个颜色数组)
  87. colorSchemes: [...colorSchemes],
  88. };
  89. },
  90. async mounted() {
  91. this.getData();
  92. this.color1 = colorSchemes[0].colors;
  93. // console.log(this.color1, colorSchemes[0].colors, "color1");
  94. },
  95. methods: {
  96. // 根据配色方案设置每个选项的样式
  97. getOptionStyle(scheme) {
  98. return {
  99. background: `linear-gradient(to right, ${scheme.join(", ")})`,
  100. color: "#fff",
  101. height: "30px",
  102. lineHeight: "30px",
  103. borderRadius: "4px",
  104. };
  105. },
  106. async getData() {
  107. if (this.fileAddr !== "") {
  108. try {
  109. this.loading = true;
  110. this.cancelToken = axios.CancelToken.source();
  111. const resultChartsData = await axios.get(this.fileAddr, {
  112. cancelToken: this.cancelToken.token,
  113. });
  114. this.chartData = resultChartsData.data;
  115. this.drawChart();
  116. this.isError = false;
  117. this.loading = false;
  118. } catch (error) {
  119. this.isError = true;
  120. this.loading = false;
  121. }
  122. }
  123. },
  124. drawChart() {
  125. // 提取散点数据和线数据
  126. const data =
  127. this.chartData.data &&
  128. this.chartData.data.filter(
  129. (item) => item.enginName !== "合同功率曲线"
  130. )[0]; // 点数据
  131. const lineData =
  132. this.chartData.data &&
  133. this.chartData.data.filter(
  134. (item) => item.enginName === "合同功率曲线"
  135. )[0]; // 线数据
  136. // 提取唯一时间标签,并计算 tickvals 和 ticktext
  137. const uniqueTimeLabels = data.colorbar
  138. ? [...new Set(data.colorbar)]
  139. : [...new Set(data.color)]; // 从 colorbar 中提取唯一的时间标签
  140. // console.log(data.colorbar, "data.colorbar 1");
  141. const tickvals = uniqueTimeLabels.map((label, index) => index + 1); // 根据时间标签生成 tickvals
  142. const ticktext = uniqueTimeLabels.map((dateStr) => {
  143. const date = new Date(dateStr);
  144. return date.toLocaleDateString("en-CA", {
  145. year: "numeric",
  146. month: "2-digit",
  147. }); // 格式化为 'yyyy-MM'
  148. }); // 使用格式化后的时间作为 ticktext
  149. const timeMapping = uniqueTimeLabels.reduce((acc, curr, index) => {
  150. acc[curr] = index + 1;
  151. return acc;
  152. }, {});
  153. // 获取 yData 的最小值和最大值来做比例值的计算
  154. const minValue = Math.min(...tickvals);
  155. const maxValue = Math.max(...tickvals);
  156. // 计算比例值
  157. const colors = ticktext.map((item, ind) => {
  158. // 计算比例值(可以根据需要调整映射的数据范围)
  159. const proportion = (tickvals[ind] - minValue) / (maxValue - minValue);
  160. return [
  161. proportion, // 比例值
  162. this.color1[ind], // 对应的颜色
  163. ];
  164. });
  165. // 将时间字符串映射为数字
  166. let colorValues = [];
  167. // data.colorbar.map((date, index) => timeMapping[date]);
  168. if (data.colorbar) {
  169. colorValues = data.colorbar.map((date, index) => timeMapping[date]);
  170. } else {
  171. colorValues = data.color.map((date, index) => timeMapping[date]);
  172. }
  173. let scatterTrace = {}; // 用于存放散点图的 trace
  174. let lineTrace = {}; // 用于存放折线图的 trace
  175. // 保存原始颜色和大小
  176. this.originalColors = [...data.yData];
  177. this.originalSizes = new Array(data.xData.length).fill(6); // 初始点大小
  178. // 绘制散点图
  179. if (this.chartType === "scatter") {
  180. scatterTrace = {
  181. x: data.xData,
  182. y: data.yData,
  183. mode: "markers", // 散点
  184. type: "scattergl", // 使用散点图
  185. text: data.engineName, // 提示文本
  186. marker: {
  187. color: colorValues, // 使用时间数据来映射颜色
  188. colorscale: this.color1
  189. ? [...colors]
  190. : [
  191. [0, "#F9FDD2"],
  192. [0.15, "#E9F6BD"],
  193. [0.3, "#C2E3B9"],
  194. [0.45, "#8AC8BE"],
  195. [0.6, "#5CA8BF"],
  196. [0.75, "#407DB3"],
  197. [0.9, "#2E4C9A"],
  198. [1, "#1B2973"],
  199. ], // 默认颜色渐变
  200. colorbar: {
  201. title: data.colorbartitle, // 色标标题
  202. tickvals: tickvals, // 设置刻度值
  203. ticktext: ticktext, // 设置刻度文本
  204. tickmode: "array", // 使用数组模式
  205. tickangle: -45, // 可选:调整刻度文本的角度
  206. },
  207. size: new Array(data.xData.length).fill(this.pointSize), // 点的大小
  208. },
  209. hovertemplate:
  210. `${this.chartData.xaixs}:` +
  211. ` %{x} <br> ` +
  212. `${this.chartData.yaixs}:` +
  213. "%{y} <br>" +
  214. `时间: %{customdata}<extra></extra>`, // 在 hover 中显示格式化后的时间
  215. customdata: data.colorbar || data.color, // 将格式化后的时间存入 customdata
  216. };
  217. }
  218. if (lineData) {
  219. // 绘制折线图
  220. lineTrace = {
  221. x: lineData.xData, // 线数据的 xData
  222. y: lineData.yData, // 线数据的 yData
  223. mode: "lines+markers", // 线和点同时显示
  224. type: "scattergl", // 使用 scattergl 类型
  225. text: lineData.engineName, // 提示文本
  226. line: {
  227. color: "red", // 线条颜色
  228. },
  229. };
  230. }
  231. // 图表布局
  232. const layout = {
  233. title: data.title,
  234. xaxis: {
  235. title: this.chartData.xaixs,
  236. gridcolor: "rgb(255,255,255)", // 网格线颜色
  237. tickcolor: "rgb(255,255,255)",
  238. backgroundcolor: "#e5ecf6",
  239. showbackground: true, // 显示背景
  240. },
  241. yaxis: {
  242. title: this.chartData.yaixs,
  243. gridcolor: "rgb(255,255,255)", // 网格线颜色
  244. tickcolor: "rgb(255,255,255)",
  245. backgroundcolor: "#e5ecf6",
  246. showbackground: true, // 显示背景
  247. },
  248. showlegend: false,
  249. plot_bgcolor: "#e5ecf6",
  250. gridcolor: "#fff", // 设置网格线颜色
  251. };
  252. // 配置工具栏按钮
  253. const config = {
  254. modeBarButtonsToAdd: [
  255. {
  256. name: "选择",
  257. icon: Plotly.Icons.pencil,
  258. click: (gd) => this.handleSelectClick(gd),
  259. },
  260. {
  261. name: "清除选中",
  262. icon: Plotly.Icons.undo,
  263. click: (gd) => this.handleClearSelect(gd),
  264. },
  265. {
  266. name: "下载CSV文件",
  267. icon: Plotly.Icons.disk,
  268. click: (gd) => this.handleDownloadCSV(gd),
  269. },
  270. ],
  271. modeBarButtonsToRemove: [
  272. "lasso2d", // 移除不需要的工具按钮
  273. ],
  274. displaylogo: false,
  275. editable: true,
  276. scrollZoom: false,
  277. };
  278. // 合并散点和折线图的数据
  279. const traces = [];
  280. if (scatterTrace) traces.push(scatterTrace); // 如果有散点数据
  281. if (lineTrace) traces.push(lineTrace); // 如果有线图数据
  282. // 使用 Plotly 绘制图表
  283. Plotly.react(
  284. this.$refs[`plotlyChart-${this.index}`], // 这里是对 DOM 元素的引用
  285. traces,
  286. layout,
  287. config
  288. ).then(() => {
  289. // 确保图表加载完成后设置工具栏按钮
  290. const plotElement = this.$refs[`plotlyChart-${this.index}`];
  291. Plotly.relayout(plotElement, layout); // 使用 relayout 来确保自定义按钮应用
  292. });
  293. },
  294. handleSelectClick(gd) {
  295. // 绑定 plotly_click 事件
  296. gd.on("plotly_click", (data) => {
  297. const pointIndex = data.points[0].pointIndex;
  298. const xClick = data.points[0].x;
  299. const yClick = data.points[0].y;
  300. // 将点击的点添加到选中的点数组
  301. this.selectedPoints.push({
  302. x: xClick, // 点击点的 x 坐标
  303. y: yClick, // 点击点的 y 坐标
  304. index: pointIndex, // 点击点的索引
  305. time: data.points[0].text, // 点击点的时间信息
  306. });
  307. // 初始化颜色和大小数组
  308. let newColors = [...this.originalColors];
  309. let newSize = [...this.originalSizes];
  310. // 如果选中的点数大于等于3,进行多边形选择区域的处理
  311. if (this.selectedPoints.length >= 3) {
  312. const xv = this.selectedPoints.map((p) => p.x);
  313. const yv = this.selectedPoints.map((p) => p.y);
  314. // 判断点是否在多边形内
  315. function inPolygon(x, y, xv, yv) {
  316. let inside = false;
  317. for (let i = 0, j = xv.length - 1; i < xv.length; j = i++) {
  318. const intersect =
  319. yv[i] > y !== yv[j] > y &&
  320. x < ((xv[j] - xv[i]) * (y - yv[i])) / (yv[j] - yv[i]) + xv[i];
  321. if (intersect) inside = !inside;
  322. }
  323. return inside;
  324. }
  325. // 用于跟踪已添加的 (x, y) 组合
  326. const addedPoints = {};
  327. // 遍历图表数据中的所有点,检查是否在多边形内
  328. gd.data[0].x.forEach((xVal, i) => {
  329. const yVal = gd.data[0].y[i];
  330. if (inPolygon(xVal, yVal, xv, yv)) {
  331. const pointKey = `${xVal}-${yVal}`;
  332. if (!addedPoints[pointKey]) {
  333. this.selectedPoints.push({
  334. x: gd.data[0].x[i],
  335. y: gd.data[0].y[i],
  336. time: gd.data[0].text[i],
  337. });
  338. newColors[i] = "red"; // 高亮选择的点
  339. newSize[i] = 10; // 设置点的大小
  340. addedPoints[pointKey] = true;
  341. }
  342. }
  343. });
  344. }
  345. // 更新选中点的颜色和大小
  346. this.selectedPoints.forEach((point) => {
  347. newColors[point.index] = "red";
  348. newSize[point.index] = 10;
  349. });
  350. // 使用 Plotly.restyle 更新颜色和大小
  351. Plotly.restyle(gd, {
  352. "marker.color": [newColors],
  353. "marker.size": [newSize],
  354. });
  355. // 处理选中的数据
  356. this.getSelectData(this.selectedPoints, gd.layout);
  357. });
  358. },
  359. handleClearSelect(gd) {
  360. this.selectedPoints = [];
  361. Plotly.restyle(gd, {
  362. "marker.color": [this.originalColors],
  363. "marker.size": [this.originalSizes],
  364. });
  365. },
  366. getSelectData(selectedPoints, layout) {
  367. // 在这里处理选中的数据,您可以将其展示或导出等
  368. console.log("选中的点数据:", selectedPoints);
  369. console.log("布局信息:", layout);
  370. },
  371. handleDownloadCSV(gd) {
  372. if (this.selectedPoints.length === 0) {
  373. alert("没有选中的数据");
  374. return;
  375. }
  376. this.downloadCSV();
  377. },
  378. downloadCSV() {
  379. const headers = [this.chartData.xaixs, this.chartData.yaixs];
  380. const csvRows = [headers]; // 保存标头
  381. // 使用 Set 或 Map 去重
  382. const uniquePoints = [];
  383. this.selectedPoints.forEach((point) => {
  384. if (!uniquePoints.some((p) => p.x === point.x && p.y === point.y)) {
  385. uniquePoints.push(point);
  386. }
  387. });
  388. // 将去重后的点加入 CSV 数据
  389. uniquePoints.forEach((point) => {
  390. csvRows.push(`${point.x},${point.y}`);
  391. });
  392. const csvString = csvRows.join("\n");
  393. const blob = new Blob([csvString], { type: "text/csv; charset=utf-8" });
  394. const url = URL.createObjectURL(blob);
  395. const a = document.createElement("a");
  396. a.href = url;
  397. a.download = "selected_data.csv";
  398. a.click();
  399. URL.revokeObjectURL(url);
  400. },
  401. updateChartColor(color) {
  402. // 更新图表颜色
  403. // this.color1 = color;
  404. this.drawChart();
  405. },
  406. },
  407. };
  408. </script>
  409. <style scoped>
  410. /* 自定义样式 */
  411. </style>