|
@@ -1,11 +1,12 @@
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, onMounted, watch, computed, nextTick } from 'vue'
|
|
|
+import { nextTick, onMounted, ref, watch } from 'vue'
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
-import { Download, Grid, TrendCharts, DataLine } from '@element-plus/icons-vue'
|
|
|
+import { DataLine, Download, Grid, TrendCharts } from '@element-plus/icons-vue'
|
|
|
import assist from '/@/api/assist'
|
|
|
import { useLoading } from '/@/utils/loading-util'
|
|
|
import download from 'downloadjs'
|
|
|
import VueCharts from './VueCharts.vue'
|
|
|
+import { EChartsOption } from 'echarts'
|
|
|
|
|
|
const props = defineProps<{
|
|
|
uuid: string
|
|
@@ -58,41 +59,43 @@ const display = ref<'table' | 'bar' | 'line'>('table')
|
|
|
const chart = ref()
|
|
|
|
|
|
// 图表配置选项
|
|
|
-const chartOptions = ref('')
|
|
|
+const chartOptions = ref<EChartsOption>()
|
|
|
|
|
|
// 生成柱状图配置 - 统计每列数据的分布情况
|
|
|
-const generateBarChartOptions = () => {
|
|
|
- if (!data.value || !data.value.fields || !data.value.data) return ''
|
|
|
+const generateBarChartOptions: () => EChartsOption | undefined = () => {
|
|
|
+ if (!data.value || !data.value.fields || !data.value.data) return undefined
|
|
|
|
|
|
// 统计每个字段的数据分布
|
|
|
const fieldStats: { [field: string]: { [value: string]: number } } = {}
|
|
|
|
|
|
// 初始化统计对象
|
|
|
- data.value.fields.forEach(field => {
|
|
|
+ data.value.fields.forEach((field) => {
|
|
|
fieldStats[field] = {}
|
|
|
})
|
|
|
|
|
|
// 统计每个字段中每个值的出现次数
|
|
|
- data.value.data.forEach(row => {
|
|
|
- data.value!.fields.forEach(field => {
|
|
|
+ data.value.data.forEach((row) => {
|
|
|
+ data.value!.fields.forEach((field) => {
|
|
|
const value = row[field] || '空值'
|
|
|
fieldStats[field][value] = (fieldStats[field][value] || 0) + 1
|
|
|
})
|
|
|
})
|
|
|
|
|
|
// 准备图表数据
|
|
|
- const xAxisData = data.value.fields // 横坐标是表头(字段名)
|
|
|
+ const xAxisData = data.value.fields.map((field) => {
|
|
|
+ return field.length > 6 ? field.slice(0, 3) + '...' : field
|
|
|
+ }) // 横坐标是表头(字段名)
|
|
|
const seriesData: { [value: string]: number[] } = {}
|
|
|
|
|
|
// 收集所有可能的值
|
|
|
const allValues = new Set<string>()
|
|
|
- Object.values(fieldStats).forEach(fieldStat => {
|
|
|
- Object.keys(fieldStat).forEach(value => allValues.add(value))
|
|
|
+ Object.values(fieldStats).forEach((fieldStat) => {
|
|
|
+ Object.keys(fieldStat).forEach((value) => allValues.add(value))
|
|
|
})
|
|
|
|
|
|
// 为每个值创建一个系列
|
|
|
- Array.from(allValues).forEach(value => {
|
|
|
- seriesData[value] = data.value!.fields.map(field => fieldStats[field][value] || 0)
|
|
|
+ Array.from(allValues).forEach((value) => {
|
|
|
+ seriesData[value] = data.value!.fields.map((field) => fieldStats[field][value] || 0)
|
|
|
})
|
|
|
|
|
|
// 生成系列配置
|
|
@@ -100,149 +103,97 @@ const generateBarChartOptions = () => {
|
|
|
name: value,
|
|
|
type: 'bar',
|
|
|
data: counts,
|
|
|
- stack: 'total' // 使用堆叠柱状图更好地展示分布
|
|
|
+ stack: 'total', // 使用堆叠柱状图更好地展示分布,
|
|
|
}))
|
|
|
|
|
|
- const option = {
|
|
|
- title: {
|
|
|
- text: '数据分布统计',
|
|
|
- left: 'center'
|
|
|
- },
|
|
|
- tooltip: {
|
|
|
- trigger: 'axis',
|
|
|
- axisPointer: {
|
|
|
- type: 'shadow'
|
|
|
- },
|
|
|
- backgroundColor: 'rgba(50, 50, 50, 0.9)',
|
|
|
- borderColor: '#ccc',
|
|
|
- borderWidth: 1,
|
|
|
- textStyle: {
|
|
|
- color: '#fff',
|
|
|
- fontSize: 12
|
|
|
- },
|
|
|
- padding: [8, 12],
|
|
|
- extraCssText: 'max-width: 300px; word-wrap: break-word;',
|
|
|
- formatter: (params: any) => {
|
|
|
- if (!params || params.length === 0) return ''
|
|
|
- let result = `<div style="font-weight: bold; margin-bottom: 4px;">${params[0].axisValue}</div>`
|
|
|
- params.forEach((param: any) => {
|
|
|
- if (param.value > 0) { // 只显示有数据的项
|
|
|
- result += `<div style="margin: 2px 0;">${param.marker}<span style="margin-left: 4px;">${param.seriesName}: ${param.value}</span></div>`
|
|
|
- }
|
|
|
- })
|
|
|
- return result
|
|
|
- }
|
|
|
- },
|
|
|
- legend: {
|
|
|
- data: Object.keys(seriesData),
|
|
|
- top: 30,
|
|
|
- type: 'scroll' // 如果图例太多,使用滚动
|
|
|
- },
|
|
|
- grid: {
|
|
|
- left: '3%',
|
|
|
- right: '4%',
|
|
|
- bottom: '3%',
|
|
|
- containLabel: true
|
|
|
- },
|
|
|
+ return {
|
|
|
xAxis: {
|
|
|
type: 'category',
|
|
|
data: xAxisData,
|
|
|
- axisLabel: {
|
|
|
- rotate: 45 // 如果字段名太长,旋转显示
|
|
|
- }
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ show: true, // 显示图例
|
|
|
+ type: 'scroll', // 如果图例太多,可以加滚动
|
|
|
+ top: 'top', // 位置:上方
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ show: true,
|
|
|
+ trigger: 'item',
|
|
|
},
|
|
|
yAxis: {
|
|
|
type: 'value',
|
|
|
- name: '出现次数'
|
|
|
+ name: '出现次数',
|
|
|
},
|
|
|
- series: series
|
|
|
- }
|
|
|
-
|
|
|
- return encodeURIComponent(JSON.stringify(option))
|
|
|
+ series: series,
|
|
|
+ } as EChartsOption
|
|
|
}
|
|
|
|
|
|
-// 生成折线图配置 - 展示数据趋势(按行索引)
|
|
|
-const generateLineChartOptions = () => {
|
|
|
- if (!data.value || !data.value.fields || !data.value.data) return ''
|
|
|
+// 生成折线图配置 - 统计每列数据的分布情况(与柱状图算法同步)
|
|
|
+const generateLineChartOptions: () => EChartsOption | undefined = () => {
|
|
|
+ if (!data.value || !data.value.fields || !data.value.data) return undefined
|
|
|
|
|
|
- // 尝试找到数值型字段
|
|
|
- const numericFields = data.value.fields.filter(field => {
|
|
|
- return data.value!.data.some(row => {
|
|
|
- const value = row[field]
|
|
|
- return !isNaN(parseFloat(value)) && isFinite(parseFloat(value))
|
|
|
- })
|
|
|
+ // 统计每个字段的数据分布
|
|
|
+ const fieldStats: { [field: string]: { [value: string]: number } } = {}
|
|
|
+
|
|
|
+ // 初始化统计对象
|
|
|
+ data.value.fields.forEach((field) => {
|
|
|
+ fieldStats[field] = {}
|
|
|
})
|
|
|
|
|
|
- // 如果没有数值型字段,则显示每个字段的数据长度趋势
|
|
|
- const fieldsToShow = numericFields.length > 0 ? numericFields : data.value.fields
|
|
|
+ // 统计每个字段中每个值的出现次数
|
|
|
+ data.value.data.forEach((row) => {
|
|
|
+ data.value!.fields.forEach((field) => {
|
|
|
+ const value = row[field] || '空值'
|
|
|
+ fieldStats[field][value] = (fieldStats[field][value] || 0) + 1
|
|
|
+ })
|
|
|
+ })
|
|
|
|
|
|
- // X轴数据:行索引或者第一列数据
|
|
|
- const xAxisData = data.value.data.map((_, index) => `第${index + 1}行`)
|
|
|
+ // 准备图表数据
|
|
|
+ const xAxisData = data.value.fields.map((field) => {
|
|
|
+ return field.length > 6 ? field.slice(0, 3) + '...' : field
|
|
|
+ }) // 横坐标是表头(字段名)
|
|
|
+ const seriesData: { [value: string]: number[] } = {}
|
|
|
|
|
|
- // 生成系列数据
|
|
|
- const series = fieldsToShow.map(field => {
|
|
|
- const seriesData = data.value!.data.map(row => {
|
|
|
- const value = row[field]
|
|
|
- // 如果是数值型字段,直接使用数值
|
|
|
- if (numericFields.includes(field)) {
|
|
|
- return parseFloat(value) || 0
|
|
|
- }
|
|
|
- // 否则使用字符串长度
|
|
|
- return (value || '').toString().length
|
|
|
- })
|
|
|
+ // 收集所有可能的值
|
|
|
+ const allValues = new Set<string>()
|
|
|
+ Object.values(fieldStats).forEach((fieldStat) => {
|
|
|
+ Object.keys(fieldStat).forEach((value) => allValues.add(value))
|
|
|
+ })
|
|
|
|
|
|
- return {
|
|
|
- name: field,
|
|
|
- type: 'line',
|
|
|
- smooth: true,
|
|
|
- data: seriesData,
|
|
|
- connectNulls: false
|
|
|
- }
|
|
|
+ // 为每个值创建一个系列
|
|
|
+ Array.from(allValues).forEach((value) => {
|
|
|
+ seriesData[value] = data.value!.fields.map((field) => fieldStats[field][value] || 0)
|
|
|
})
|
|
|
|
|
|
- const option = {
|
|
|
- title: {
|
|
|
- text: numericFields.length > 0 ? '数值趋势图' : '字段长度趋势图',
|
|
|
- left: 'center'
|
|
|
- },
|
|
|
- tooltip: {
|
|
|
- trigger: 'axis',
|
|
|
- formatter: (params: any) => {
|
|
|
- let result = `${params[0].axisValue}<br/>`
|
|
|
- params.forEach((param: any) => {
|
|
|
- const suffix = numericFields.includes(param.seriesName) ? '' : ' (字符长度)'
|
|
|
- result += `${param.marker}${param.seriesName}: ${param.value}${suffix}<br/>`
|
|
|
- })
|
|
|
- return result
|
|
|
- }
|
|
|
- },
|
|
|
- legend: {
|
|
|
- data: fieldsToShow,
|
|
|
- top: 30,
|
|
|
- type: 'scroll'
|
|
|
- },
|
|
|
- grid: {
|
|
|
- left: '3%',
|
|
|
- right: '4%',
|
|
|
- bottom: '3%',
|
|
|
- containLabel: true
|
|
|
- },
|
|
|
+ // 生成系列配置
|
|
|
+ const series = Object.entries(seriesData).map(([value, counts]) => ({
|
|
|
+ name: value,
|
|
|
+ type: 'line',
|
|
|
+ smooth: true,
|
|
|
+ data: counts,
|
|
|
+ connectNulls: false,
|
|
|
+ }))
|
|
|
+
|
|
|
+ return {
|
|
|
xAxis: {
|
|
|
type: 'category',
|
|
|
- boundaryGap: false,
|
|
|
data: xAxisData,
|
|
|
- axisLabel: {
|
|
|
- rotate: 45
|
|
|
- }
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ show: true, // 显示图例
|
|
|
+ type: 'scroll', // 如果图例太多,可以加滚动
|
|
|
+ top: 'top', // 位置:上方
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ show: true,
|
|
|
+ trigger: 'axis',
|
|
|
},
|
|
|
yAxis: {
|
|
|
type: 'value',
|
|
|
- name: numericFields.length > 0 ? '数值' : '字符长度'
|
|
|
+ name: '出现次数',
|
|
|
},
|
|
|
- series: series
|
|
|
- }
|
|
|
-
|
|
|
- return encodeURIComponent(JSON.stringify(option))
|
|
|
+ series: series,
|
|
|
+ } as EChartsOption
|
|
|
}
|
|
|
|
|
|
// 监听显示模式变化,生成对应的图表配置
|
|
@@ -335,11 +286,7 @@ watch(display, async (newVal) => {
|
|
|
|
|
|
<!-- 图表视图 -->
|
|
|
<div v-else class="chart-wrapper">
|
|
|
- <VueCharts
|
|
|
- ref="chart"
|
|
|
- :data="chartOptions"
|
|
|
- v-if="chartOptions"
|
|
|
- />
|
|
|
+ <VueCharts ref="chart" :data="chartOptions" v-if="chartOptions" />
|
|
|
</div>
|
|
|
</div>
|
|
|
|