CustomChart.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  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. const props = withDefaults(
  12. defineProps<{
  13. chartData: Record<string, any>[] | Record<string, any>;
  14. chartConfig: ModuleDataChart;
  15. height?: string;
  16. width?: string;
  17. }>(),
  18. {
  19. chartData: () => [],
  20. height: '100%',
  21. width: '100%',
  22. }
  23. );
  24. const chartRef = ref<HTMLDivElement | null>(null);
  25. const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
  26. // 核心方法,生成适用与 echart 的选项,这个方法需要适配 chart 类型的每种子类型
  27. const genChartOption = () => {
  28. // 依据每一个图表配置生成图表选项
  29. const { yAxis = [], xAxis = [], legend, order, type, sortBy, series } = props.chartConfig;
  30. const textStyle = {
  31. color: '#fff',
  32. };
  33. let sorttedData: any[] = [];
  34. if (Array.isArray(props.chartData)) {
  35. sorttedData = props.chartData;
  36. } else {
  37. sorttedData = [props.chartData];
  38. }
  39. // 如果这个配置指明了需要排序则执行排序
  40. if (sortBy && order) {
  41. sorttedData.sort((pre, cur) => {
  42. if (order === 'asc') {
  43. return get(pre, sortBy, 0) - get(cur, sortBy, 0);
  44. } else {
  45. return get(cur, sortBy, 0) - get(pre, sortBy, 0);
  46. }
  47. });
  48. }
  49. // 该项作为下面所有图表依赖的基准系列数据
  50. const baseSeries: { name: string; data: [string, string][] }[] = sorttedData.reduce((res: any[], baseData) => {
  51. series.forEach((serie) => {
  52. res.push({
  53. name: getFormattedText(baseData, serie.label),
  54. data: (getData(baseData, serie.readFrom) || []).map((data) => {
  55. return [getData(data, serie.xprop), getData(data, serie.yprop)]; /** x y */
  56. // return { name: getData(data, serie.xprop), value: getData(data, serie.yprop) }; /** x y */
  57. }),
  58. });
  59. });
  60. return res;
  61. }, []);
  62. if (type === 'pie') {
  63. return {
  64. textStyle,
  65. legend: {
  66. textStyle,
  67. show: legend.show,
  68. },
  69. tooltip: {
  70. trigger: 'item',
  71. },
  72. color: ['#d9a1ff', '#00d1ff', '#82fe78'],
  73. series: baseSeries.map((serie) => {
  74. return {
  75. type: 'pie',
  76. radius: ['50%', '75%'],
  77. center: ['50%', '55%'],
  78. labelLine: { show: false },
  79. label: { show: false },
  80. itemStyle: {
  81. shadowBlur: 20,
  82. shadowColor: '#259bcf',
  83. },
  84. data: serie.data.map((e) => ({
  85. name: e[0],
  86. value: e[1],
  87. })),
  88. };
  89. }),
  90. };
  91. }
  92. // 柱状图
  93. if (type === 'bar') {
  94. return {
  95. textStyle,
  96. grid: {
  97. top: 50,
  98. bottom: 50,
  99. },
  100. legend: {
  101. textStyle,
  102. show: legend.show,
  103. },
  104. tooltip: {
  105. trigger: 'item',
  106. },
  107. xAxis: xAxis.map((e) => {
  108. return {
  109. ...e,
  110. type: 'category',
  111. axisLabel: {
  112. interval: 0,
  113. width: 800 / get(baseSeries, '[0].data.length', 1),
  114. overflow: 'break',
  115. },
  116. };
  117. }),
  118. yAxis: yAxis.map((e) => {
  119. return {
  120. ...e,
  121. splitLine: {
  122. lineStyle: {
  123. opacity: 0.1,
  124. },
  125. },
  126. };
  127. }),
  128. series: baseSeries.reduce((curr: EChartsOption[], serie, index) => {
  129. const colors = ['#66ffff', '#ffff66'];
  130. if (baseSeries.length === 1) {
  131. curr.push({
  132. ...serie,
  133. type: 'pictorialBar',
  134. symbol: 'circle',
  135. symbolPosition: 'end',
  136. symbolSize: [20, 20],
  137. symbolOffset: [0, -10],
  138. barGap: '-100%',
  139. yAxisIndex: index,
  140. itemStyle: {
  141. color: colors[index % colors.length],
  142. },
  143. });
  144. }
  145. curr.push({
  146. ...serie,
  147. type: 'bar',
  148. silent: true,
  149. yAxisIndex: index,
  150. barWidth: 10,
  151. barGap: '100%',
  152. itemStyle: {
  153. color: new graphic.LinearGradient(0, 0, 0, 1, [
  154. { offset: 0, color: colors[index % colors.length] },
  155. { offset: 0.2, color: colors[index % colors.length] },
  156. { offset: 1, color: `${colors[index % colors.length]}22` },
  157. ]),
  158. borderRadius: [5, 5, 0, 0],
  159. },
  160. });
  161. return curr;
  162. }, []),
  163. };
  164. }
  165. // 折线图和上面的柱状图类似
  166. if (type === 'line') {
  167. return {
  168. textStyle,
  169. legend: {
  170. show: legend.show,
  171. top: 10,
  172. right: 10,
  173. textStyle,
  174. },
  175. grid: {
  176. left: 60,
  177. top: 40,
  178. right: 60,
  179. bottom: 30,
  180. },
  181. xAxis: xAxis.map((e) => {
  182. return {
  183. ...e,
  184. type: 'category',
  185. axisLabel: {
  186. width: 100,
  187. overflow: 'break',
  188. },
  189. };
  190. }),
  191. yAxis: yAxis.map((e) => {
  192. return {
  193. ...e,
  194. splitLine: {
  195. lineStyle: {
  196. opacity: 0.1,
  197. },
  198. },
  199. };
  200. }),
  201. series: baseSeries.map((serie) => {
  202. return {
  203. ...serie,
  204. type: 'line',
  205. };
  206. }),
  207. };
  208. }
  209. // 平滑曲线图
  210. if (type === 'line_smooth') {
  211. return {
  212. textStyle,
  213. legend: {
  214. show: legend.show,
  215. top: 10,
  216. textStyle,
  217. },
  218. grid: {
  219. left: 60,
  220. right: 60,
  221. bottom: 30,
  222. },
  223. xAxis: xAxis.map((e) => {
  224. return {
  225. ...e,
  226. type: 'category',
  227. };
  228. }),
  229. yAxis: yAxis.map((e) => {
  230. return {
  231. ...e,
  232. splitLine: {
  233. lineStyle: {
  234. opacity: 0.1,
  235. },
  236. },
  237. };
  238. }),
  239. series: baseSeries.map((serie) => {
  240. return {
  241. ...serie,
  242. type: 'line',
  243. smooth: true,
  244. itemStyle: {
  245. opacity: 0,
  246. },
  247. };
  248. }),
  249. };
  250. }
  251. // 折线区域图,即折线下面的区域填满
  252. if (type === 'line_area') {
  253. return {
  254. textStyle,
  255. legend: {
  256. textStyle,
  257. show: legend.show,
  258. },
  259. grid: {
  260. left: 50,
  261. top: 50,
  262. right: 50,
  263. bottom: 50,
  264. },
  265. xAxis: xAxis.map((e) => {
  266. return {
  267. ...e,
  268. type: 'category',
  269. boundaryGap: false,
  270. };
  271. }),
  272. yAxis: yAxis.map((e) => {
  273. return {
  274. ...e,
  275. splitLine: {
  276. lineStyle: {
  277. color: '#fff',
  278. opacity: 0.1,
  279. },
  280. },
  281. };
  282. }),
  283. series: baseSeries.map((serie, index) => {
  284. const colors = ['#66ffff', '#6666ff'];
  285. return {
  286. ...serie,
  287. type: 'line',
  288. symbol: 'none',
  289. endLabel: { distance: 0 },
  290. lineStyle: { color: colors[index % colors.length] },
  291. areaStyle: {
  292. origin: 'auto',
  293. color: new graphic.LinearGradient(0, 0, 0, 1, [
  294. { offset: 0, color: colors[index % colors.length] },
  295. { offset: 0.2, color: colors[index % colors.length] },
  296. { offset: 1, color: `${colors[index % colors.length]}22` },
  297. ]),
  298. },
  299. };
  300. }),
  301. };
  302. }
  303. if (type === 'line_bar') {
  304. return {
  305. textStyle,
  306. legend: {
  307. textStyle,
  308. show: legend.show,
  309. top: 10,
  310. right: 10,
  311. },
  312. grid: {
  313. left: 40,
  314. top: 50,
  315. right: 40,
  316. bottom: 10,
  317. show: false,
  318. },
  319. xAxis: xAxis.map((e) => {
  320. return {
  321. ...e,
  322. type: 'category',
  323. };
  324. }),
  325. yAxis: yAxis.map((e) => {
  326. return {
  327. ...e,
  328. };
  329. }),
  330. series: baseSeries.map((serie, i) => {
  331. return {
  332. ...serie,
  333. type: i % 2 ? 'line' : 'bar',
  334. smooth: true,
  335. barWidth: 20,
  336. };
  337. }),
  338. };
  339. }
  340. // 堆叠柱状图
  341. if (type === 'bar_stack') {
  342. return {
  343. textStyle,
  344. tooltip: {
  345. trigger: 'axis',
  346. axisPointer: {
  347. type: 'shadow',
  348. },
  349. },
  350. grid: {
  351. top: 50,
  352. bottom: 30,
  353. },
  354. legend: {
  355. textStyle,
  356. show: legend.show,
  357. },
  358. xAxis: xAxis.map((e) => {
  359. return {
  360. ...e,
  361. type: 'category',
  362. };
  363. }),
  364. yAxis: yAxis.map((e) => {
  365. return {
  366. ...e,
  367. splitLine: {
  368. lineStyle: {
  369. color: 'rgba(21,80,126,0.3)',
  370. type: 'dashed',
  371. },
  372. },
  373. };
  374. }),
  375. series: baseSeries.map((serie) => {
  376. return {
  377. ...serie,
  378. type: 'bar',
  379. stack: 'stackME',
  380. barMaxWidth: '24',
  381. emphasis: {
  382. focus: 'series',
  383. },
  384. label: {
  385. show: true,
  386. position: 'top', //在上方显示
  387. textStyle: {
  388. //数值样式
  389. color: '#fff',
  390. fontSize: 14,
  391. },
  392. },
  393. };
  394. }),
  395. color: ['#F56731', '#00E8FF'],
  396. };
  397. }
  398. // 柱状图,圆柱形样式
  399. if (type === 'bar_cylinder') {
  400. return {
  401. textStyle,
  402. grid: {
  403. top: 40,
  404. bottom: 50,
  405. },
  406. legend: {
  407. textStyle,
  408. show: legend.show,
  409. },
  410. tooltip: {
  411. trigger: 'item',
  412. },
  413. xAxis: xAxis.map((e) => {
  414. return {
  415. ...e,
  416. type: 'category',
  417. axisLabel: {
  418. interval: 0,
  419. width: 800 / get(baseSeries, '[0].data.length', 1),
  420. overflow: 'break',
  421. },
  422. };
  423. }),
  424. yAxis: yAxis.map((e) => {
  425. return {
  426. ...e,
  427. splitLine: {
  428. lineStyle: {
  429. opacity: 0.1,
  430. },
  431. },
  432. };
  433. }),
  434. series: baseSeries.reduce((curr: EChartsOption[], serie, index) => {
  435. const colors = ['#66ffff', '#00ff66', '#ffff66'];
  436. if (baseSeries.length === 1) {
  437. curr.push({
  438. ...serie,
  439. type: 'pictorialBar',
  440. symbol: 'circle',
  441. symbolPosition: 'end',
  442. symbolSize: [20, 10],
  443. symbolOffset: [0, -5],
  444. barGap: '-100%',
  445. yAxisIndex: index,
  446. itemStyle: {
  447. color: ({ dataIndex }) => colors[dataIndex % colors.length],
  448. },
  449. });
  450. curr.push({
  451. ...serie,
  452. type: 'pictorialBar',
  453. symbol: 'circle',
  454. symbolPosition: 'start',
  455. symbolSize: [20, 10],
  456. symbolOffset: [0, 5],
  457. barGap: '-100%',
  458. yAxisIndex: index,
  459. itemStyle: {
  460. color: ({ dataIndex }) => colors[dataIndex % colors.length],
  461. },
  462. });
  463. }
  464. curr.push({
  465. ...serie,
  466. type: 'bar',
  467. silent: true,
  468. yAxisIndex: index,
  469. barWidth: 20,
  470. barGap: '100%',
  471. itemStyle: {
  472. color: ({ dataIndex }) =>
  473. new graphic.LinearGradient(0, 0, 0, 1, [
  474. { offset: 0, color: `${colors[dataIndex % colors.length]}44` },
  475. { offset: 0.5, color: colors[dataIndex % colors.length] },
  476. { offset: 1, color: colors[dataIndex % colors.length] },
  477. ]),
  478. borderRadius: [5, 5, 0, 0],
  479. },
  480. });
  481. return curr;
  482. }, []),
  483. };
  484. }
  485. return {};
  486. };
  487. watch(
  488. () => props.chartData,
  489. () => {
  490. initCharts();
  491. },
  492. {
  493. immediate: true,
  494. }
  495. );
  496. function initCharts() {
  497. const o = genChartOption();
  498. setOptions(o as EChartsOption, false);
  499. }
  500. </script>