barStackChart_vertical.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. <template>
  2. <div class="echart_content">
  3. <div class="is_show_all" v-if="props.showCheckBox">
  4. <el-checkbox
  5. v-model="isShowAll"
  6. :indeterminate="isIndeterminate"
  7. @change="ToggleChangeAll"
  8. >
  9. 显示全部
  10. </el-checkbox>
  11. </div>
  12. <!-- 竖向堆叠条形图 -->
  13. <div ref="barStackChart" class="chart_box"></div>
  14. </div>
  15. </template>
  16. <script setup lang="ts">
  17. import { ref, watch, onMounted, onBeforeUnmount, nextTick } from "vue";
  18. import * as echarts from "echarts";
  19. import throttle from "lodash/throttle";
  20. import { getScorePerformanceAnalysis } from "@/utils/common";
  21. // ================= Props & Emits =================
  22. interface Props {
  23. showCheckBox?: boolean;
  24. data?: (string | number)[][];
  25. color?: string[];
  26. isClick?: boolean;
  27. legendList?: string[];
  28. tooltipData?: any[];
  29. }
  30. const props = withDefaults(defineProps<Props>(), {
  31. showCheckBox: true,
  32. data: () => [],
  33. color: () => [],
  34. isClick: false,
  35. legendList: () => [],
  36. tooltipData: () => [],
  37. });
  38. const emit = defineEmits<{
  39. (e: "HandleChartClick", index: number, xName: string): void;
  40. }>();
  41. // ================= Refs & State =================
  42. const barStackChart = ref<HTMLElement | null>(null);
  43. const myChart = ref<echarts.ECharts | null>(null);
  44. const isShowAll = ref(true);
  45. const isIndeterminate = ref(false);
  46. const legendSelected = ref<Record<string, boolean>>({});
  47. const legenSelectList = ref<string[]>([]);
  48. const legenAllList = ref<string[]>([]);
  49. const defaultColors = getScorePerformanceAnalysis();
  50. // ================= Helper Methods =================
  51. const initLegenSelectList = () => {
  52. if (props.legendList.length > 0) {
  53. legenSelectList.value = props.legendList;
  54. } else {
  55. legenSelectList.value = (props.data[0]?.slice(1) as string[]) || [];
  56. }
  57. };
  58. // ================= Core Methods =================
  59. const LoadEchart = () => {
  60. if (myChart.value) {
  61. myChart.value.dispose();
  62. }
  63. if (!barStackChart.value) return;
  64. myChart.value = echarts.init(barStackChart.value, null, {
  65. devicePixelRatio: 2,
  66. });
  67. if (props.data.length === 0) {
  68. return;
  69. }
  70. const dataset = {
  71. source: props.data,
  72. };
  73. const dataCount = props.data.length;
  74. legenAllList.value = (props.data[0]?.slice(1) as string[]) || [];
  75. legendSelected.value = (dataset.source[0] as string[])
  76. .slice(1)
  77. .reduce((acc: Record<string, boolean>, dim) => {
  78. acc[dim] = false;
  79. return acc;
  80. }, {});
  81. legenSelectList.value.forEach((item) => {
  82. legendSelected.value[item] = true;
  83. });
  84. isShowAll.value = legenSelectList.value.length === legenAllList.value.length;
  85. isIndeterminate.value =
  86. legenSelectList.value.length > 0 &&
  87. legenSelectList.value.length < legenAllList.value.length;
  88. let totalWidth = barStackChart.value.clientWidth;
  89. let singleSeriesWidth = Math.ceil((totalWidth - 140) / dataCount);
  90. const barMinWidth = 30;
  91. const dataZoomNum = Math.floor((totalWidth - 140) / (barMinWidth * 1));
  92. const dataZoomEnd = Math.floor((100 / dataCount) * dataZoomNum);
  93. const dataZoom = {
  94. start: 0,
  95. end: dataZoomEnd,
  96. type: "slider",
  97. show: true,
  98. borderColor: "transparent",
  99. borderCap: "round",
  100. xAxisIndex: [0],
  101. height: 8,
  102. left: 20,
  103. right: 20,
  104. bottom: 0,
  105. fillerColor: "transparent",
  106. zoomLock: true,
  107. handleSize: "0",
  108. handleStyle: { color: "#b8b8b8", borderWidth: 2 },
  109. backgroundColor: "transparent",
  110. showDataShadow: false,
  111. showDetail: false,
  112. filterMode: "filter",
  113. };
  114. const chartColors = props.color?.length > 0 ? props.color : defaultColors;
  115. const option: echarts.EChartsOption = {
  116. dataset: dataset,
  117. tooltip: {
  118. triggerOn: "mousemove",
  119. confine: true,
  120. extraCssText:
  121. "border-radius: 4px;padding:5px 0px 5px 5px;white-space:normal;word-warp:break-word;max-width: 400px;",
  122. enterable: true,
  123. formatter: (params: any) => {
  124. const seriesName = params.name;
  125. let tooltip = `<div class='tooltip_content'>`;
  126. let title = seriesName;
  127. tooltip += `<div class='tooltip_title'>${title}</div>`;
  128. let rateLabel = (dataset.source[0] as string[]).slice(1);
  129. for (let i = 0; i < rateLabel.length; i++) {
  130. let labelName = rateLabel[i];
  131. let rate = params.value[i + 1];
  132. let list: any[] = [],
  133. rateNum = "",
  134. isShowTip = props.tooltipData?.length > 0;
  135. if (props.tooltipData?.length > 0) {
  136. const dataIndex = params.dataIndex;
  137. const itemTooltip = props.tooltipData?.[dataIndex]?.[i] ?? "";
  138. list = itemTooltip?.list ?? [];
  139. rateNum = itemTooltip?.rateNum ?? "-";
  140. }
  141. if (isShowTip) {
  142. tooltip += `<div class='tooltip_student'><span class='tooltip_rect_icon' style='background:${chartColors[i]}'></span>${labelName}:${rate == "-" ? "-" : rate + "%"},${rateNum}人</div>`;
  143. if (i == rateLabel.length - 1) {
  144. for (let j = 0; j < list.length; j++) {
  145. tooltip += `<div class='tooltip_student'><span class='tooltip_rect_icon' style='background:#CCCCCC'></span>${list[j].label}:${list[j].value}</div>`;
  146. }
  147. }
  148. } else {
  149. tooltip += `<div class='tooltip_student'><span class='tooltip_rect_icon' style='background:${chartColors[i]}'></span>${labelName}:${rate == "-" ? "-" : rate + "%"}</div>`;
  150. }
  151. }
  152. tooltip += `</div>`;
  153. return tooltip;
  154. },
  155. },
  156. legend: {
  157. show: true,
  158. top: '6px',
  159. left: props.showCheckBox ? "110px" : "0px",
  160. itemGap: 20,
  161. itemHeight: 10,
  162. itemWidth: 20,
  163. textStyle: { fontSize: 12, color: "#333" },
  164. selectedMode: true,
  165. selected: legendSelected.value,
  166. formatter: (name: string) => name,
  167. },
  168. grid: {
  169. top: 50,
  170. left: 40,
  171. right: 60,
  172. bottom: 0,
  173. containLabel: true,
  174. },
  175. dataZoom: singleSeriesWidth < barMinWidth ? dataZoom : null,
  176. yAxis: {
  177. type: "value",
  178. axisLabel: {
  179. formatter: "{value}%",
  180. fontSize: 14,
  181. color: "#666",
  182. fontWeight: 400,
  183. },
  184. axisTick: {
  185. lineStyle: { color: "red", type: "dashed", width: 1 },
  186. },
  187. },
  188. xAxis: {
  189. type: "category",
  190. axisLabel: {
  191. fontSize: 14,
  192. color: "#666",
  193. fontWeight: 400,
  194. interval: 0,
  195. rotate: singleSeriesWidth < 80 ? 45 : 0,
  196. formatter: function (value: string) {
  197. const valueWidth = value.length * 14;
  198. if (valueWidth > singleSeriesWidth) {
  199. if (singleSeriesWidth < 100) {
  200. if (valueWidth > 80) {
  201. return value.slice(0, 2) + "..." + value.slice(-3);
  202. } else {
  203. return value;
  204. }
  205. } else {
  206. let maxLength = Math.floor(singleSeriesWidth / 14) - 1;
  207. return value.slice(0, maxLength) + "...";
  208. }
  209. } else {
  210. return value;
  211. }
  212. },
  213. },
  214. tooltip: {
  215. show: true,
  216. formatter: function (params: any) {
  217. return params.value;
  218. },
  219. },
  220. },
  221. series: (dataset.source[0] as string[]).slice(1).map(function (dim, index) {
  222. const color = chartColors[index];
  223. return {
  224. type: "bar",
  225. stack: "total",
  226. barMaxWidth: 50,
  227. label: {
  228. show: singleSeriesWidth > 22,
  229. position: "inside",
  230. color: "#fff",
  231. fontSize: 12,
  232. formatter: function (params: any) {
  233. if (params.value[params.seriesIndex + 1] == 0) {
  234. return "";
  235. } else {
  236. return params.value[params.seriesIndex + 1] + "%";
  237. }
  238. },
  239. },
  240. itemStyle: { color: color },
  241. };
  242. }),
  243. };
  244. myChart.value.setOption(option);
  245. myChart.value.on("legendselectchanged", HandleLegendSelectChanged);
  246. if (props.isClick) {
  247. // const containerWidth = barStackChart.value.offsetWidth;
  248. const xAxisDataLength = props.data.length - 1;
  249. const gridRect = myChart.value
  250. .getModel()
  251. .getComponent("grid")
  252. .coordinateSystem.getRect();
  253. const singleLabelWidth = gridRect.width / xAxisDataLength;
  254. myChart.value.on("click", (params: any) => {
  255. if (params.seriesType === "bar") {
  256. let pixelPosition = myChart.value!.convertToPixel(
  257. { xAxisIndex: 0 },
  258. params.name,
  259. );
  260. myChart.value!.setOption({
  261. graphic: {
  262. id: "highlight-box",
  263. type: "rect",
  264. shape: {
  265. x: pixelPosition - singleLabelWidth / 2,
  266. y: gridRect.y,
  267. width: singleLabelWidth,
  268. height: gridRect.height,
  269. },
  270. style: { fill: params.color + "30" },
  271. },
  272. });
  273. emit("HandleChartClick", params.dataIndex, params.name);
  274. }
  275. });
  276. let defaultPixelPosition = myChart.value.convertToPixel(
  277. { xAxisIndex: 0 },
  278. props.data[1][0],
  279. );
  280. myChart.value.setOption({
  281. graphic: {
  282. id: "highlight-box",
  283. type: "rect",
  284. shape: {
  285. x: defaultPixelPosition - singleLabelWidth / 2,
  286. y: gridRect.y,
  287. width: singleLabelWidth,
  288. height: gridRect.height,
  289. },
  290. style: { fill: "rgba(84,112,198,0.1)" },
  291. },
  292. });
  293. }
  294. };
  295. const HandleLegendSelectChanged = (params: any) => {
  296. const selected = params.selected;
  297. legenSelectList.value = [];
  298. for (let i = 0; i < legenAllList.value.length; i++) {
  299. if (selected[legenAllList.value[i]] === true) {
  300. legenSelectList.value.push(legenAllList.value[i]);
  301. }
  302. }
  303. isShowAll.value = legenSelectList.value.length === legenAllList.value.length;
  304. isIndeterminate.value =
  305. legenSelectList.value.length > 0 &&
  306. legenSelectList.value.length < legenAllList.value.length;
  307. };
  308. const handleResize = throttle(() => {
  309. nextTick(() => {
  310. myChart.value?.resize();
  311. });
  312. }, 500);
  313. const ToggleChangeAll = (value: boolean | string | number) => {
  314. const isChecked = value === true;
  315. if (isChecked) {
  316. legenSelectList.value = [...legenAllList.value];
  317. isIndeterminate.value = false;
  318. } else {
  319. legenSelectList.value = [];
  320. isIndeterminate.value = false;
  321. }
  322. LoadEchart();
  323. };
  324. // ================= Watch =================
  325. watch(
  326. () => props.data,
  327. () => {
  328. initLegenSelectList();
  329. LoadEchart();
  330. },
  331. { deep: true },
  332. );
  333. // ================= Lifecycle =================
  334. onMounted(() => {
  335. window.addEventListener("resize", handleResize);
  336. initLegenSelectList();
  337. LoadEchart();
  338. });
  339. onBeforeUnmount(() => {
  340. window.removeEventListener("resize", handleResize);
  341. if (myChart.value) {
  342. myChart.value.dispose();
  343. }
  344. });
  345. </script>
  346. <style lang="scss" scoped>
  347. </style>