CustomChart.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  1. <template>
  2. <div ref="chartRef" :style="{ height, width }"></div>
  3. </template>
  4. <script lang="ts" setup>
  5. import { ref, Ref, watch } from 'vue';
  6. import { useECharts } from '/@/hooks/web/useECharts';
  7. import { get } from 'lodash-es';
  8. import { ModuleDataChart } from '/@/views/vent/deviceManager/configurationTable/types';
  9. import { EChartsOption, graphic } from 'echarts';
  10. import { getData, getFormattedText } from '../../hooks/helper';
  11. import { install$10 } from 'echarts/types/dist/shared';
  12. const props = withDefaults(
  13. defineProps<{
  14. chartData: Record<string, any>[] | Record<string, any>;
  15. chartConfig: ModuleDataChart;
  16. height?: string;
  17. width?: string;
  18. }>(),
  19. {
  20. chartData: () => [],
  21. height: '100%',
  22. width: '100%',
  23. }
  24. );
  25. const chartRef = ref<HTMLDivElement | null>(null);
  26. const { setOptions, getInstance } = useECharts(chartRef as Ref<HTMLDivElement>);
  27. // 核心方法,生成适用与 echart 的选项,这个方法需要适配 chart 类型的每种子类型
  28. const genChartOption = () => {
  29. const inst = getInstance();
  30. const domWidth = inst ? inst.getWidth() : 500;
  31. // 依据每一个图表配置生成图表选项
  32. const { yAxis = [], xAxis = [], legend, order, type, sortBy, series, dataZoom = [] } = props.chartConfig;
  33. const textStyle = {
  34. color: '#fff',
  35. };
  36. let sorttedData: any[] = [];
  37. if (Array.isArray(props.chartData)) {
  38. sorttedData = props.chartData;
  39. } else {
  40. sorttedData = [props.chartData];
  41. }
  42. // 如果这个配置指明了需要排序则执行排序
  43. if (sortBy && order) {
  44. sorttedData.sort((pre, cur) => {
  45. if (order === 'asc') {
  46. return get(pre, sortBy, 0) - get(cur, sortBy, 0);
  47. } else {
  48. return get(cur, sortBy, 0) - get(pre, sortBy, 0);
  49. }
  50. });
  51. }
  52. // 该项作为下面所有图表依赖的基准系列数据
  53. const baseSeries: { name: string; data: [string, string][] }[] = sorttedData.reduce((res: any[], baseData) => {
  54. series.forEach((serie) => {
  55. res.push({
  56. name: getFormattedText(baseData, serie.label),
  57. data: (getData(baseData, serie.readFrom) || []).map((data) => {
  58. return [getData(data, serie.xprop), getData(data, serie.yprop)]; /** x y */
  59. // return { name: getData(data, serie.xprop), value: getData(data, serie.yprop) }; /** x y */
  60. }),
  61. });
  62. });
  63. return res;
  64. }, []);
  65. const baseDataZoom = dataZoom.map((e, i) => {
  66. return {
  67. type: 'slider',
  68. show: get(baseSeries, '[0].data.length', 1) > e.maxAxisLength,
  69. xAxisIndex: i,
  70. end: e.maxAxisLength,
  71. };
  72. });
  73. // if (type === 'pie') {
  74. // return {
  75. // textStyle,
  76. // legend: {
  77. // textStyle,
  78. // show: legend.show,
  79. // },
  80. // tooltip: {
  81. // trigger: 'item',
  82. // },
  83. // color: ['#d9a1ff', '#00d1ff', '#82fe78'],
  84. // series: baseSeries.map((serie) => {
  85. // return {
  86. // type: 'pie',
  87. // radius: ['50%', '75%'],
  88. // center: ['50%', '55%'],
  89. // labelLine: { show: false },
  90. // label: { show: false },
  91. // itemStyle: {
  92. // shadowBlur: 20,
  93. // shadowColor: '#259bcf',
  94. // },
  95. // data: serie.data.map((e) => ({
  96. // name: e[0],
  97. // value: e[1],
  98. // })),
  99. // };
  100. // }),
  101. // };
  102. // }
  103. if (type === 'pie') {
  104. return {
  105. textStyle,
  106. legend: {
  107. textStyle,
  108. show: legend.show,
  109. },
  110. tooltip: {
  111. trigger: 'item',
  112. },
  113. color: ['#73C0DE', '#5470C6', '#91CC75', '#FAC858', '#ED6666', '#17d1b2', '#2ae271', '#11bce7', '#c127f0', '#ee125b'],
  114. series: baseSeries.reduce((curr: EChartsOption[], serie, index) => {
  115. const colors = ['#73C0DE', '#5470C6', '#91CC75', '#FAC858', '#ED6666', '#17d1b2', '#2ae271', '#11bce7', '#c127f0', '#ee125b'];
  116. if (baseSeries.length === 1) {
  117. curr.push({
  118. ...serie,
  119. type: 'pie',
  120. radius: ['30%', '64%'],
  121. center: ['50%', '55%'],
  122. z: 1,
  123. padAngle: 5,
  124. itemStyle: {
  125. shadowColor: 'rgba(0, 0, 0, 0.5)',
  126. borderWidth: 10,
  127. },
  128. label: {
  129. show: true,
  130. position: 'inner',
  131. formatter: '{d}%',
  132. color: '#fff',
  133. fontSize: 14,
  134. rich: {
  135. d: {
  136. fontFamily: '微软雅黑',
  137. fontSize: 16,
  138. color: '#fff',
  139. lineHeight: 30,
  140. },
  141. },
  142. },
  143. labelLine: { show: false },
  144. data: serie.data.map((e) => ({
  145. name: e[0],
  146. value: e[1],
  147. })),
  148. });
  149. curr.push({
  150. ...serie,
  151. type: 'pie',
  152. radius: ['64%', '74%'],
  153. center: ['50%', '55%'],
  154. labelLine: { show: false },
  155. label: { show: false },
  156. padAngle: 5,
  157. avoidLabelOverlap: false,
  158. itemStyle: {
  159. borderWidth: 10,
  160. normal: {
  161. color: function (obj) {
  162. return `${colors[obj['dataIndex']]}88`;
  163. },
  164. },
  165. },
  166. data: serie.data.map((e) => ({
  167. name: e[0],
  168. value: e[1],
  169. })),
  170. });
  171. }
  172. curr.push({
  173. ...serie,
  174. type: 'pie',
  175. radius: ['25%', '30%'],
  176. center: ['50%', '55%'],
  177. labelLine: { show: false },
  178. label: { show: false },
  179. z: 5,
  180. padAngle: 5,
  181. itemStyle: {
  182. borderWidth: 10,
  183. normal: {
  184. color: function (obj) {
  185. return `${colors[obj['dataIndex']]}88`;
  186. },
  187. },
  188. },
  189. data: serie.data.map((e) => ({
  190. name: e[0],
  191. value: e[1],
  192. })),
  193. });
  194. return curr;
  195. }, []),
  196. };
  197. }
  198. // 柱状图
  199. if (type === 'bar') {
  200. return {
  201. textStyle,
  202. grid: {
  203. top: 50,
  204. bottom: dataZoom.length ? 70 : 50,
  205. },
  206. legend: {
  207. textStyle,
  208. show: legend.show,
  209. },
  210. tooltip: {
  211. trigger: 'item',
  212. },
  213. dataZoom: baseDataZoom,
  214. xAxis: xAxis.map((e) => {
  215. return {
  216. ...e,
  217. type: 'category',
  218. axisLabel: {
  219. interval: 0,
  220. width: 800 / get(baseSeries, '[0].data.length', 1),
  221. overflow: 'break',
  222. },
  223. };
  224. }),
  225. yAxis: yAxis.map((e) => {
  226. return {
  227. ...e,
  228. splitLine: {
  229. lineStyle: {
  230. opacity: 0.1,
  231. },
  232. },
  233. };
  234. }),
  235. series: baseSeries.reduce((curr: EChartsOption[], serie, index) => {
  236. const colors = ['#66ffff', '#ffff66'];
  237. if (baseSeries.length === 1) {
  238. curr.push({
  239. ...serie,
  240. type: 'pictorialBar',
  241. symbol: 'circle',
  242. symbolPosition: 'end',
  243. symbolSize: [20, 20],
  244. symbolOffset: [0, -10],
  245. barGap: '-100%',
  246. yAxisIndex: index,
  247. itemStyle: {
  248. color: colors[index % colors.length],
  249. },
  250. });
  251. }
  252. curr.push({
  253. ...serie,
  254. type: 'bar',
  255. silent: true,
  256. yAxisIndex: index,
  257. barWidth: 10,
  258. barGap: '100%',
  259. itemStyle: {
  260. color: new graphic.LinearGradient(0, 0, 0, 1, [
  261. { offset: 0, color: colors[index % colors.length] },
  262. { offset: 0.2, color: colors[index % colors.length] },
  263. { offset: 1, color: `${colors[index % colors.length]}22` },
  264. ]),
  265. borderRadius: [5, 5, 0, 0],
  266. },
  267. });
  268. return curr;
  269. }, []),
  270. };
  271. }
  272. // 折线图和上面的柱状图类似
  273. if (type === 'line') {
  274. return {
  275. textStyle,
  276. legend: {
  277. show: legend.show,
  278. top: 10,
  279. right: 10,
  280. textStyle,
  281. },
  282. grid: {
  283. left: 60,
  284. top: 40,
  285. right: 60,
  286. bottom: dataZoom.length ? 70 : 30,
  287. },
  288. dataZoom: baseDataZoom,
  289. xAxis: xAxis.map((e) => {
  290. return {
  291. ...e,
  292. type: 'category',
  293. axisLabel: {
  294. width: 100,
  295. overflow: 'break',
  296. },
  297. };
  298. }),
  299. yAxis: yAxis.map((e) => {
  300. return {
  301. ...e,
  302. splitLine: {
  303. lineStyle: {
  304. opacity: 0.1,
  305. },
  306. },
  307. };
  308. }),
  309. series: baseSeries.map((serie) => {
  310. return {
  311. ...serie,
  312. type: 'line',
  313. };
  314. }),
  315. };
  316. }
  317. // 平滑曲线图
  318. if (type === 'line_smooth') {
  319. return {
  320. textStyle,
  321. legend: {
  322. show: legend.show,
  323. top: 10,
  324. textStyle,
  325. },
  326. grid: {
  327. left: 60,
  328. right: 60,
  329. bottom: dataZoom.length ? 70 : 30,
  330. },
  331. dataZoom: baseDataZoom,
  332. xAxis: xAxis.map((e) => {
  333. return {
  334. ...e,
  335. type: 'category',
  336. };
  337. }),
  338. yAxis: yAxis.map((e) => {
  339. return {
  340. ...e,
  341. splitLine: {
  342. lineStyle: {
  343. opacity: 0.1,
  344. },
  345. },
  346. };
  347. }),
  348. series: baseSeries.map((serie) => {
  349. return {
  350. ...serie,
  351. type: 'line',
  352. smooth: true,
  353. itemStyle: {
  354. opacity: 0,
  355. },
  356. };
  357. }),
  358. };
  359. }
  360. // 折线区域图,即折线下面的区域填满
  361. if (type === 'line_area') {
  362. return {
  363. textStyle,
  364. legend: {
  365. textStyle,
  366. show: legend.show,
  367. },
  368. grid: {
  369. left: 50,
  370. top: 50,
  371. right: 50,
  372. bottom: dataZoom.length ? 70 : 50,
  373. },
  374. dataZoom: baseDataZoom,
  375. xAxis: xAxis.map((e) => {
  376. return {
  377. ...e,
  378. type: 'category',
  379. boundaryGap: false,
  380. };
  381. }),
  382. yAxis: yAxis.map((e) => {
  383. return {
  384. ...e,
  385. splitLine: {
  386. lineStyle: {
  387. color: '#fff',
  388. opacity: 0.1,
  389. },
  390. },
  391. };
  392. }),
  393. series: baseSeries.map((serie, index) => {
  394. const colors = ['#66ffff', '#6666ff'];
  395. return {
  396. ...serie,
  397. type: 'line',
  398. symbol: 'none',
  399. endLabel: { distance: 0 },
  400. lineStyle: { color: colors[index % colors.length] },
  401. areaStyle: {
  402. origin: 'auto',
  403. color: new graphic.LinearGradient(0, 0, 0, 1, [
  404. { offset: 0, color: colors[index % colors.length] },
  405. { offset: 0.2, color: colors[index % colors.length] },
  406. { offset: 1, color: `${colors[index % colors.length]}22` },
  407. ]),
  408. },
  409. };
  410. }),
  411. };
  412. }
  413. if (type === 'line_bar') {
  414. return {
  415. textStyle,
  416. legend: {
  417. textStyle,
  418. show: legend.show,
  419. top: 10,
  420. right: 10,
  421. },
  422. grid: {
  423. left: 40,
  424. top: 50,
  425. right: 40,
  426. bottom: dataZoom.length ? 70 : 10,
  427. show: false,
  428. },
  429. dataZoom: baseDataZoom,
  430. xAxis: xAxis.map((e) => {
  431. return {
  432. ...e,
  433. type: 'category',
  434. };
  435. }),
  436. yAxis: yAxis.map((e) => {
  437. return {
  438. ...e,
  439. };
  440. }),
  441. series: baseSeries.map((serie, i) => {
  442. return {
  443. ...serie,
  444. type: i % 2 ? 'line' : 'bar',
  445. smooth: true,
  446. barWidth: 20,
  447. };
  448. }),
  449. };
  450. }
  451. // 堆叠柱状图
  452. if (type === 'bar_stack') {
  453. return {
  454. textStyle,
  455. tooltip: {
  456. trigger: 'axis',
  457. axisPointer: {
  458. type: 'shadow',
  459. },
  460. },
  461. grid: {
  462. top: 50,
  463. bottom: 30,
  464. },
  465. legend: {
  466. textStyle,
  467. show: legend.show,
  468. },
  469. xAxis: xAxis.map((e) => {
  470. return {
  471. ...e,
  472. type: 'category',
  473. };
  474. }),
  475. yAxis: yAxis.map((e) => {
  476. return {
  477. ...e,
  478. splitLine: {
  479. lineStyle: {
  480. color: 'rgba(21,80,126,0.3)',
  481. type: 'dashed',
  482. },
  483. },
  484. };
  485. }),
  486. series: baseSeries.map((serie) => {
  487. return {
  488. ...serie,
  489. type: 'bar',
  490. stack: 'stackME',
  491. barMaxWidth: '24',
  492. emphasis: {
  493. focus: 'series',
  494. },
  495. label: {
  496. show: true,
  497. position: 'top', //在上方显示
  498. textStyle: {
  499. //数值样式
  500. color: '#fff',
  501. fontSize: 14,
  502. },
  503. },
  504. };
  505. }),
  506. color: ['#F56731', '#00E8FF'],
  507. };
  508. }
  509. // 柱状图,圆柱形样式
  510. if (type === 'bar_cylinder') {
  511. return {
  512. textStyle,
  513. grid: {
  514. top: 40,
  515. bottom: 50,
  516. },
  517. legend: {
  518. textStyle,
  519. show: legend.show,
  520. },
  521. tooltip: {
  522. trigger: 'item',
  523. },
  524. xAxis: xAxis.map((e) => {
  525. return {
  526. ...e,
  527. type: 'category',
  528. axisLabel: {
  529. interval: 0,
  530. width: (domWidth - 100) / get(baseSeries, '[0].data.length', 1),
  531. overflow: 'break',
  532. },
  533. };
  534. }),
  535. yAxis: yAxis.map((e) => {
  536. return {
  537. ...e,
  538. splitLine: {
  539. lineStyle: {
  540. opacity: 0.1,
  541. },
  542. },
  543. };
  544. }),
  545. series: baseSeries.reduce((curr: EChartsOption[], serie, index) => {
  546. // const colors = ['#66ffff', '#00ff66', '#ffff66'];
  547. const colors = ['#73C0DE', '#ED6666', '#5470C6', '#91CC75', '#FAC858', '#17d1b2', '#2ae271', '#11bce7', '#c127f0', '#ee125b'];
  548. if (baseSeries.length === 1) {
  549. curr.push({
  550. ...serie,
  551. type: 'pictorialBar',
  552. symbol: 'circle',
  553. symbolPosition: 'end',
  554. symbolSize: [20, 10],
  555. symbolOffset: [0, -5],
  556. barGap: '-100%',
  557. yAxisIndex: index,
  558. itemStyle: {
  559. color: ({ dataIndex }) => colors[dataIndex % colors.length],
  560. },
  561. });
  562. curr.push({
  563. ...serie,
  564. type: 'pictorialBar',
  565. symbol: 'circle',
  566. symbolPosition: 'start',
  567. symbolSize: [20, 10],
  568. symbolOffset: [0, 5],
  569. barGap: '-100%',
  570. yAxisIndex: index,
  571. itemStyle: {
  572. color: ({ dataIndex }) => colors[dataIndex % colors.length],
  573. },
  574. });
  575. }
  576. curr.push({
  577. ...serie,
  578. type: 'bar',
  579. // silent: true,
  580. yAxisIndex: index,
  581. barWidth: 20,
  582. barGap: '100%',
  583. itemStyle: {
  584. color: ({ dataIndex }) =>
  585. new graphic.LinearGradient(0, 0, 0, 1, [
  586. { offset: 0, color: `${colors[dataIndex % colors.length]}44` },
  587. { offset: 0.5, color: colors[dataIndex % colors.length] },
  588. { offset: 1, color: colors[dataIndex % colors.length] },
  589. ]),
  590. borderRadius: [5, 5, 0, 0],
  591. },
  592. label: {
  593. show: true, //开启显示
  594. position: 'top', //在上方显示
  595. textStyle: {
  596. //数值样式
  597. color: '#ffffffbb',
  598. fontSize: 13,
  599. },
  600. formatter: function (obj) {
  601. return obj['data'][1];
  602. },
  603. },
  604. });
  605. return curr;
  606. }, []),
  607. };
  608. }
  609. return {};
  610. };
  611. watch(
  612. () => props.chartData,
  613. () => {
  614. initCharts();
  615. },
  616. {
  617. immediate: true,
  618. }
  619. );
  620. function initCharts() {
  621. const o = genChartOption();
  622. setOptions(o as EChartsOption, false);
  623. }
  624. </script>