|
@@ -1,468 +1,382 @@
|
|
|
<template>
|
|
<template>
|
|
|
<div class="echart_content">
|
|
<div class="echart_content">
|
|
|
- <div class="is_show_all" v-if="showCheckBox">
|
|
|
|
|
|
|
+ <div class="is_show_all" v-if="props.showCheckBox">
|
|
|
<el-checkbox
|
|
<el-checkbox
|
|
|
v-model="isShowAll"
|
|
v-model="isShowAll"
|
|
|
:indeterminate="isIndeterminate"
|
|
:indeterminate="isIndeterminate"
|
|
|
- @change="toggleChangeAll"
|
|
|
|
|
|
|
+ @change="ToggleChangeAll"
|
|
|
>
|
|
>
|
|
|
显示全部
|
|
显示全部
|
|
|
</el-checkbox>
|
|
</el-checkbox>
|
|
|
</div>
|
|
</div>
|
|
|
<!-- 竖向堆叠条形图 -->
|
|
<!-- 竖向堆叠条形图 -->
|
|
|
- <div ref="barStackChartRef" class="chart_box"></div>
|
|
|
|
|
|
|
+ <div ref="barStackChart" class="chart_box"></div>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
-import { ref, watch, onMounted, onCreated, onUnmounted, nextTick, withDefaults } from 'vue'
|
|
|
|
|
-import _throttle from 'lodash/throttle'
|
|
|
|
|
-import * as echarts from 'echarts'
|
|
|
|
|
-import { getScorePerformanceAnalysis } from '@/utils/common'
|
|
|
|
|
-
|
|
|
|
|
-// ===================== 类型定义 =====================
|
|
|
|
|
-/** 提示框额外数据项 */
|
|
|
|
|
-interface TooltipExtraItem {
|
|
|
|
|
- label: string
|
|
|
|
|
- value: string | number
|
|
|
|
|
|
|
+import { ref, watch, onMounted, onBeforeUnmount, nextTick } from "vue";
|
|
|
|
|
+import * as echarts from "echarts";
|
|
|
|
|
+import throttle from "lodash/throttle";
|
|
|
|
|
+import { getScorePerformanceAnalysis } from "@/utils/common";
|
|
|
|
|
+
|
|
|
|
|
+// ================= Props & Emits =================
|
|
|
|
|
+interface Props {
|
|
|
|
|
+ showCheckBox?: boolean;
|
|
|
|
|
+ data?: (string | number)[][];
|
|
|
|
|
+ color?: string[];
|
|
|
|
|
+ isClick?: boolean;
|
|
|
|
|
+ legendList?: string[];
|
|
|
|
|
+ tooltipData?: any[];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/** 单条提示数据 */
|
|
|
|
|
-interface TooltipItem {
|
|
|
|
|
- list: TooltipExtraItem[]
|
|
|
|
|
- rateNum: string | number
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/** Props 类型 */
|
|
|
|
|
-interface BarStackChartProps {
|
|
|
|
|
- showCheckBox: boolean
|
|
|
|
|
- data: (string | number)[][]
|
|
|
|
|
- color: string[]
|
|
|
|
|
- isClick: boolean
|
|
|
|
|
- legendList: string[]
|
|
|
|
|
- tooltipData: TooltipItem[][]
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// ===================== 常量配置 =====================
|
|
|
|
|
-const CHART_DPR = 2
|
|
|
|
|
-const GRID_LEFT = 40
|
|
|
|
|
-const GRID_RIGHT = 60
|
|
|
|
|
-const GRID_HORIZONTAL_SUM = GRID_LEFT + GRID_RIGHT
|
|
|
|
|
-const BAR_MIN_WIDTH = 30
|
|
|
|
|
-const BAR_MAX_WIDTH = 50
|
|
|
|
|
-const DATA_ZOOM_HEIGHT = 8
|
|
|
|
|
-const THROTTLE_DELAY = 500
|
|
|
|
|
-const HIGHLIGHT_OPACITY = '30'
|
|
|
|
|
-const DEFAULT_CHART_HEIGHT = 400
|
|
|
|
|
-
|
|
|
|
|
-// ===================== Props 修复:纯TS类型 + withDefaults 设置默认值 =====================
|
|
|
|
|
-const props = withDefaults(defineProps<BarStackChartProps>(), {
|
|
|
|
|
|
|
+const props = withDefaults(defineProps<Props>(), {
|
|
|
showCheckBox: true,
|
|
showCheckBox: true,
|
|
|
data: () => [],
|
|
data: () => [],
|
|
|
color: () => [],
|
|
color: () => [],
|
|
|
isClick: false,
|
|
isClick: false,
|
|
|
legendList: () => [],
|
|
legendList: () => [],
|
|
|
- tooltipData: () => []
|
|
|
|
|
-})
|
|
|
|
|
|
|
+ tooltipData: () => [],
|
|
|
|
|
+});
|
|
|
|
|
|
|
|
-// ===================== 事件 =====================
|
|
|
|
|
const emit = defineEmits<{
|
|
const emit = defineEmits<{
|
|
|
- HandleChartClick: [index: number, name: string]
|
|
|
|
|
-}>()
|
|
|
|
|
-
|
|
|
|
|
-// ===================== 响应式变量 =====================
|
|
|
|
|
-const barStackChartRef = ref<HTMLDivElement | null>(null)
|
|
|
|
|
-let myChart: echarts.ECharts | null = null
|
|
|
|
|
-
|
|
|
|
|
-// 多选框状态
|
|
|
|
|
-const isShowAll = ref(true)
|
|
|
|
|
-const isIndeterminate = ref(false)
|
|
|
|
|
-const legendSelected = ref<Record<string, boolean>>({})
|
|
|
|
|
-const legenSelectList = ref<string[]>([])
|
|
|
|
|
-const legenAllList = ref<string[]>([])
|
|
|
|
|
-
|
|
|
|
|
-// 全局颜色池
|
|
|
|
|
-const colorsPool = ref<string[]>(getScorePerformanceAnalysis())
|
|
|
|
|
-
|
|
|
|
|
-// 窗口缩放节流
|
|
|
|
|
-const handleResize = _throttle(async () => {
|
|
|
|
|
- await nextTick()
|
|
|
|
|
- myChart?.resize()
|
|
|
|
|
-}, THROTTLE_DELAY)
|
|
|
|
|
-
|
|
|
|
|
-// ===================== 工具函数 =====================
|
|
|
|
|
-/**
|
|
|
|
|
- * 生成 DataZoom 滚动条配置
|
|
|
|
|
- */
|
|
|
|
|
-function getDataZoom(totalWidth: number, dataCount: number): echarts.DataZoomSliderOption | null {
|
|
|
|
|
- const contentWidth = totalWidth - GRID_HORIZONTAL_SUM
|
|
|
|
|
- const singleSeriesWidth = Math.ceil(contentWidth / dataCount)
|
|
|
|
|
-
|
|
|
|
|
- if (singleSeriesWidth >= BAR_MIN_WIDTH) return null
|
|
|
|
|
-
|
|
|
|
|
- const dataZoomNum = Math.floor(contentWidth / BAR_MIN_WIDTH)
|
|
|
|
|
- const dataZoomEnd = Math.floor((100 / dataCount) * dataZoomNum)
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- type: 'slider',
|
|
|
|
|
- xAxisIndex: [0],
|
|
|
|
|
- start: 0,
|
|
|
|
|
- end: dataZoomEnd,
|
|
|
|
|
- height: DATA_ZOOM_HEIGHT,
|
|
|
|
|
- left: 20,
|
|
|
|
|
- right: 20,
|
|
|
|
|
- bottom: 0,
|
|
|
|
|
- borderColor: 'transparent',
|
|
|
|
|
- borderCap: 'round',
|
|
|
|
|
- fillerColor: 'transparent',
|
|
|
|
|
- zoomLock: true,
|
|
|
|
|
- handleSize: '0',
|
|
|
|
|
- handleStyle: {
|
|
|
|
|
- color: '#b8b8b8',
|
|
|
|
|
- borderWidth: 2
|
|
|
|
|
- },
|
|
|
|
|
- backgroundColor: 'transparent',
|
|
|
|
|
- showDataShadow: false,
|
|
|
|
|
- showDetail: false,
|
|
|
|
|
- filterMode: 'filter'
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ (e: "HandleChartClick", index: number, xName: string): void;
|
|
|
|
|
+}>();
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * X轴文字超长截断处理
|
|
|
|
|
- */
|
|
|
|
|
-function formatXLabel(value: string, singleSeriesWidth: number): string {
|
|
|
|
|
- const charUnit = 14
|
|
|
|
|
- const textWidth = value.length * charUnit
|
|
|
|
|
|
|
+// ================= Refs & State =================
|
|
|
|
|
+const barStackChart = ref<HTMLElement | null>(null);
|
|
|
|
|
+const myChart = ref<echarts.ECharts | null>(null);
|
|
|
|
|
|
|
|
- if (textWidth <= singleSeriesWidth) return value
|
|
|
|
|
|
|
+const isShowAll = ref(true);
|
|
|
|
|
+const isIndeterminate = ref(false);
|
|
|
|
|
+const legendSelected = ref<Record<string, boolean>>({});
|
|
|
|
|
+const legenSelectList = ref<string[]>([]);
|
|
|
|
|
+const legenAllList = ref<string[]>([]);
|
|
|
|
|
|
|
|
- if (singleSeriesWidth < 100) {
|
|
|
|
|
- return textWidth > 80 ? `${value.slice(0, 2)}...${value.slice(-3)}` : value
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const maxLen = Math.floor(singleSeriesWidth / charUnit) - 1
|
|
|
|
|
- return value.slice(0, maxLen) + '...'
|
|
|
|
|
-}
|
|
|
|
|
|
|
+const defaultColors = getScorePerformanceAnalysis();
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * 图例选中状态更新
|
|
|
|
|
- */
|
|
|
|
|
-function updateLegendStatus() {
|
|
|
|
|
- isShowAll.value = legenSelectList.value.length === legenAllList.value.length
|
|
|
|
|
- isIndeterminate.value = legenSelectList.value.length > 0 && legenSelectList.value.length < legenAllList.value.length
|
|
|
|
|
-}
|
|
|
|
|
|
|
+// ================= Helper Methods =================
|
|
|
|
|
+const initLegenSelectList = () => {
|
|
|
|
|
+ if (props.legendList.length > 0) {
|
|
|
|
|
+ legenSelectList.value = props.legendList;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ legenSelectList.value = (props.data[0]?.slice(1) as string[]) || [];
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * 绑定柱子点击与高亮效果
|
|
|
|
|
- */
|
|
|
|
|
-function bindBarClick(totalWidth: number, dataCount: number) {
|
|
|
|
|
- if (!myChart) return
|
|
|
|
|
-
|
|
|
|
|
- const gridModel = myChart.getModel().getComponent('grid')
|
|
|
|
|
- const gridRect = gridModel.coordinateSystem.getRect()
|
|
|
|
|
- const xAxisDataLength = dataCount - 1
|
|
|
|
|
- const singleLabelWidth = gridRect.width / xAxisDataLength
|
|
|
|
|
-
|
|
|
|
|
- // 默认高亮第一项
|
|
|
|
|
- const firstItem = props.data[1]?.[0] ?? ''
|
|
|
|
|
- const defaultPixelPos = myChart.convertToPixel({ xAxisIndex: 0 }, firstItem)
|
|
|
|
|
- myChart.setOption({
|
|
|
|
|
- graphic: {
|
|
|
|
|
- id: 'highlight-box',
|
|
|
|
|
- type: 'rect',
|
|
|
|
|
- shape: {
|
|
|
|
|
- x: defaultPixelPos - singleLabelWidth / 2,
|
|
|
|
|
- y: gridRect.y,
|
|
|
|
|
- width: singleLabelWidth,
|
|
|
|
|
- height: gridRect.height
|
|
|
|
|
- },
|
|
|
|
|
- style: {
|
|
|
|
|
- fill: 'rgba(84,112,198,0.1)'
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
|
|
+// ================= Core Methods =================
|
|
|
|
|
+const LoadEchart = () => {
|
|
|
|
|
+ if (myChart.value) {
|
|
|
|
|
+ myChart.value.dispose();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!barStackChart.value) return;
|
|
|
|
|
|
|
|
- // 点击事件
|
|
|
|
|
- myChart.on('click', (params) => {
|
|
|
|
|
- if (params.seriesType !== 'bar') return
|
|
|
|
|
|
|
+ myChart.value = echarts.init(barStackChart.value, null, {
|
|
|
|
|
+ devicePixelRatio: 2,
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
- const pixelPos = myChart.convertToPixel({ xAxisIndex: 0 }, params.name)
|
|
|
|
|
- myChart.setOption({
|
|
|
|
|
- graphic: {
|
|
|
|
|
- id: 'highlight-box',
|
|
|
|
|
- type: 'rect',
|
|
|
|
|
- shape: {
|
|
|
|
|
- x: pixelPos - singleLabelWidth / 2,
|
|
|
|
|
- y: gridRect.y,
|
|
|
|
|
- width: singleLabelWidth,
|
|
|
|
|
- height: gridRect.height
|
|
|
|
|
- },
|
|
|
|
|
- style: {
|
|
|
|
|
- fill: (params.color as string) + HIGHLIGHT_OPACITY
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ if (props.data.length === 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- emit('HandleChartClick', params.dataIndex, params.name as string)
|
|
|
|
|
- })
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ const dataset = {
|
|
|
|
|
+ source: props.data,
|
|
|
|
|
+ };
|
|
|
|
|
+ const dataCount = props.data.length;
|
|
|
|
|
|
|
|
-// ===================== 核心:初始化图表 =====================
|
|
|
|
|
-function loadEchart() {
|
|
|
|
|
- const dom = barStackChartRef.value
|
|
|
|
|
- if (!dom) return
|
|
|
|
|
|
|
+ legenAllList.value = (props.data[0]?.slice(1) as string[]) || [];
|
|
|
|
|
|
|
|
- // 销毁旧实例,防止多实例叠加
|
|
|
|
|
- if (myChart) {
|
|
|
|
|
- myChart.dispose()
|
|
|
|
|
- myChart = null
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ legendSelected.value = (dataset.source[0] as string[])
|
|
|
|
|
+ .slice(1)
|
|
|
|
|
+ .reduce((acc: Record<string, boolean>, dim) => {
|
|
|
|
|
+ acc[dim] = false;
|
|
|
|
|
+ return acc;
|
|
|
|
|
+ }, {});
|
|
|
|
|
|
|
|
- myChart = echarts.init(dom, null, { devicePixelRatio: CHART_DPR })
|
|
|
|
|
- const sourceData = props.data
|
|
|
|
|
- if (sourceData.length === 0) return
|
|
|
|
|
|
|
+ legenSelectList.value.forEach((item) => {
|
|
|
|
|
+ legendSelected.value[item] = true;
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
- const dataCount = sourceData.length
|
|
|
|
|
- const headerRow = sourceData[0]
|
|
|
|
|
- const allLegend = headerRow.slice(1)
|
|
|
|
|
- legenAllList.value = allLegend
|
|
|
|
|
|
|
+ isShowAll.value = legenSelectList.value.length === legenAllList.value.length;
|
|
|
|
|
+ isIndeterminate.value =
|
|
|
|
|
+ legenSelectList.value.length > 0 &&
|
|
|
|
|
+ legenSelectList.value.length < legenAllList.value.length;
|
|
|
|
|
|
|
|
- // 初始化图例选中状态
|
|
|
|
|
- legendSelected.value = allLegend.reduce((acc, dim) => {
|
|
|
|
|
- acc[dim as string] = false
|
|
|
|
|
- return acc
|
|
|
|
|
- }, {} as Record<string, boolean>)
|
|
|
|
|
|
|
+ let totalWidth = barStackChart.value.clientWidth;
|
|
|
|
|
+ let singleSeriesWidth = Math.ceil((totalWidth - 140) / dataCount);
|
|
|
|
|
+ const barMinWidth = 30;
|
|
|
|
|
+ const dataZoomNum = Math.floor((totalWidth - 140) / (barMinWidth * 1));
|
|
|
|
|
+ const dataZoomEnd = Math.floor((100 / dataCount) * dataZoomNum);
|
|
|
|
|
|
|
|
- // 优先使用外部传入图例
|
|
|
|
|
- if (props.legendList.length > 0) {
|
|
|
|
|
- legenSelectList.value = [...props.legendList]
|
|
|
|
|
- } else {
|
|
|
|
|
- legenSelectList.value = [...allLegend]
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const dataZoom = {
|
|
|
|
|
+ start: 0,
|
|
|
|
|
+ end: dataZoomEnd,
|
|
|
|
|
+ type: "slider",
|
|
|
|
|
+ show: true,
|
|
|
|
|
+ borderColor: "transparent",
|
|
|
|
|
+ borderCap: "round",
|
|
|
|
|
+ xAxisIndex: [0],
|
|
|
|
|
+ height: 8,
|
|
|
|
|
+ left: 20,
|
|
|
|
|
+ right: 20,
|
|
|
|
|
+ bottom: 0,
|
|
|
|
|
+ fillerColor: "transparent",
|
|
|
|
|
+ zoomLock: true,
|
|
|
|
|
+ handleSize: "0",
|
|
|
|
|
+ handleStyle: { color: "#b8b8b8", borderWidth: 2 },
|
|
|
|
|
+ backgroundColor: "transparent",
|
|
|
|
|
+ showDataShadow: false,
|
|
|
|
|
+ showDetail: false,
|
|
|
|
|
+ filterMode: "filter",
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- // 同步选中状态
|
|
|
|
|
- legenSelectList.value.forEach(item => {
|
|
|
|
|
- legendSelected.value[item] = true
|
|
|
|
|
- })
|
|
|
|
|
- updateLegendStatus()
|
|
|
|
|
-
|
|
|
|
|
- // 计算柱子宽度 & 滚动条
|
|
|
|
|
- const totalWidth = dom.clientWidth
|
|
|
|
|
- const contentWidth = totalWidth - GRID_HORIZONTAL_SUM
|
|
|
|
|
- const singleSeriesWidth = Math.ceil(contentWidth / dataCount)
|
|
|
|
|
- const dataZoomOpt = getDataZoom(totalWidth, dataCount)
|
|
|
|
|
-
|
|
|
|
|
- // 构建 series
|
|
|
|
|
- const seriesList: echarts.SeriesBarOption[] = allLegend.map((dim, index) => {
|
|
|
|
|
- const color = props.color[index] || colorsPool.value[index]
|
|
|
|
|
- return {
|
|
|
|
|
- type: 'bar',
|
|
|
|
|
- stack: 'total',
|
|
|
|
|
- barMaxWidth: BAR_MAX_WIDTH,
|
|
|
|
|
- label: {
|
|
|
|
|
- show: singleSeriesWidth > 22,
|
|
|
|
|
- position: 'inside',
|
|
|
|
|
- color: '#fff',
|
|
|
|
|
- fontSize: 12,
|
|
|
|
|
- formatter: (params) => {
|
|
|
|
|
- const val = params.value?.[params.seriesIndex + 1]
|
|
|
|
|
- return val === 0 ? '' : `${val}%`
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- itemStyle: {
|
|
|
|
|
- color
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ const chartColors = props.color?.length > 0 ? props.color : defaultColors;
|
|
|
|
|
|
|
|
- // ECharts 配置项
|
|
|
|
|
const option: echarts.EChartsOption = {
|
|
const option: echarts.EChartsOption = {
|
|
|
- dataset: {
|
|
|
|
|
- source: sourceData
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ dataset: dataset,
|
|
|
tooltip: {
|
|
tooltip: {
|
|
|
|
|
+ triggerOn: "mousemove",
|
|
|
confine: true,
|
|
confine: true,
|
|
|
|
|
+ extraCssText:
|
|
|
|
|
+ "border-radius: 4px;padding:5px 0px 5px 5px;white-space:normal;word-warp:break-word;max-width: 400px;",
|
|
|
enterable: true,
|
|
enterable: true,
|
|
|
- extraCssText: 'border-radius: 4px;padding:5px 0 5px 5px;white-space:normal;word-wrap:break-word;max-width: 400px;',
|
|
|
|
|
- triggerOn: 'mousemove',
|
|
|
|
|
- formatter: (params) => {
|
|
|
|
|
- const firstParams = Array.isArray(params) ? params[0] : params
|
|
|
|
|
- const title = firstParams.name ?? ''
|
|
|
|
|
- const rateLabelList = headerRow.slice(1)
|
|
|
|
|
- const dataIndex = firstParams.dataIndex
|
|
|
|
|
-
|
|
|
|
|
- let tooltip = `<div class="tooltip_content"><div class="tooltip_title">${title}</div>`
|
|
|
|
|
-
|
|
|
|
|
- rateLabelList.forEach((label, i) => {
|
|
|
|
|
- const rate = firstParams.value?.[i + 1]
|
|
|
|
|
- const curTip = props.tooltipData[dataIndex]?.[i]
|
|
|
|
|
- const rateNum = curTip?.rateNum ?? '-'
|
|
|
|
|
- const tipList = curTip?.list ?? []
|
|
|
|
|
- const showTip = props.tooltipData.length > 0
|
|
|
|
|
-
|
|
|
|
|
- const rateText = rate === '-' ? '-' : `${rate}%`
|
|
|
|
|
- const color = props.color[i] || colorsPool.value[i]
|
|
|
|
|
-
|
|
|
|
|
- if (showTip) {
|
|
|
|
|
- tooltip += `<div class="tooltip_student">
|
|
|
|
|
- <span class="tooltip_rect_icon" style="background:${color}"></span>
|
|
|
|
|
- ${label}:${rateText},${rateNum}人
|
|
|
|
|
- </div>`
|
|
|
|
|
- // 最后一项追加额外提示
|
|
|
|
|
- if (i === rateLabelList.length - 1) {
|
|
|
|
|
- tipList.forEach(item => {
|
|
|
|
|
- tooltip += `<div class="tooltip_student">
|
|
|
|
|
- <span class="tooltip_rect_icon" style="background:#CCCCCC"></span>
|
|
|
|
|
- ${item.label}:${item.value}
|
|
|
|
|
- </div>`
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ formatter: (params: any) => {
|
|
|
|
|
+ const seriesName = params.name;
|
|
|
|
|
+ let tooltip = `<div class='tooltip_content'>`;
|
|
|
|
|
+ let title = seriesName;
|
|
|
|
|
+ tooltip += `<div class='tooltip_title'>${title}</div>`;
|
|
|
|
|
+
|
|
|
|
|
+ let rateLabel = (dataset.source[0] as string[]).slice(1);
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < rateLabel.length; i++) {
|
|
|
|
|
+ let labelName = rateLabel[i];
|
|
|
|
|
+ let rate = params.value[i + 1];
|
|
|
|
|
+ let list: any[] = [],
|
|
|
|
|
+ rateNum = "",
|
|
|
|
|
+ isShowTip = props.tooltipData?.length > 0;
|
|
|
|
|
+
|
|
|
|
|
+ if (props.tooltipData?.length > 0) {
|
|
|
|
|
+ const dataIndex = params.dataIndex;
|
|
|
|
|
+ const itemTooltip = props.tooltipData?.[dataIndex]?.[i] ?? "";
|
|
|
|
|
+ list = itemTooltip?.list ?? [];
|
|
|
|
|
+ rateNum = itemTooltip?.rateNum ?? "-";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (isShowTip) {
|
|
|
|
|
+ tooltip += `<div class='tooltip_student'><span class='tooltip_rect_icon' style='background:${chartColors[i]}'></span>${labelName}:${rate == "-" ? "-" : rate + "%"},${rateNum}人</div>`;
|
|
|
|
|
+ if (i == rateLabel.length - 1) {
|
|
|
|
|
+ for (let j = 0; j < list.length; j++) {
|
|
|
|
|
+ tooltip += `<div class='tooltip_student'><span class='tooltip_rect_icon' style='background:#CCCCCC'></span>${list[j].label}:${list[j].value}</div>`;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
- tooltip += `<div class="tooltip_student">
|
|
|
|
|
- <span class="tooltip_rect_icon" style="background:${color}"></span>
|
|
|
|
|
- ${label}:${rateText}
|
|
|
|
|
- </div>`
|
|
|
|
|
|
|
+ tooltip += `<div class='tooltip_student'><span class='tooltip_rect_icon' style='background:${chartColors[i]}'></span>${labelName}:${rate == "-" ? "-" : rate + "%"}</div>`;
|
|
|
}
|
|
}
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- tooltip += '</div>'
|
|
|
|
|
- return tooltip
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ tooltip += `</div>`;
|
|
|
|
|
+ return tooltip;
|
|
|
|
|
+ },
|
|
|
},
|
|
},
|
|
|
legend: {
|
|
legend: {
|
|
|
show: true,
|
|
show: true,
|
|
|
- left: props.showCheckBox ? '110px' : '0px',
|
|
|
|
|
|
|
+ top: '6px',
|
|
|
|
|
+ left: props.showCheckBox ? "110px" : "0px",
|
|
|
itemGap: 20,
|
|
itemGap: 20,
|
|
|
itemHeight: 10,
|
|
itemHeight: 10,
|
|
|
itemWidth: 20,
|
|
itemWidth: 20,
|
|
|
- textStyle: { fontSize: 12, color: '#333' },
|
|
|
|
|
|
|
+ textStyle: { fontSize: 12, color: "#333" },
|
|
|
selectedMode: true,
|
|
selectedMode: true,
|
|
|
- selected: legendSelected.value
|
|
|
|
|
|
|
+ selected: legendSelected.value,
|
|
|
|
|
+ formatter: (name: string) => name,
|
|
|
},
|
|
},
|
|
|
grid: {
|
|
grid: {
|
|
|
top: 50,
|
|
top: 50,
|
|
|
- left: GRID_LEFT,
|
|
|
|
|
- right: GRID_RIGHT,
|
|
|
|
|
|
|
+ left: 40,
|
|
|
|
|
+ right: 60,
|
|
|
bottom: 0,
|
|
bottom: 0,
|
|
|
- containLabel: true
|
|
|
|
|
|
|
+ containLabel: true,
|
|
|
},
|
|
},
|
|
|
- dataZoom: dataZoomOpt,
|
|
|
|
|
|
|
+ dataZoom: singleSeriesWidth < barMinWidth ? dataZoom : null,
|
|
|
yAxis: {
|
|
yAxis: {
|
|
|
- type: 'value',
|
|
|
|
|
|
|
+ type: "value",
|
|
|
axisLabel: {
|
|
axisLabel: {
|
|
|
- formatter: '{value}%',
|
|
|
|
|
|
|
+ formatter: "{value}%",
|
|
|
fontSize: 14,
|
|
fontSize: 14,
|
|
|
- color: '#666',
|
|
|
|
|
- fontWeight: 400
|
|
|
|
|
|
|
+ color: "#666",
|
|
|
|
|
+ fontWeight: 400,
|
|
|
},
|
|
},
|
|
|
axisTick: {
|
|
axisTick: {
|
|
|
- lineStyle: {
|
|
|
|
|
- color: 'red',
|
|
|
|
|
- type: 'dashed',
|
|
|
|
|
- width: 1
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ lineStyle: { color: "red", type: "dashed", width: 1 },
|
|
|
|
|
+ },
|
|
|
},
|
|
},
|
|
|
xAxis: {
|
|
xAxis: {
|
|
|
- type: 'category',
|
|
|
|
|
|
|
+ type: "category",
|
|
|
axisLabel: {
|
|
axisLabel: {
|
|
|
fontSize: 14,
|
|
fontSize: 14,
|
|
|
- color: '#666',
|
|
|
|
|
|
|
+ color: "#666",
|
|
|
fontWeight: 400,
|
|
fontWeight: 400,
|
|
|
interval: 0,
|
|
interval: 0,
|
|
|
rotate: singleSeriesWidth < 80 ? 45 : 0,
|
|
rotate: singleSeriesWidth < 80 ? 45 : 0,
|
|
|
- formatter: (val) => formatXLabel(val, singleSeriesWidth)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ formatter: function (value: string) {
|
|
|
|
|
+ const valueWidth = value.length * 14;
|
|
|
|
|
+ if (valueWidth > singleSeriesWidth) {
|
|
|
|
|
+ if (singleSeriesWidth < 100) {
|
|
|
|
|
+ if (valueWidth > 80) {
|
|
|
|
|
+ return value.slice(0, 2) + "..." + value.slice(-3);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return value;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ let maxLength = Math.floor(singleSeriesWidth / 14) - 1;
|
|
|
|
|
+ return value.slice(0, maxLength) + "...";
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return value;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ tooltip: {
|
|
|
|
|
+ show: true,
|
|
|
|
|
+ formatter: function (params: any) {
|
|
|
|
|
+ return params.value;
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
},
|
|
},
|
|
|
- series: seriesList
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- myChart.setOption(option)
|
|
|
|
|
|
|
+ series: (dataset.source[0] as string[]).slice(1).map(function (dim, index) {
|
|
|
|
|
+ const color = chartColors[index];
|
|
|
|
|
+ return {
|
|
|
|
|
+ type: "bar",
|
|
|
|
|
+ stack: "total",
|
|
|
|
|
+ barMaxWidth: 50,
|
|
|
|
|
+ label: {
|
|
|
|
|
+ show: singleSeriesWidth > 22,
|
|
|
|
|
+ position: "inside",
|
|
|
|
|
+ color: "#fff",
|
|
|
|
|
+ fontSize: 12,
|
|
|
|
|
+ formatter: function (params: any) {
|
|
|
|
|
+ if (params.value[params.seriesIndex + 1] == 0) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return params.value[params.seriesIndex + 1] + "%";
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ itemStyle: { color: color },
|
|
|
|
|
+ };
|
|
|
|
|
+ }),
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- // 图例切换监听
|
|
|
|
|
- myChart.on('legendselectchanged', handleLegendSelectChanged)
|
|
|
|
|
|
|
+ myChart.value.setOption(option);
|
|
|
|
|
+ myChart.value.on("legendselectchanged", HandleLegendSelectChanged);
|
|
|
|
|
|
|
|
- // 开启柱子点击事件
|
|
|
|
|
if (props.isClick) {
|
|
if (props.isClick) {
|
|
|
- bindBarClick(totalWidth, dataCount)
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ // const containerWidth = barStackChart.value.offsetWidth;
|
|
|
|
|
+ const xAxisDataLength = props.data.length - 1;
|
|
|
|
|
+ const gridRect = myChart.value
|
|
|
|
|
+ .getModel()
|
|
|
|
|
+ .getComponent("grid")
|
|
|
|
|
+ .coordinateSystem.getRect();
|
|
|
|
|
+ const singleLabelWidth = gridRect.width / xAxisDataLength;
|
|
|
|
|
+
|
|
|
|
|
+ myChart.value.on("click", (params: any) => {
|
|
|
|
|
+ if (params.seriesType === "bar") {
|
|
|
|
|
+ let pixelPosition = myChart.value!.convertToPixel(
|
|
|
|
|
+ { xAxisIndex: 0 },
|
|
|
|
|
+ params.name,
|
|
|
|
|
+ );
|
|
|
|
|
+ myChart.value!.setOption({
|
|
|
|
|
+ graphic: {
|
|
|
|
|
+ id: "highlight-box",
|
|
|
|
|
+ type: "rect",
|
|
|
|
|
+ shape: {
|
|
|
|
|
+ x: pixelPosition - singleLabelWidth / 2,
|
|
|
|
|
+ y: gridRect.y,
|
|
|
|
|
+ width: singleLabelWidth,
|
|
|
|
|
+ height: gridRect.height,
|
|
|
|
|
+ },
|
|
|
|
|
+ style: { fill: params.color + "30" },
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ emit("HandleChartClick", params.dataIndex, params.name);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
-// ===================== 事件处理 =====================
|
|
|
|
|
-/**
|
|
|
|
|
- * 图例切换事件
|
|
|
|
|
- */
|
|
|
|
|
-function handleLegendSelectChanged(params: echarts.LegendSelectChangedParams) {
|
|
|
|
|
- const selected = params.selected
|
|
|
|
|
- legenSelectList.value = []
|
|
|
|
|
-
|
|
|
|
|
- legenAllList.value.forEach(item => {
|
|
|
|
|
- if (selected[item]) {
|
|
|
|
|
- legenSelectList.value.push(item)
|
|
|
|
|
|
|
+ let defaultPixelPosition = myChart.value.convertToPixel(
|
|
|
|
|
+ { xAxisIndex: 0 },
|
|
|
|
|
+ props.data[1][0],
|
|
|
|
|
+ );
|
|
|
|
|
+ myChart.value.setOption({
|
|
|
|
|
+ graphic: {
|
|
|
|
|
+ id: "highlight-box",
|
|
|
|
|
+ type: "rect",
|
|
|
|
|
+ shape: {
|
|
|
|
|
+ x: defaultPixelPosition - singleLabelWidth / 2,
|
|
|
|
|
+ y: gridRect.y,
|
|
|
|
|
+ width: singleLabelWidth,
|
|
|
|
|
+ height: gridRect.height,
|
|
|
|
|
+ },
|
|
|
|
|
+ style: { fill: "rgba(84,112,198,0.1)" },
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const HandleLegendSelectChanged = (params: any) => {
|
|
|
|
|
+ const selected = params.selected;
|
|
|
|
|
+ legenSelectList.value = [];
|
|
|
|
|
+ for (let i = 0; i < legenAllList.value.length; i++) {
|
|
|
|
|
+ if (selected[legenAllList.value[i]] === true) {
|
|
|
|
|
+ legenSelectList.value.push(legenAllList.value[i]);
|
|
|
}
|
|
}
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- updateLegendStatus()
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 显示全部 复选框切换
|
|
|
|
|
- */
|
|
|
|
|
-function toggleChangeAll(val: boolean) {
|
|
|
|
|
- if (val) {
|
|
|
|
|
- legenSelectList.value = [...legenAllList.value]
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ isShowAll.value = legenSelectList.value.length === legenAllList.value.length;
|
|
|
|
|
+ isIndeterminate.value =
|
|
|
|
|
+ legenSelectList.value.length > 0 &&
|
|
|
|
|
+ legenSelectList.value.length < legenAllList.value.length;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const handleResize = throttle(() => {
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ myChart.value?.resize();
|
|
|
|
|
+ });
|
|
|
|
|
+}, 500);
|
|
|
|
|
+
|
|
|
|
|
+const ToggleChangeAll = (value: boolean | string | number) => {
|
|
|
|
|
+ const isChecked = value === true;
|
|
|
|
|
+ if (isChecked) {
|
|
|
|
|
+ legenSelectList.value = [...legenAllList.value];
|
|
|
|
|
+ isIndeterminate.value = false;
|
|
|
} else {
|
|
} else {
|
|
|
- legenSelectList.value = []
|
|
|
|
|
|
|
+ legenSelectList.value = [];
|
|
|
|
|
+ isIndeterminate.value = false;
|
|
|
}
|
|
}
|
|
|
- isIndeterminate.value = false
|
|
|
|
|
- loadEchart()
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ LoadEchart();
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
-// ===================== 监听 & 生命周期 =====================
|
|
|
|
|
-// 数据源变化重绘图表
|
|
|
|
|
|
|
+// ================= Watch =================
|
|
|
watch(
|
|
watch(
|
|
|
() => props.data,
|
|
() => props.data,
|
|
|
() => {
|
|
() => {
|
|
|
- loadEchart()
|
|
|
|
|
|
|
+ initLegenSelectList();
|
|
|
|
|
+ LoadEchart();
|
|
|
},
|
|
},
|
|
|
- { deep: true }
|
|
|
|
|
-)
|
|
|
|
|
|
|
+ { deep: true },
|
|
|
|
|
+);
|
|
|
|
|
|
|
|
|
|
+// ================= Lifecycle =================
|
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
|
- window.addEventListener('resize', handleResize)
|
|
|
|
|
- loadEchart()
|
|
|
|
|
-})
|
|
|
|
|
-
|
|
|
|
|
-onUnmounted(() => {
|
|
|
|
|
- // 清除监听 & 销毁实例,防止内存泄漏
|
|
|
|
|
- window.removeEventListener('resize', handleResize)
|
|
|
|
|
- if (myChart) {
|
|
|
|
|
- myChart.dispose()
|
|
|
|
|
- myChart = null
|
|
|
|
|
|
|
+ window.addEventListener("resize", handleResize);
|
|
|
|
|
+ initLegenSelectList();
|
|
|
|
|
+ LoadEchart();
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+onBeforeUnmount(() => {
|
|
|
|
|
+ window.removeEventListener("resize", handleResize);
|
|
|
|
|
+ if (myChart.value) {
|
|
|
|
|
+ myChart.value.dispose();
|
|
|
}
|
|
}
|
|
|
-})
|
|
|
|
|
|
|
+});
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
<style lang="scss" scoped>
|
|
|
-.echart_content {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.is_show_all {
|
|
|
|
|
- margin-bottom: 8px;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.chart_box {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: v-bind(DEFAULT_CHART_HEIGHT + 'px');
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-:deep(.tooltip_rect_icon) {
|
|
|
|
|
- display: inline-block;
|
|
|
|
|
- width: 8px;
|
|
|
|
|
- height: 8px;
|
|
|
|
|
- border-radius: 2px;
|
|
|
|
|
- margin-right: 6px;
|
|
|
|
|
-}
|
|
|
|
|
-</style>
|
|
|
|
|
|
|
+</style>
|