从excel提取和过滤数据到echarts中绘制图
主页面
介绍
echarts的事例页面,导入数据比较麻烦,此项目从excel中提取数据(含过滤数据),以注入页面.
代码说明
- 所有的需要从excel中读取的参数,从代码中替换.需以{{data}} 包含在内
- 使用绘制参数的解析代码参数可以解析出来所有参数
- 数据配置上传文件后,可以选择列
- 数据过滤条件设定后,点击绘制,此时才会过滤数据
- 代码块实际是返回echarts.options的信息,下面给了多个事例
- 图表设置实际没有使用,需要后端支持才可以,将编辑好的代码保存到数据库,或者取出
- 其他的参数,例如绘制参数,建议直接编辑代码完成,并且建议以变量的形式填写在最上方,以提供出来
页面代码 commonChart.vue
<template>
<div class="app-container" style="height: 100vh;">
<el-row style="height: 100%;">
<el-col :span="10" style="height: 100%;">
<el-tabs type="border-card" style="height: 100%;">
<el-tab-pane label="图表设置" style="height: 100%;">
<el-form ref="elForm" :model="chartForm" label-width="80px" style="height: 100%;">
<el-form-item label="选择图表" prop="chartType" >
<el-select v-model="chartForm.chartType" placeholder="请选择选择图表" clearable
:style="{width: '100%'}">
<el-option v-for="(item, index) in chartFormOptions" :key="index" :label="item.label"
:value="item.value" :disabled="item.disabled"></el-option>
</el-select>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="代码编辑" style="height: 100%;">
<vue-ace-editor v-model="chartForm.customChartCode" ref="aceEditor"
:width="'100%'"
:height="'100%'"
mode='javascript'
theme='github'/>
</el-tab-pane>
<el-tab-pane label="数据配置">
<el-form :model="dataForm" ref="queryForm" :inline="false" label-width="80px">
<el-form-item label="上传文件">
<el-upload
class="upload-demo"
action=""
:before-upload="handleBeforeUpload"
:file-list="dataForm.fileList"
:show-file-list="false">
<el-button size="mini" slot="trigger" type="primary">XLSX文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="数据过滤">
<el-table :data="dataForm.dataFilter" style="width: 100%">
<!-- 选择变量 -->
<el-table-column label="选择变量" prop="variable">
<template v-slot="{ row, $index }">
<el-select v-model="row.variable" placeholder="请选择变量">
<el-option
v-for="(item, index) in dataHeader"
:key="index"
:label="item"
:value="index"
></el-option>
</el-select>
</template>
</el-table-column>
<!-- 选择判断关系 -->
<el-table-column label="选择判断关系" prop="relation">
<template v-slot="{ row, $index }">
<el-select v-model="row.relation" placeholder="请选择关系">
<el-option label="等于" value="equals"></el-option>
<el-option label="不等于" value="not_equals"></el-option>
<el-option label="大于" value="greater_than"></el-option>
<el-option label="小于" value="less_than"></el-option>
</el-select>
</template>
</el-table-column>
<!-- 选择判断值 -->
<el-table-column label="选择判断值" prop="value">
<template v-slot="{ row, $index }">
<el-select v-if="uniqueData[row.variable] && uniqueData[row.variable].length<10"
v-model="row.value"
placeholder="请选择值">
<el-option
v-for="(item, index) in uniqueData[row.variable]"
:key="index"
:label="item"
:value="item"
></el-option>
</el-select>
<el-input v-else v-model="row.value">
</el-input>
</template>
</el-table-column>
<el-table-column label="操作" >
<template v-slot="scope">
<el-button type="primary" @click="addRow" size="small">+</el-button>
<el-button type="danger" @click="deleteRow(scope.$index)" size="small">x</el-button>
</template>
</el-table-column>
</el-table>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="绘制参数">
<el-form ref="elForm" :model="drawForm" label-width="180px">
<el-form-item label="操作">
<el-button type="primary" icon="el-icon-search" size="mini" @click="analyzeCode()">解析代码参数
</el-button>
</el-form-item>
<el-form-item
v-for="(item, index) in customStatisticalDataColumnName"
:key="index"
:label="`统计参数:${item}`">
<el-select
v-model="drawForm.statisticalDataColumnCustom[index]"
placeholder="请选择统计数据列"
>
<el-option
v-for="(item, index) in dataHeader"
:key="index"
:label="item"
:value="index"
></el-option>
</el-select>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</el-col>
<el-col :span="14" style="height: 100%;">
<el-card style="height: 100%;">
<div slot="header" class="clearfix">
<el-button type="primary" icon="el-icon-search" size="medium" @click="redrawChart()">绘制</el-button>
</div>
<div id="chart" style="height: 100%;width: 100%"/>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
import * as echarts from "echarts";
import * as XLSX from 'xlsx';
import VueAceEditor from 'vue2-ace-editor';
import 'brace/mode/javascript'; // 引入 JavaScript 语言模式
import 'brace/theme/chrome';
export default {
components: {
VueAceEditor,
},
data() {
return {
dialogVisible: true,
// 图表配置参数
chartForm: {
//图表类型
chartType: "",
//自定义图表-代码块
customChartCode: null,
},
// 数据配置参数
dataForm: {
fileList: [], // 存储上传的文件列表
//过滤条件
dataFilter: [
{
variable: "",
relation: "",
value: ""
}
]
},
// 绘制参数
drawForm: {
statisticalDataColumnCustom: []
},
//自定义图表的配置参数列表
customStatisticalDataColumnName: [],
chartFormOptions: [
{
"label": "自定义图表",
"value": 0
},
{
"label": "正态分布图",
"value": 1
}, {
"label": "散点图",
"value": 2
}
],
dataHeader: [],
dataBody: [],
uniqueData: [],
filterData: [],
validConditions: [],
chart: null
};
},
mounted() {
this.initChart();
this.$nextTick(() => {
const aceEditor = this.$refs.aceEditor.editor;
if (aceEditor) {
console.log('Editor Loaded:', aceEditor); // 确认编辑器加载成功
aceEditor.getSession().setMode('ace/mode/javascript'); // 强制设置语言模式
aceEditor.setTheme('ace/theme/chrome'); // 强制设置主题
aceEditor.setFontSize('14px'); // 设置字体大小
aceEditor.setHighlightActiveLine(true); // 设置是否高亮活动行
}
});
},
methods: {
onEditorLoaded(editor) {
console.log('Editor Loaded:', editor); // 检查 editor 对象
if (editor) {
console.log('Ace Mode:', editor.getSession().getMode().$id); // 确认是否加载了 JavaScript 模式
console.log('Ace Theme:', editor.getTheme()); // 确认主题
}
},
analyzeCode() {
//提取含{{}}的字符列表["{{d1}}", "{{d2}}"]
let matchesName = this.chartForm.customChartCode.match(/\{\{(.*?)\}\}/g);
let matchesNameLabel = []
if (matchesName) {
// 去除 {{ 和 }}列表
matchesNameLabel = matchesName.map(match => match.slice(2, -2));
}
this.customStatisticalDataColumnName = matchesNameLabel;
this.$message({message: '解析成功,提取自定义统计参数' + matchesNameLabel, type: 'warning'})
},
addRow() {
// 添加一行数据到表单中
this.dataForm.dataFilter.push({variable: '', relation: '', value: ''});
},
deleteRow(index) {
// 删除对应的行
this.dataForm.dataFilter.splice(index, 1);
},
handleBeforeUpload(file) {
const reader = new FileReader()
reader.onload = (e) => {
const data = e.target.result;
// 解析xlsx文件
const workbook = XLSX.read(data, {type: 'binary'});
// 获取第一个工作表
const sheetName = workbook.SheetNames[0];
const sheet = workbook.Sheets[sheetName];
// 原始
let data_json = XLSX.utils.sheet_to_json(sheet, {header: 1, defval: null});
// 取出第一行作为表头
this.dataHeader = data_json.shift(); // 删除并返回数组的第一个元素
// 剩余的行作为表体
this.dataBody = data_json;
this.buildDynamicForms();
this.$message({message: '解析文件成功,获取记录:' + this.dataBody.length + "条", type: 'warning'})
// this.drawChart();
};
// 读取文件为二进制字符串
reader.readAsBinaryString(file);
return false; // 阻止默认的上传行为
},
buildDynamicForms() {
//获取每列的distance值
let headLength = this.dataHeader.length;
for (let i = 0; i < headLength; i++) {
this.uniqueData[i] = this.getUniqueColumn(this.dataBody, i);
}
},
getUniqueColumn(data, index) {
// 检查数据是否为空
if (!data || !Array.isArray(data) || data.length === 0) {
return [];
}
// 如果数据是二维数组
if (Array.isArray(data[0])) {
// 提取指定列并去重
const column = data.map(row => row[index]);
return [...new Set(column)];
}
// 如果数据是对象数组
if (typeof data[0] === 'object') {
// 提取指定列(即对象的属性)并去重
const column = data.map(item => item[index]);
return [...new Set(column)];
}
// 如果数据格式不匹配
return [];
},
redrawChart() {
this.filterDataByConditions();
let opt = this.chartForm.customChartCode;
let mapping = this.drawForm.statisticalDataColumnCustom;
for (let i = 0; i < mapping.length; i++) {
let data = this.filterData.map(row => row[mapping[i]]);
opt = opt.replace("{{" + this.customStatisticalDataColumnName[i] + "}}", JSON.stringify(data));
}
let rx = (new Function(opt))();
console.log(rx)
this.chart.clear()
this.chart.setOption(rx)
},
initChart() {
let chartDom = document.getElementById("chart");
this.chart = echarts.init(chartDom);
},
filterDataByConditions() {
this.filterData = this.dataBody.filter(item => {
this.validConditions = this.dataForm.dataFilter.filter(c => c.value !== "");
return this.validConditions.every(row => {
let rowKey = row.variable;
let operator = row.relation;
let rowValue = row.value;
switch (operator) {
case "equals":
return item[rowKey] == rowValue;
case "not_equals":
return item[rowKey] != rowValue;
case "greater_than":
return item[rowKey] > rowValue;
case "less_than":
return item[rowKey] < rowValue;
default:
throw new Error(`Unknown operator: ${operator}`);
}
});
});
},
}
}
;
</script>
<style scoped>
/* 或者使用 ::v-deep */
::v-deep .el-dialog__wrapper {
pointer-events: none;
}
::v-deep .el-dialog__body {
padding-bottom: 5px
}
::v-deep .el-dialog {
pointer-events: auto;
}
::v-deep .el-card__body{
height: 100%;
}
::v-deep .el-tabs__content{
height: 90% !important;
}
</style>
代码事例
事例1 单散点分布 singleScatterDistribution
// 单散点分布 singleScatterDistribution
/*******************配置参数*******************/
//第一组的x,y,和size,都是一维数组
let dx = {{dx}};
let dy = {{dy}};
let dSize = {{dSize}};
/*******************计算实体********************/
let dataX = dx.map((value, index) => [value, dy[index], dSize[index]]);
return {
toolbox: {
show: true,
feature: {
dataZoom: {
yAxisIndex: 'none'
},
dataView: {readOnly: false},
restore: {},
saveAsImage: {}
}
},
xAxis: {},
yAxis: {},
legend: {
data: ['数据名称'] // 图例项,对应 series 中的 name
},
dataZoom: [
{
type: 'slider',
show: true,
yAxisIndex: [0],
//不过滤数据,只改变数轴范围。
filterMode: 'none'
},
{
type: 'inside',
yAxisIndex: [0],
filterMode: 'none'
},
//x缩放轴
{
type: 'slider',
show: true,
xAxisIndex: [0],
filterMode: 'none',
height: 20
},
{
type: 'inside',
xAxisIndex: [0],
filterMode: 'none'
}
],
series: [
{
name: '数据名称',
symbolSize: function (data) {
//return data[2]/100; // 返回 data 中的第三个值(size)
return 7;
},
data: dataX,
type: 'scatter',
itemStyle: {
color: '#5470C6' // 设置第一组数据的颜色
}
}
]
}
事例2 双散点分布 doubleScatterDistribution
// 双散点分布 doubleScatterDistribution
/*******************配置参数*******************/
//第一组的x,y,和size,都是一维数组
let d1x = {{d1.x}};
let d1y = {{d1.y}};
let d1size = {{d1.size}};
//第二组的x,y,和size,都是一维数组
let d2x = {{d1.x}};
let d2y = {{d1.y}};
let d2size = {{d1.size}};
/*******************计算实体********************/
let d1 = d1x.map((value, index) => [value, d1y[index], d1size[index]]);
let d2 = d2x.map((value, index) => [value, d2y[index], d2size[index]]);
return {
toolbox: {
show: true,
feature: {
dataZoom: {
yAxisIndex: 'none'
},
dataView: {readOnly: false},
restore: {},
saveAsImage: {}
}
},
xAxis: {},
yAxis: {},
legend: {
data: ['新数据', '旧数据'] // 图例项,对应 series 中的 name
},
dataZoom: [
{
type: 'slider',
show: true,
yAxisIndex: [0],
//不过滤数据,只改变数轴范围。
filterMode: 'none'
},
{
type: 'inside',
yAxisIndex: [0],
filterMode: 'none'
},
//x缩放轴
{
type: 'slider',
show: true,
xAxisIndex: [0],
filterMode: 'none',
height: 20
},
{
type: 'inside',
xAxisIndex: [0],
filterMode: 'none'
}
],
series: [
{
name: '新数据',
symbolSize: function (data) {
//return data[2]/100; // 返回 data 中的第三个值(size)
return 7;
},
data: d1,
type: 'scatter',
itemStyle: {
color: '#5470C6' // 设置第一组数据的颜色
}
},
{
name: '旧数据',
symbolSize: function (data) {
//return data[2] + 5; // 返回 data 中的第三个值(size)
return 7;
},
data: d2,
type: 'scatter',
itemStyle: {
color: '#EE6666' // 设置第二组数据的颜色
}
}
]
}
事例3 单正态分布图 singleNormalDistribution
// 单正态分布图 singleNormalDistribution
/*******************配置参数*******************/
// 组间距
let interval = 0.1;
// 参数实例 [1,2,3,4,5]
let da = {{data}}
// 标题
let title = "这里填写标题"
//图表颜色
let drawColor = "rgba(19,72,206,0.5)"
/*******************计算实体********************/
//计算正态曲线
function fxNormalDistribution(array, std, mean) {
let valueList = [];
for (let i = 0; i < array.length; i++) {
let ND =
Math.sqrt(2 * Math.PI) *
std *
Math.pow(
Math.E,
-(Math.pow(array[i] - mean, 2) / (2 * Math.pow(std, 2)))
);
valueList.push(ND.toFixed(3));
}
return valueList;
}
//计算标准差
function getStd(data, mean) {
let sumXY = function (x, y) {
return Number(x) + Number(y);
};
let square = function (x) {
return Number(x) * Number(x);
};
let deviations = data.map(function (x) {
return x - mean;
});
return Math.sqrt(deviations.map(square).reduce(sumXY) / (data.length - 1));
}
//对有序数组求中位数
function getMedianSorted(arr) {
// 获取数组长度
let len = arr.length;
// 如果没有元素,返回undefined或你可以返回其他合适的值
if (len === 0) {
return undefined;
}
// 如果只有一个元素,那么它就是中位数
if (len === 1) {
return arr[0];
}
// 如果数组长度是奇数,返回中间的数
if (len % 2 === 1) {
return arr[Math.floor(len / 2)];
}
// 如果数组长度是偶数,返回中间两个数的平均值
else {
let mid1 = arr[len / 2 - 1];
let mid2 = arr[len / 2];
return (mid1 + mid2) / 2.0;
}
}
function getUniqueColumn(data, index) {
// 检查数据是否为空
if (!data || !Array.isArray(data) || data.length === 0) {
return [];
}
// 如果数据是二维数组
if (Array.isArray(data[0])) {
// 提取指定列并去重
const column = data.map(row => row[index]);
return [...new Set(column)];
}
// 如果数据是对象数组
if (typeof data[0] === 'object') {
// 提取指定列(即对象的属性)并去重
const column = data.map(item => item[index]);
return [...new Set(column)];
}
// 如果数据格式不匹配
return [];
}
function getInterval(va, xAxisX) {
for (let i = 0; i < xAxisX.length; i++) {
if (xAxisX[i] > va) {
if (i === 0) {
return xAxisX[0].toString();
} else {
return xAxisX[i - 1].toString();
}
}
}
return xAxisX[xAxisX.length - 1].toString();
}
function extracted(da) {
//获取参数指定的公差,标准值,组间据
da.sort((a, b) => a - b);
let mid = getMedianSorted(da).toFixed(3) * 1.0;
// 计算各项最大值,最小值,均值,以及cpk值,以及数组长度
let length = da.length;
let max = Math.max(...da);
let min = Math.min(...da);
let mean = da.reduce((sum, val) => sum + val, 0) / length;
//依据原始数据重新计算数据X轴
//x轴最大最小前后范围
let dataRangeMinOP = 1;
let dataRangeMaxOP = 1.1;
// 设定区间起始和结束值,以及间距 ,X轴的步距
let start = min - dataRangeMinOP;
let end = max + dataRangeMaxOP;
start = start.toFixed(0) * 1.0;
// 计算区间数量
let numIntervals = Math.ceil((end - start) / interval);
//获取所有区间,以及初始化区间每个区间的频数为0
let xAxis = [];
let barYaxis = new Array(numIntervals).fill(0);
for (let i = start; i <= end; i = i + interval) {
let str = i.toFixed(1).toString();
xAxis.push(str);
}
// 遍历数组并计算频数
da.forEach((value) => {
if (value >= start && value <= end) {
// 找到值所在的区间
let intervalIndex = Math.floor((value - start) / interval);
// 增加该区间的频数
barYaxis[intervalIndex]++;
}
});
//正态曲线计算的基本数据和方法
//计算标准差
let std = getStd(da, mean);
//依据x轴,计算正态分布值
let lineYaxis = fxNormalDistribution(xAxis, std, mean);
//求得分布曲线最大值,用以绘制纵向线
let y1Max = Math.max(...lineYaxis);
let midInterval = getInterval(mid, xAxis);
//定义Y轴
//定义实际数据的频数柱状图
let barDataSet = {
type: "bar",
smooth: true,
xAxisIndex: 0,
yAxisIndex: 0,
areaStyle: {
opacity: 0,
},
markLine: {
silent: true,
label: {
fontSize: 10
},
lineStyle: {
color: "rgb(255,0,0)",
},
data: [],
},
data: barYaxis,
itemStyle: {
normal: {
label: {
formatter: "{c} %",
show: false, //默认显示
position: "top", //在上方显示
textStyle: {
//数值样式
fontSize: 10,
},
},
},
},
};
//计算实际数据的正态分布图
let lineDataSet = {
type: "line",
smooth: true,
xAxisIndex: 0,
yAxisIndex: 1,
areaStyle: {
opacity: 0,
},
markLine: {
silent: true,
label: {
fontSize: 12
},
data: [
[
{
name: "中位数:" + mid,
coord: [midInterval, 0],
},
{
coord: [midInterval, y1Max],
},
],
],
},
data: lineYaxis,
itemStyle: {
normal: {
label: {
formatter: "{c} %",
show: false, //开启显示
position: "top", //在上方显示
textStyle: {
//数值样式
fontSize: 10,
},
},
},
},
};
return {xAxis, barDataSet, lineDataSet};
}
function getOptions() {
x = extracted(da);
return {
color: drawColor,
//工具栏。内置有导出图片,数据视图,动态类型切换,数据区域缩放,重置五个工具。
toolbox: {
show: true,
feature: {
dataZoom: {
yAxisIndex: "none",
},
dataView: {readOnly: false},
restore: {},
saveAsImage: {},
},
},
title: {
text: title,
left: "center"
},
grid: {
top: 80
},
//提示框组件
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow",
},
},
//图例
legend: {
right: "0%",
top: "0",
textStyle:
{
fontSize: 12
}
},
xAxis: [
{boundaryGap: true, type: "category", gridIndex: 0, data: x.xAxis},
],
//定义y轴,2个轴
yAxis: [
{
type: "value", gridIndex: 0, max: function (value) {
return value.max + 5;
}
},
{
type: "value", gridIndex: 0, max: function (value) {
return (value.max + 0.2).toFixed(1);
}
}
],
series: [
x.barDataSet, x.lineDataSet,
],
}
}
return getOptions();