3DDrawingChart.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  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. },
  242. yaxis: {
  243. title: this.chartData.yaixs,
  244. type: "category", // 让 Y 轴按类别均匀分布
  245. categoryorder: "array", // 自定义顺序,确保间隔均匀
  246. categoryarray: [...new Set(this.chartData.data[0].yData)], // 以原始数据顺序排序
  247. tickvals: tickvals,
  248. ticktext: ticktext,
  249. gridcolor: "rgb(255,255,255)",
  250. tickcolor: "rgb(255,255,255)",
  251. backgroundcolor: "#e0e7f1",
  252. showbackground: true,
  253. // linewidth: 2, // 轴线宽度
  254. linecolor: "black", // 轴线颜色
  255. ticks: "outside", // 设置刻度线在轴线外
  256. // tickwidth: 2,
  257. tickcolor: "black",
  258. tickangle: 25,
  259. },
  260. zaxis: {
  261. title: this.chartData.zaixs,
  262. gridcolor: "rgb(255,255,255)",
  263. tickcolor: "rgb(255,255,255)",
  264. backgroundcolor: "#e0e7f1",
  265. showbackground: true,
  266. fixedrange: true, // 防止缩放
  267. // linewidth: 2, // 轴线宽度
  268. linecolor: "black", // 轴线颜色
  269. ticks: "outside", // 设置刻度线在轴线外
  270. // tickwidth: 2,
  271. tickcolor: "black",
  272. tickangle: -90,
  273. },
  274. aspectratio: {
  275. x: 2.2,
  276. y: 1.7,
  277. z: 1,
  278. },
  279. plot_bgcolor: "#e5ecf6",
  280. gridcolor: "#fff",
  281. bgcolor: "#e5ecf6", // 设置背景颜色
  282. camera: {
  283. up: {
  284. x: 0.200292643688136,
  285. y: 0.2488259353493132,
  286. z: 0.947612004346693,
  287. },
  288. center: {
  289. x: -0.052807476121180814,
  290. y: 0.02451796399554085,
  291. z: -0.022911006648570736,
  292. },
  293. eye: {
  294. x: -2.126379643342493,
  295. y: -2.551422475965373,
  296. z: 1.0917667684145647,
  297. },
  298. projection: {
  299. type: "orthographic",
  300. },
  301. },
  302. },
  303. margin: { t: 50, b: 50, l: 50, r: 50 },
  304. staticPlot: false,
  305. showlegend: true,
  306. legend: {
  307. itemsizing: "constant", // ✅ 统一图例 marker 大小
  308. font: {
  309. size: 12,
  310. },
  311. marker: {
  312. size: 10, // 图例中点的大小
  313. },
  314. },
  315. };
  316. const config = {
  317. modeBarButtonsToAdd: [
  318. {
  319. name: "还原", // 自定义按钮
  320. icon: Plotly.Icons.home,
  321. click: () => this.resetCamera(),
  322. },
  323. ],
  324. modeBarButtonsToRemove: [
  325. "sendDataToCloud",
  326. "autoScale2d",
  327. "hoverClosest3d",
  328. "resetCameraLastSave3d",
  329. "resetCameraDefault3d",
  330. ],
  331. displaylogo: false, // 可选:隐藏 Plotly logo
  332. };
  333. // 获取x轴和y轴的设置
  334. const getChartSetUp = (axisTitle) => {
  335. return this.setUpImgData.find((item) => item.text.includes(axisTitle));
  336. };
  337. // 更新x轴和y轴的范围与步长
  338. const xChartSetUp = getChartSetUp(layout.scene.xaxis.title);
  339. if (xChartSetUp) {
  340. layout.scene.xaxis.dtick = xChartSetUp.dtick;
  341. layout.scene.xaxis.range = [xChartSetUp.min, xChartSetUp.max];
  342. }
  343. const yChartSetUp = getChartSetUp(layout.scene.yaxis.title);
  344. if (yChartSetUp) {
  345. layout.scene.yaxis.dtick = yChartSetUp.dtick;
  346. layout.scene.yaxis.range = [yChartSetUp.min, yChartSetUp.max];
  347. }
  348. const zChartSetUp = getChartSetUp(layout.scene.zaxis.title);
  349. if (zChartSetUp) {
  350. layout.scene.zaxis.dtick = zChartSetUp.dtick;
  351. layout.scene.zaxis.range = [zChartSetUp.min, zChartSetUp.max];
  352. }
  353. try {
  354. // 假设这里是 WebGL 的相关初始化代码
  355. Plotly.react(`plotly-3d-chart-` + this.index, traces, layout).catch(
  356. (err) => {
  357. console.error("WebGL 错误: ", err);
  358. // 你可以根据错误类型做更多处理
  359. if (err.message.includes("shaderSource")) {
  360. // alert("着色器编译失败!");
  361. }
  362. }
  363. );
  364. // 监听图表的 relayout 事件,获取并输出相机视角
  365. const plotElement = document.getElementById(
  366. `plotly-3d-chart-` + this.index
  367. );
  368. plotElement.on("plotly_relayout", function (eventData) {
  369. // 在每次布局变更时,打印当前相机视角
  370. if (eventData["scene.camera"]) {
  371. console.log(
  372. "当前相机视角:",
  373. eventData["scene.camera"],
  374. eventData["scene.aspectratio"]
  375. );
  376. }
  377. });
  378. } catch (e) {
  379. console.error("捕获到 WebGL 错误:", e);
  380. // alert("图表渲染失败!");
  381. }
  382. // Plotly.newPlot(`plotly-3d-chart-` + this.index, traces, layout);
  383. },
  384. resetCamera() {
  385. Plotly.relayout(`plotly-3d-chart-` + this.index, {
  386. "scene.camera": {
  387. up: {
  388. x: 0.200292643688136,
  389. y: 0.2488259353493132,
  390. z: 0.947612004346693,
  391. },
  392. center: {
  393. x: -0.052807476121180814,
  394. y: 0.02451796399554085,
  395. z: -0.022911006648570736,
  396. },
  397. eye: {
  398. x: -2.126379643342493,
  399. y: -2.551422475965373,
  400. z: 1.0917667684145647,
  401. },
  402. projection: {
  403. type: "orthographic",
  404. },
  405. },
  406. "scene.aspectratio": {
  407. x: 2.2,
  408. y: 1.7,
  409. z: 1,
  410. },
  411. });
  412. },
  413. },
  414. };
  415. </script>
  416. <style scoped>
  417. /* #scene {
  418. background: #e5ecf6 !important;
  419. }
  420. .js-plotly-plot .plotly,
  421. .js-plotly-plot .plotly div {
  422. background: #e5ecf6 !important;
  423. } */
  424. /* 样式可以根据需求自定义 */
  425. #plotly-3d-chart {
  426. width: 100%;
  427. height: 600px;
  428. }
  429. ::v-deep canvas {
  430. /* height: 400px !important; */
  431. }
  432. </style>