Time3DChart.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  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. <div
  46. v-loading="loading"
  47. :id="`plotly-3d-chart-` + index"
  48. ref="plotlyChart"
  49. style="height: 600px; background-color: #e5ecf6"
  50. ></div>
  51. </div>
  52. </template>
  53. <script>
  54. import Plotly, { doCamera } from "plotly.js-dist";
  55. import axios from "axios";
  56. import { myMixin } from "@/mixins/chartRequestMixin";
  57. import { colorSchemes } from "@/views/overview/js/colors";
  58. import { mapState } from "vuex";
  59. export default {
  60. props: {
  61. fileAddr: {
  62. default: "",
  63. type: String,
  64. },
  65. index: {
  66. default: "",
  67. type: String,
  68. },
  69. setUpImgData: {
  70. default: () => [],
  71. type: Array,
  72. },
  73. },
  74. data() {
  75. return {
  76. color1: [], // 默认颜色
  77. chartData: {},
  78. chartType: "scatter", // 当前图表类型(默认是散点图)
  79. colorSchemes: [...colorSchemes],
  80. pointSize: 1, // 默认点大小
  81. };
  82. },
  83. mixins: [myMixin],
  84. computed: {
  85. ...mapState("themes", {
  86. themeColor: "themeColor",
  87. }),
  88. },
  89. watch: {
  90. themeColor: {
  91. handler(newval) {
  92. if (newval.length === 0) {
  93. this.color1 = this.colorSchemes[0].colors;
  94. } else {
  95. this.color1 = newval;
  96. }
  97. this.updateChartColor();
  98. },
  99. deep: true,
  100. },
  101. setUpImgData: {
  102. handler(newType) {
  103. this.renderChart();
  104. },
  105. deep: true,
  106. },
  107. },
  108. async mounted() {
  109. this.$nextTick(() => {
  110. this.getData();
  111. // if (this.themeColor.length === 0) {
  112. this.color1 = this.colorSchemes[0].colors;
  113. // } else {
  114. // this.color1 = this.themeColor;
  115. // }
  116. });
  117. },
  118. methods: {
  119. async getData() {
  120. if (this.fileAddr !== "") {
  121. try {
  122. this.loading = true;
  123. this.cancelToken = axios.CancelToken.source();
  124. const resultChartsData = await axios.get(this.fileAddr, {
  125. cancelToken: this.cancelToken.token,
  126. });
  127. if (typeof resultChartsData.data === "string") {
  128. let dataString = resultChartsData.data;
  129. dataString = dataString.trim(); // 去除前后空格
  130. dataString = dataString.replace(/Infinity/g, '"Infinity"'); // 处理无效字符
  131. try {
  132. const parsedData = JSON.parse(dataString);
  133. this.chartData = parsedData;
  134. } catch (error) {
  135. console.error("JSON 解析失败:", error);
  136. }
  137. } else {
  138. this.chartData = resultChartsData.data;
  139. }
  140. this.renderChart();
  141. this.isError = false;
  142. this.loading = false;
  143. } catch (error) {
  144. this.isError = true;
  145. this.loading = false;
  146. }
  147. }
  148. },
  149. // 格式化日期为 YY-MM 格式
  150. formatDate(dateString) {
  151. const date = new Date(dateString);
  152. const year = date.getFullYear(); // 获取年份后两位
  153. const month = ("0" + (date.getMonth() + 1)).slice(-2); // 获取月份并确保两位数
  154. return `${year}-${month}`;
  155. },
  156. renderChart() {
  157. // 提取 Y 轴数据中的月份,并去重
  158. const uniqueMonths = Array.from(
  159. new Set(
  160. this.chartData.data[0].yData.map((date) => this.formatDate(date))
  161. )
  162. );
  163. if (!this.color1) {
  164. this.color1 = colorSchemes[0].colors;
  165. }
  166. // 设置每个月份对应的颜色
  167. const monthColors = this.color1;
  168. // 为每个月份生成独立的 trace,每个 trace 对应一个月份
  169. const traces = uniqueMonths.map((month, monthIndex) => {
  170. const monthData = this.chartData.data[0].yData
  171. .map((date, index) => (this.formatDate(date) === month ? index : -1))
  172. .filter((index) => index !== -1);
  173. const trace = {
  174. x: monthData.map((index) => this.chartData.data[0].xData[index]), // 发电机转速
  175. y: monthData.map((index) => this.chartData.data[0].yData[index]), // 时间
  176. z: monthData.map((index) => this.chartData.data[0].zData[index]), // 有功功率
  177. mode: "markers",
  178. type: "scatter3d", // 3D 散点图
  179. marker: {
  180. size: this.pointSize,
  181. color: monthColors[monthIndex],
  182. opacity: 0.8,
  183. lighting: {
  184. ambient: 0.3, // 环境光(影响整体亮度)
  185. diffuse: 1, // 漫反射光(增加真实感)
  186. specular: 0.5, // 镜面反射(增强高光)
  187. roughness: 0.5, // 低值=光滑表面,高值=粗糙表面
  188. fresnel: 0.2, // 边缘高光强度
  189. },
  190. },
  191. name: month, // 图例项名称,格式为 YY-MM
  192. legendgroup: `month-${monthIndex}`, // 图例分组
  193. hovertemplate:
  194. `${this.chartData.xaixs}:` +
  195. ` %{x} <br> ` +
  196. `${this.chartData.yaixs}:` +
  197. "%{y} <br>" +
  198. `${this.chartData.zaixs}:` +
  199. "%{z} <br><extra></extra>",
  200. };
  201. return trace;
  202. });
  203. const layout = {
  204. title: {
  205. text: this.chartData.data[0].title,
  206. font: {
  207. size: 16,
  208. weight: "bold",
  209. },
  210. },
  211. scene: {
  212. xaxis: {
  213. title: {
  214. text: this.chartData.xaixs,
  215. standoff: 100,
  216. },
  217. gridcolor: "#fff",
  218. backgroundcolor: "#e0e7f1",
  219. showbackground: true,
  220. linecolor: "black",
  221. ticks: "outside",
  222. ticklen: 10,
  223. tickcolor: "black",
  224. zeroline: false,
  225. tickangle: -10,
  226. // nticks: 5,
  227. dtick: this.chartData.xaixs === "风速(m/s)" ? 1 : undefined,
  228. // range:
  229. // this.chartData.xaixs === "发电机转速(r/min)" ||
  230. // this.chartData.xaixs === "发电机转速(r/min)"
  231. // ? [1000, 2000]
  232. // : undefined,
  233. // range: this.chartData.xaixs === "桨距角(°)" ? [-1, 20] : undefined,
  234. },
  235. // 对 Y 轴不显示默认标题,只保留 tick 标签,并适当加大 standoff 以防止标签挤在一起
  236. yaxis: {
  237. title: {
  238. text: this.chartData.yaixs, // 隐藏默认标题
  239. },
  240. type: "category", // 让 Y 轴按类别均匀分布
  241. categoryorder: "category ascending", // 按类别字母顺序排列
  242. type: "date",
  243. tickformat: "%Y-%m",
  244. // dtick: "M3",//显式设置每3个月一个刻度
  245. gridcolor: "#fff",
  246. tickcolor: "#e5ecf6",
  247. backgroundcolor: "#e0e7f1",
  248. showbackground: true,
  249. linecolor: "black",
  250. ticks: "outside",
  251. tickcolor: "black",
  252. zeroline: false,
  253. tickangle: 25,
  254. nticks: 3,
  255. },
  256. zaxis: {
  257. title: {
  258. text: this.chartData.zaixs,
  259. },
  260. gridcolor: "#fff",
  261. tickcolor: "#fff",
  262. backgroundcolor: "#e0e7f1",
  263. showbackground: true,
  264. linecolor: "black",
  265. ticks: "outside",
  266. tickcolor: "black",
  267. zeroline: false,
  268. tickangle: -90,
  269. nticks: 3,
  270. },
  271. bgcolor: "#e5ecf6",
  272. aspectratio: {
  273. x: 2.2,
  274. y: 1.7,
  275. z: 1,
  276. },
  277. aspectmode: "manual",
  278. gridcolor: "#fff",
  279. camera: {
  280. up: {
  281. x: 0.200292643688136,
  282. y: 0.2488259353493132,
  283. z: 0.947612004346693,
  284. },
  285. center: {
  286. x: -0.052807476121180814,
  287. y: 0.02451796399554085,
  288. z: -0.022911006648570736,
  289. },
  290. eye: {
  291. x: -2.126379643342493,
  292. y: -2.551422475965373,
  293. z: 1.0917667684145647,
  294. },
  295. projection: {
  296. type: "orthographic",
  297. },
  298. },
  299. },
  300. margin: { t: 50, b: 50, l: 50, r: 50 },
  301. staticPlot: false,
  302. showlegend: true,
  303. legend: {
  304. itemsizing: "constant", // ✅ 统一图例 marker 大小
  305. font: {
  306. size: 12,
  307. },
  308. marker: {
  309. size: 10,
  310. },
  311. },
  312. };
  313. const config = {
  314. modeBarButtonsToAdd: [
  315. {
  316. name: "还原", // 自定义按钮
  317. icon: Plotly.Icons.home,
  318. click: () => this.resetCamera(),
  319. },
  320. ],
  321. modeBarButtonsToRemove: [
  322. "sendDataToCloud",
  323. "resetCameraLastSave3d",
  324. "resetCameraDefault3d",
  325. "resetCameraLastSave",
  326. "sendDataToCloud",
  327. "zoom2d", // 缩放按钮
  328. "zoom3d",
  329. "plotlylogo2D",
  330. "plotlylogo3D",
  331. ],
  332. responsive: true,
  333. displaylogo: false, // 可选:隐藏 Plotly logo
  334. };
  335. // 获取x轴和y轴的设置
  336. const getChartSetUp = (axisTitle) => {
  337. return this.setUpImgData.find((item) => item.text.includes(axisTitle));
  338. };
  339. // 更新x轴和y轴的范围与步长
  340. const xChartSetUp = getChartSetUp(layout.scene.xaxis.title);
  341. if (xChartSetUp) {
  342. layout.scene.xaxis.dtick = xChartSetUp.dtick;
  343. layout.scene.xaxis.range = [xChartSetUp.min, xChartSetUp.max];
  344. }
  345. const yChartSetUp = getChartSetUp(layout.scene.yaxis.title);
  346. if (yChartSetUp) {
  347. layout.scene.yaxis.dtick = yChartSetUp.dtick;
  348. layout.scene.yaxis.range = [yChartSetUp.min, yChartSetUp.max];
  349. }
  350. const zChartSetUp = getChartSetUp(layout.scene.zaxis.title);
  351. if (zChartSetUp) {
  352. layout.scene.zaxis.dtick = zChartSetUp.dtick;
  353. layout.scene.zaxis.range = [zChartSetUp.min, zChartSetUp.max];
  354. }
  355. Plotly.newPlot(
  356. `plotly-3d-chart-` + this.index,
  357. traces,
  358. layout,
  359. config
  360. ).then(function (gd) {
  361. // 获取工具栏按钮
  362. const toolbar = gd.querySelector(".modebar");
  363. const buttons = toolbar.querySelectorAll(".modebar-btn");
  364. // 定义一个映射对象,方便修改按钮提示
  365. const titleMap = {
  366. "Download plot as a png": "保存图片",
  367. Autoscale: "缩放",
  368. Pan: "平移",
  369. "Zoom out": "放大",
  370. "Zoom in": "缩小",
  371. "Box Select": "选择框操作",
  372. "Lasso Select": "套索选择操作",
  373. "Reset axes": "重置操作",
  374. "Reset camera to default": "重置相机视角",
  375. "Turntable rotation": "转台式旋转",
  376. "Orbital rotation": "轨道式旋转",
  377. };
  378. // 遍历所有按钮,修改它们的 title
  379. buttons.forEach(function (button) {
  380. const dataTitle = button.getAttribute("data-title");
  381. // 如果标题匹配,修改属性值
  382. if (titleMap[dataTitle]) {
  383. button.setAttribute("data-title", titleMap[dataTitle]);
  384. }
  385. });
  386. });
  387. // 监听图表的 relayout 事件,获取并输出相机视角
  388. const plotElement = document.getElementById(
  389. `plotly-3d-chart-` + this.index
  390. );
  391. plotElement.on("plotly_relayout", function (eventData) {
  392. // 在每次布局变更时,打印当前相机视角
  393. if (eventData["scene.camera"]) {
  394. console.log(
  395. "当前相机视角:",
  396. eventData["scene.camera"],
  397. eventData["scene.aspectratio"]
  398. );
  399. }
  400. });
  401. },
  402. // 还原视角
  403. resetCamera() {
  404. Plotly.relayout(`plotly-3d-chart-` + this.index, {
  405. "scene.camera": {
  406. up: {
  407. x: 0.200292643688136,
  408. y: 0.2488259353493132,
  409. z: 0.947612004346693,
  410. },
  411. center: {
  412. x: -0.052807476121180814,
  413. y: 0.02451796399554085,
  414. z: -0.022911006648570736,
  415. },
  416. eye: {
  417. x: -2.126379643342493,
  418. y: -2.551422475965373,
  419. z: 1.0917667684145647,
  420. },
  421. projection: {
  422. type: "orthographic",
  423. },
  424. },
  425. "scene.aspectratio": {
  426. x: 2.2,
  427. y: 1.7,
  428. z: 1,
  429. },
  430. });
  431. },
  432. updateChartColor() {
  433. this.renderChart(); // 当配色方案或点大小发生变化时重新渲染图表
  434. },
  435. // 获取配色选项样式
  436. getOptionStyle(scheme) {
  437. return {
  438. background: `linear-gradient(to right, ${scheme
  439. .slice(0, 8)
  440. .join(", ")})`,
  441. color: "#fff",
  442. height: "30px",
  443. lineHeight: "30px",
  444. borderRadius: "0px",
  445. };
  446. },
  447. },
  448. };
  449. </script>
  450. <style scoped>
  451. /* 样式可以根据需求自定义 */
  452. #plotly-3d-chart {
  453. width: 100%;
  454. height: 600px;
  455. }
  456. ::v-deep canvas {
  457. /* height: 400px !important; */
  458. }
  459. </style>