3DDrawingChart.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. <template>
  2. <div>
  3. <!-- 配色方案选择和图表类型切换 -->
  4. <div style="display: flex; align-items: center; padding-top: 20px">
  5. <div style="margin-right: 20px; display: flex; align-items: center">
  6. <el-select
  7. size="small"
  8. v-model="color1"
  9. @change="updateChartColor"
  10. placeholder="选择配色方案"
  11. style="width: 200px"
  12. >
  13. <el-option
  14. v-for="(scheme, index) in colorSchemes"
  15. :key="index"
  16. :label="scheme.label"
  17. :value="scheme.colors"
  18. >
  19. <span
  20. v-for="color in scheme.colors.slice(0, 8)"
  21. :style="{
  22. background: color,
  23. width: '20px',
  24. height: '20px',
  25. display: 'inline-block',
  26. }"
  27. ></span>
  28. </el-option>
  29. </el-select>
  30. </div>
  31. <!-- 点大小控制 -->
  32. <div style="display: flex; align-items: center">
  33. <el-slider
  34. v-model="pointSize"
  35. :min="1"
  36. :max="15"
  37. :step="1"
  38. label="点的大小"
  39. show-stops
  40. style="width: 150px"
  41. @change="updateChartColor"
  42. ></el-slider>
  43. </div>
  44. </div>
  45. <!-- 图表展示区域 -->
  46. <div style="height: 600px">
  47. <div
  48. v-loading="loading"
  49. :id="`plotly-3d-chart-` + index"
  50. ref="plotlyChart"
  51. style="height: 600px; background-color: #e5ecf6"
  52. >
  53. <el-empty v-if="isError" description="请求失败"></el-empty>
  54. </div>
  55. </div>
  56. </div>
  57. </template>
  58. <script>
  59. import Plotly from "plotly.js-dist";
  60. import axios from "axios";
  61. import { myMixin } from "@/mixins/chartRequestMixin";
  62. import { colorSchemes } from "@/views/overview/js/colors";
  63. import { mapState } from "vuex";
  64. export default {
  65. props: {
  66. fileAddr: {
  67. default: "",
  68. type: String,
  69. },
  70. index: {
  71. default: "",
  72. type: String,
  73. },
  74. setUpImgData: {
  75. default: () => [],
  76. type: Array,
  77. },
  78. },
  79. data() {
  80. return {
  81. color1: [], // 默认颜色
  82. // 配色方案列表(每个方案是一个颜色数组)
  83. colorSchemes: colorSchemes,
  84. chartData: {},
  85. chartType: "scatter", // 当前图表类型(默认是散点图)
  86. pointSize: 1, // 默认点大小
  87. };
  88. },
  89. mixins: [myMixin],
  90. computed: {
  91. ...mapState("themes", {
  92. themeColor: "themeColor",
  93. }),
  94. },
  95. watch: {
  96. themeColor: {
  97. handler(newval) {
  98. if (newval.length === 0) {
  99. this.color1 = this.colorSchemes[0].colors;
  100. } else {
  101. this.color1 = newval;
  102. }
  103. this.updateChartColor();
  104. },
  105. deep: true,
  106. },
  107. setUpImgData: {
  108. handler(newType) {
  109. this.renderChart();
  110. },
  111. deep: true,
  112. },
  113. },
  114. async mounted() {
  115. this.$nextTick(() => {
  116. this.color1 = this.colorSchemes[0].colors;
  117. this.getData();
  118. });
  119. },
  120. methods: {
  121. async getData() {
  122. if (this.fileAddr !== "") {
  123. try {
  124. this.loading = true;
  125. this.cancelToken = axios.CancelToken.source();
  126. const resultChartsData = await axios.get(this.fileAddr, {
  127. cancelToken: this.cancelToken.token,
  128. });
  129. if (typeof resultChartsData.data === "string") {
  130. let dataString = resultChartsData.data;
  131. dataString = dataString.trim(); // 去除前后空格
  132. dataString = dataString.replace(/Infinity/g, '"Infinity"'); // 替换 Infinity 为 "Infinity"
  133. try {
  134. const parsedData = JSON.parse(dataString);
  135. this.chartData = parsedData;
  136. } catch (error) {
  137. console.error("JSON 解析失败:", error);
  138. }
  139. } else {
  140. this.chartData = resultChartsData.data;
  141. }
  142. this.renderChart();
  143. this.isError = false;
  144. this.loading = false;
  145. } catch (error) {
  146. this.isError = true;
  147. this.loading = false;
  148. }
  149. }
  150. },
  151. // 更新配色方案
  152. updateChartColor() {
  153. this.renderChart(); // 当配色方案或点大小发生变化时重新渲染图表
  154. },
  155. // 切换图表类型
  156. setChartType(type) {
  157. this.chartType = type;
  158. this.renderChart(); // 切换图表类型时重新渲染图表
  159. },
  160. // 获取配色选项样式
  161. getOptionStyle(scheme) {
  162. return {
  163. background: `linear-gradient(to right, ${scheme
  164. .slice(0, 8)
  165. .join(", ")})`,
  166. color: "#fff",
  167. height: "30px",
  168. lineHeight: "30px",
  169. borderRadius: "0px",
  170. };
  171. },
  172. renderChart() {
  173. const uniqueColors = [...new Set(this.chartData.data[0].color)];
  174. if (!this.color1) {
  175. this.color1 = this.colorSchemes[0].colors;
  176. }
  177. const traces = uniqueColors.map((color, idx) => {
  178. const colorData = this.chartData.data[0].color.map((c) =>
  179. c === color ? 1 : 0
  180. );
  181. const trace = {
  182. x: this.chartData.data[0].xData.filter((_, i) => colorData[i] === 1),
  183. y: this.chartData.data[0].yData.filter((_, i) => colorData[i] === 1),
  184. z: this.chartData.data[0].zData.filter((_, i) => colorData[i] === 1),
  185. mode: this.chartType === "scatter" ? "markers" : "lines", // 根据选择的图表类型来设置模式
  186. type: "scatter3d",
  187. marker: {
  188. size: this.pointSize, // 使用动态点大小
  189. color: this.color1[idx], // 使用配色方案
  190. colorscale: "YlGnBu",
  191. },
  192. name: ` ${color}`,
  193. legendgroup: `group-${idx}`,
  194. hovertemplate:
  195. `${this.chartData.xaixs}:` +
  196. ` %{x} <br> ` +
  197. `${this.chartData.yaixs}:` +
  198. "%{y} <br>" +
  199. `${this.chartData.zaixs}:` +
  200. "%{z} <extra></extra>",
  201. };
  202. return trace;
  203. });
  204. const yData = [...new Set(this.chartData.data[0].yData)]; // 获取唯一的yData
  205. const totalTicks = 10; // 想要显示的刻度数量
  206. // 保证第一个和最后一个刻度
  207. const firstValue = yData[0];
  208. const lastValue = yData[yData.length - 1];
  209. // 计算中间部分的刻度值
  210. const interval = Math.floor((yData.length - 1) / 8); // 总长度减去最初和最后一个,计算间隔
  211. // 选择需要展示的刻度
  212. const tickvals = [firstValue]; // 先将第一个值放入刻度数组
  213. for (let i = 1; i < totalTicks - 1; i++) {
  214. const index = i * interval;
  215. tickvals.push(yData[index]);
  216. }
  217. tickvals.push(lastValue); // 最后将最后一个值放入刻度数组
  218. const ticktext = tickvals;
  219. const layout = {
  220. title: {
  221. text: this.chartData.data[0].title,
  222. font: {
  223. size: 16, // 设置标题字体大小(默认 16)
  224. weight: "bold",
  225. },
  226. },
  227. scene: {
  228. xaxis: {
  229. title: this.chartData.xaixs,
  230. gridcolor: "rgb(255,255,255)",
  231. tickcolor: "rgb(255,255,255)",
  232. backgroundcolor: "#e0e7f1",
  233. showbackground: true,
  234. // linewidth: 2, // 轴线宽度
  235. linecolor: "black", // 轴线颜色
  236. ticks: "outside", // 设置刻度线在轴线外
  237. fixedrange: true, // 防止缩放
  238. // tickwidth: 2,
  239. tickcolor: "black",
  240. tickangle: -10,
  241. // range:
  242. // this.chartData.xaixs === "发电机转速(r/min)" ||
  243. // this.chartData.xaixs === "发电机转速(r/min)"
  244. // ? [1000, 2000]
  245. // : undefined,
  246. // range: this.chartData.xaixs === "桨距角(°)" ? [-1, 20] : undefined,
  247. },
  248. yaxis: {
  249. title: this.chartData.yaixs,
  250. type: "category", // 让 Y 轴按类别均匀分布
  251. categoryorder: "array", // 自定义顺序,确保间隔均匀
  252. categoryarray: [...new Set(this.chartData.data[0].yData)], // 以原始数据顺序排序
  253. tickvals: tickvals,
  254. ticktext: ticktext,
  255. gridcolor: "rgb(255,255,255)",
  256. tickcolor: "rgb(255,255,255)",
  257. backgroundcolor: "#e0e7f1",
  258. showbackground: true,
  259. // linewidth: 2, // 轴线宽度
  260. linecolor: "black", // 轴线颜色
  261. ticks: "outside", // 设置刻度线在轴线外
  262. // tickwidth: 2,
  263. tickcolor: "black",
  264. tickangle: 25,
  265. },
  266. zaxis: {
  267. title: this.chartData.zaixs,
  268. gridcolor: "rgb(255,255,255)",
  269. tickcolor: "rgb(255,255,255)",
  270. backgroundcolor: "#e0e7f1",
  271. showbackground: true,
  272. fixedrange: true, // 防止缩放
  273. // linewidth: 2, // 轴线宽度
  274. linecolor: "black", // 轴线颜色
  275. ticks: "outside", // 设置刻度线在轴线外
  276. // tickwidth: 2,
  277. tickcolor: "black",
  278. tickangle: -90,
  279. },
  280. aspectratio: {
  281. x: 2.2,
  282. y: 1.7,
  283. z: 1,
  284. },
  285. plot_bgcolor: "#e5ecf6",
  286. gridcolor: "#fff",
  287. bgcolor: "#e5ecf6", // 设置背景颜色
  288. camera: {
  289. up: {
  290. x: 0.200292643688136,
  291. y: 0.2488259353493132,
  292. z: 0.947612004346693,
  293. },
  294. center: {
  295. x: -0.052807476121180814,
  296. y: 0.02451796399554085,
  297. z: -0.022911006648570736,
  298. },
  299. eye: {
  300. x: -2.126379643342493,
  301. y: -2.551422475965373,
  302. z: 1.0917667684145647,
  303. },
  304. projection: {
  305. type: "orthographic",
  306. },
  307. },
  308. },
  309. margin: { t: 50, b: 50, l: 50, r: 50 },
  310. staticPlot: false,
  311. showlegend: true,
  312. legend: {
  313. itemsizing: "constant", // ✅ 统一图例 marker 大小
  314. font: {
  315. size: 12,
  316. },
  317. marker: {
  318. size: 10, // 图例中点的大小
  319. },
  320. },
  321. };
  322. const config = {
  323. modeBarButtonsToAdd: [
  324. {
  325. name: "还原", // 自定义按钮
  326. icon: Plotly.Icons.home,
  327. click: () => this.resetCamera(),
  328. },
  329. ],
  330. responsive: true,
  331. modeBarButtonsToRemove: [
  332. "sendDataToCloud",
  333. // "autoScale2d",
  334. // "hoverClosest3d",
  335. "resetCameraLastSave3d",
  336. "resetCameraDefault3d",
  337. "resetCameraLastSave",
  338. "sendDataToCloud",
  339. // "pan2d", // 平移按钮
  340. "zoom2d", // 缩放按钮
  341. // "zoom",
  342. "zoom3d",
  343. // "select2d", // 选择框
  344. // "lasso2d", // 套索选择
  345. // "resetScale2d", // 重置轴
  346. // // "zoomIn", // 放大
  347. // // "zoomOut", // 缩小
  348. // "home", // 重置
  349. // "toImage", // 导出为图片
  350. // "hoverClosestCartesian", // 悬浮信息
  351. // "zoomIn2d", // 缩放按钮(详细版本)
  352. // "zoomOut2d", // 缩放按钮(详细版本)
  353. // "autoScale2D",
  354. "plotlylogo2D",
  355. "plotlylogo3D",
  356. // "Produced with Plotly.js(v2.35.2)", // 删除 Plotly logo
  357. ],
  358. displaylogo: false, // 可选:隐藏 Plotly logo
  359. };
  360. // 获取x轴和y轴的设置
  361. const getChartSetUp = (axisTitle) => {
  362. return this.setUpImgData.find((item) => item.text.includes(axisTitle));
  363. };
  364. // 更新x轴和y轴的范围与步长
  365. const xChartSetUp = getChartSetUp(layout.scene.xaxis.title);
  366. if (xChartSetUp) {
  367. layout.scene.xaxis.dtick = xChartSetUp.dtick;
  368. layout.scene.xaxis.range = [xChartSetUp.min, xChartSetUp.max];
  369. }
  370. const yChartSetUp = getChartSetUp(layout.scene.yaxis.title);
  371. if (yChartSetUp) {
  372. layout.scene.yaxis.dtick = yChartSetUp.dtick;
  373. layout.scene.yaxis.range = [yChartSetUp.min, yChartSetUp.max];
  374. }
  375. const zChartSetUp = getChartSetUp(layout.scene.zaxis.title);
  376. if (zChartSetUp) {
  377. layout.scene.zaxis.dtick = zChartSetUp.dtick;
  378. layout.scene.zaxis.range = [zChartSetUp.min, zChartSetUp.max];
  379. }
  380. try {
  381. // 假设这里是 WebGL 的相关初始化代码
  382. Plotly.react(
  383. `plotly-3d-chart-` + this.index,
  384. traces,
  385. {
  386. ...layout,
  387. displaylogo: false,
  388. },
  389. {
  390. ...config,
  391. }
  392. )
  393. .then(function (gd) {
  394. // 获取工具栏按钮
  395. const toolbar = gd.querySelector(".modebar");
  396. const buttons = toolbar.querySelectorAll(".modebar-btn");
  397. // 定义一个映射对象,方便修改按钮提示
  398. const titleMap = {
  399. "Download plot as a png": "保存图片",
  400. Autoscale: "缩放",
  401. Pan: "平移",
  402. "Zoom out": "放大",
  403. "Zoom in": "缩小",
  404. "Box Select": "选择框操作",
  405. "Lasso Select": "套索选择操作",
  406. "Reset axes": "重置操作",
  407. "Reset camera to default": "重置相机视角",
  408. "Turntable rotation": "转台式旋转",
  409. "Orbital rotation": "轨道式旋转",
  410. };
  411. // 遍历所有按钮,修改它们的 title
  412. buttons.forEach(function (button) {
  413. const dataTitle = button.getAttribute("data-title");
  414. // 如果标题匹配,修改属性值
  415. if (titleMap[dataTitle]) {
  416. button.setAttribute("data-title", titleMap[dataTitle]);
  417. }
  418. });
  419. })
  420. .catch((err) => {
  421. console.error("WebGL 错误: ", err);
  422. // 你可以根据错误类型做更多处理
  423. if (err.message.includes("shaderSource")) {
  424. // alert("着色器编译失败!");
  425. }
  426. });
  427. // 监听图表的 relayout 事件,获取并输出相机视角
  428. const plotElement = document.getElementById(
  429. `plotly-3d-chart-` + this.index
  430. );
  431. plotElement.on("plotly_relayout", function (eventData) {
  432. // 在每次布局变更时,打印当前相机视角
  433. if (eventData["scene.camera"]) {
  434. console.log(
  435. "当前相机视角:",
  436. eventData["scene.camera"],
  437. eventData["scene.aspectratio"]
  438. );
  439. }
  440. });
  441. } catch (e) {
  442. console.error("捕获到 WebGL 错误:", e);
  443. // alert("图表渲染失败!");
  444. }
  445. // Plotly.newPlot(`plotly-3d-chart-` + this.index, traces, layout);
  446. },
  447. resetCamera() {
  448. Plotly.relayout(`plotly-3d-chart-` + this.index, {
  449. "scene.camera": {
  450. up: {
  451. x: 0.200292643688136,
  452. y: 0.2488259353493132,
  453. z: 0.947612004346693,
  454. },
  455. center: {
  456. x: -0.052807476121180814,
  457. y: 0.02451796399554085,
  458. z: -0.022911006648570736,
  459. },
  460. eye: {
  461. x: -2.126379643342493,
  462. y: -2.551422475965373,
  463. z: 1.0917667684145647,
  464. },
  465. projection: {
  466. type: "orthographic",
  467. },
  468. },
  469. "scene.aspectratio": {
  470. x: 2.2,
  471. y: 1.7,
  472. z: 1,
  473. },
  474. });
  475. },
  476. },
  477. };
  478. </script>
  479. <style scoped>
  480. /* #scene {
  481. background: #e5ecf6 !important;
  482. }
  483. .js-plotly-plot .plotly,
  484. .js-plotly-plot .plotly div {
  485. background: #e5ecf6 !important;
  486. } */
  487. /* 样式可以根据需求自定义 */
  488. #plotly-3d-chart {
  489. width: 100%;
  490. height: 600px;
  491. }
  492. ::v-deep canvas {
  493. /* height: 400px !important; */
  494. }
  495. </style>