CustomChart.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  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 { Config } from '/@/views/vent/deviceManager/configurationTable/types';
  9. import { EChartsOption, graphic } from 'echarts';
  10. import { getFormattedText } from '../../../../deviceManager/configurationTable/adapters';
  11. const props = withDefaults(
  12. defineProps<{
  13. chartData: Record<string, any> | Record<string, any>[];
  14. chartConfig: Config['moduleData']['chart']['0'];
  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 = [], order, type, sortBy, series } = props.chartConfig;
  30. const isArray = Array.isArray(props.chartData);
  31. // 饼状图通常依赖单个对象的不同字段绘制
  32. if (type === 'pie' && !isArray) {
  33. return {
  34. legend: { show: false },
  35. color: ['#d9a1ff', '#00d1ff', '#82fe78'],
  36. series: [
  37. {
  38. type: 'pie',
  39. radius: ['50%', '75%'],
  40. center: ['50%', '55%'],
  41. data: series.map((serie) => {
  42. return {
  43. name: serie.label,
  44. value: get(props.chartData, serie.prop, 0),
  45. labelLine: { show: false },
  46. label: { show: false },
  47. itemStyle: {
  48. shadowBlur: 20,
  49. shadowColor: '#259bcf',
  50. },
  51. };
  52. }),
  53. },
  54. ],
  55. };
  56. }
  57. if (!isArray) return {};
  58. let sorttedData = [...(props.chartData as any[])];
  59. // 如果这个配置指明了需要排序则执行排序
  60. if (sortBy && order) {
  61. sorttedData.sort((pre, cur) => {
  62. if (order === 'asc') {
  63. return get(pre, sortBy, 0) - get(cur, sortBy, 0);
  64. } else {
  65. return get(cur, sortBy, 0) - get(pre, sortBy, 0);
  66. }
  67. });
  68. }
  69. // 柱状图则要求使用数组形式的数据作依赖
  70. if (type === 'bar') {
  71. return {
  72. grid: {
  73. top: 50,
  74. height: 150,
  75. },
  76. textStyle: {
  77. color: '#fff',
  78. },
  79. legend: { show: false },
  80. xAxis: xAxis.map((e) => {
  81. return {
  82. type: 'category',
  83. data: sorttedData.map((d) => {
  84. return getFormattedText(d, e.label);
  85. }),
  86. };
  87. }),
  88. yAxis: yAxis.map((e) => {
  89. return {
  90. name: e.label,
  91. position: e.align,
  92. splitLine: {
  93. lineStyle: {
  94. color: '#ffffff',
  95. opacity: 0.3,
  96. },
  97. },
  98. };
  99. }),
  100. series: series.reduce((curr: EChartsOption[], serie, index) => {
  101. const colors = ['#66ffff', '#ffff66'];
  102. // 系列选项,如果指定了x轴配置则以x轴配置优先
  103. const data = sorttedData.map((d) => {
  104. return {
  105. name: serie.label,
  106. value: get(d, serie.prop, 0),
  107. };
  108. });
  109. curr.push({
  110. name: 'pictorial element',
  111. type: 'pictorialBar',
  112. symbol: 'circle',
  113. symbolPosition: 'end',
  114. symbolSize: [16, 16],
  115. symbolOffset: [0, -8],
  116. yAxisIndex: index,
  117. itemStyle: {
  118. color: colors[index % colors.length],
  119. },
  120. data,
  121. });
  122. curr.push({
  123. name: 'reference bar',
  124. type: 'bar',
  125. silent: true,
  126. yAxisIndex: index,
  127. itemStyle: {
  128. color: new graphic.LinearGradient(0, 0, 0, 1, [
  129. { offset: 0, color: colors[index % colors.length] },
  130. { offset: 0.2, color: colors[index % colors.length] },
  131. { offset: 1, color: `${colors[index % colors.length]}22` },
  132. ]),
  133. },
  134. tooltip: { show: false },
  135. barWidth: 8,
  136. data,
  137. });
  138. return curr;
  139. }, []),
  140. };
  141. }
  142. // 折线图和上面的柱状图类似
  143. if (type === 'line') {
  144. return {
  145. legend: {
  146. top: 10,
  147. right: 10,
  148. textStyle: {
  149. color: '#fff',
  150. },
  151. },
  152. // backgroundColor: '#081f33',
  153. textStyle: {
  154. color: '#fff',
  155. },
  156. grid: {
  157. left: 60,
  158. top: 50,
  159. right: 60,
  160. bottom: 50,
  161. },
  162. xAxis: xAxis.map((e) => {
  163. return {
  164. type: 'category',
  165. data: sorttedData.map((d) => {
  166. return getFormattedText(d, e.label);
  167. }),
  168. };
  169. }),
  170. yAxis: yAxis.map((e) => {
  171. return {
  172. name: e.label,
  173. position: e.align,
  174. splitLine: {
  175. lineStyle: {
  176. color: '#fff',
  177. opacity: 0.3,
  178. },
  179. },
  180. };
  181. }),
  182. series: series.map((serie) => {
  183. const data = sorttedData.map((d) => {
  184. return {
  185. name: serie.label,
  186. value: get(d, serie.prop, 0),
  187. };
  188. });
  189. return {
  190. type: 'line',
  191. data,
  192. };
  193. }),
  194. };
  195. }
  196. // 折线图和上面的柱状图类似
  197. if (type === 'line_smooth') {
  198. return {
  199. legend: {
  200. top: 10,
  201. right: 10,
  202. textStyle: {
  203. color: '#fff',
  204. },
  205. },
  206. // backgroundColor: '#081f33',
  207. textStyle: {
  208. color: '#fff',
  209. },
  210. grid: {
  211. left: 50,
  212. top: 50,
  213. right: 50,
  214. bottom: 50,
  215. },
  216. xAxis: xAxis.map((e) => {
  217. return {
  218. type: 'category',
  219. data: sorttedData.map((d) => {
  220. return getFormattedText(d, e.label);
  221. }),
  222. };
  223. }),
  224. yAxis: yAxis.map((e) => {
  225. return {
  226. name: e.label,
  227. position: e.align,
  228. splitLine: {
  229. lineStyle: {
  230. opacity: 0.3,
  231. },
  232. },
  233. };
  234. }),
  235. series: series.map((serie) => {
  236. const data = sorttedData.map((d) => {
  237. return {
  238. name: serie.label,
  239. value: get(d, serie.prop, 0),
  240. };
  241. });
  242. return {
  243. type: 'line',
  244. data,
  245. smooth: true,
  246. };
  247. }),
  248. };
  249. }
  250. // 折线图和上面的柱状图类似
  251. if (type === 'line_area') {
  252. return {
  253. legend: {
  254. show: false,
  255. },
  256. // backgroundColor: '#081f33',
  257. textStyle: {
  258. color: '#fff',
  259. },
  260. grid: {
  261. left: 50,
  262. top: 50,
  263. right: 50,
  264. bottom: 50,
  265. },
  266. xAxis: xAxis.map((e) => {
  267. return {
  268. type: 'category',
  269. boundaryGap: false,
  270. data: sorttedData.map((d) => {
  271. return getFormattedText(d, e.label);
  272. }),
  273. };
  274. }),
  275. yAxis: yAxis.map((e) => {
  276. return {
  277. name: e.label,
  278. position: e.align,
  279. splitLine: {
  280. lineStyle: {
  281. color: '#fff',
  282. opacity: 0.3,
  283. },
  284. },
  285. };
  286. }),
  287. series: series.map((serie, index) => {
  288. const colors = ['#66ffff', '#6666ff'];
  289. const data = sorttedData.map((d) => {
  290. return {
  291. name: serie.label,
  292. value: get(d, serie.prop, 0),
  293. };
  294. });
  295. return {
  296. type: 'line',
  297. data,
  298. symbol: 'none',
  299. endLabel: { distance: 0 },
  300. // itemStyle: { show: false, opacity: 0 },
  301. lineStyle: { color: colors[index % colors.length] },
  302. areaStyle: {
  303. origin: 'auto',
  304. color: new graphic.LinearGradient(0, 0, 0, 1, [
  305. { offset: 0, color: colors[index % colors.length] },
  306. { offset: 0.2, color: colors[index % colors.length] },
  307. { offset: 1, color: `${colors[index % colors.length]}22` },
  308. ]),
  309. },
  310. };
  311. }),
  312. };
  313. }
  314. if (type === 'line_bar') {
  315. return {
  316. legend: {
  317. top: 10,
  318. right: 10,
  319. textStyle: {
  320. color: '#fff',
  321. },
  322. },
  323. // backgroundColor: '#081f33',
  324. textStyle: {
  325. color: '#fff',
  326. },
  327. grid: {
  328. left: 40,
  329. top: 50,
  330. right: 40,
  331. bottom: 10,
  332. show: false,
  333. },
  334. xAxis: xAxis.map(() => {
  335. return {
  336. type: 'category',
  337. show: false,
  338. };
  339. }),
  340. yAxis: yAxis.map(() => {
  341. return {
  342. show: false,
  343. // name: e.label,
  344. // position: e.align,
  345. };
  346. }),
  347. series: series.map((serie, i) => {
  348. const data = sorttedData.map((d) => {
  349. return {
  350. name: serie.label,
  351. value: get(d, serie.prop, 0),
  352. label: {
  353. show: true,
  354. position: 'top',
  355. borderWidth: 0,
  356. color: '#fff',
  357. },
  358. itemStyle: {
  359. color: new graphic.LinearGradient(0, 0, 0, 1, [
  360. { offset: 0, color: '#66ffff' },
  361. { offset: 0.2, color: '#66ffff' },
  362. { offset: 1, color: '#66ffff22' },
  363. ]),
  364. },
  365. };
  366. });
  367. return {
  368. type: i % 2 ? 'line' : 'bar',
  369. smooth: true,
  370. data,
  371. barWidth: 20,
  372. };
  373. }),
  374. };
  375. }
  376. return {};
  377. };
  378. watch(
  379. () => props.chartData,
  380. () => {
  381. initCharts();
  382. }
  383. );
  384. function initCharts() {
  385. const o = genChartOption();
  386. setOptions(o as EChartsOption, false);
  387. }
  388. </script>