从excel提取和过滤数据到echarts中绘制图

主页面

介绍

echarts的事例页面,导入数据比较麻烦,此项目从excel中提取数据(含过滤数据),以注入页面.

代码说明

  1. 所有的需要从excel中读取的参数,从代码中替换.需以{{data}} 包含在内
  2. 使用绘制参数的解析代码参数可以解析出来所有参数
  3. 数据配置上传文件后,可以选择列
  4. 数据过滤条件设定后,点击绘制,此时才会过滤数据
  5. 代码块实际是返回echarts.options的信息,下面给了多个事例
  6. 图表设置实际没有使用,需要后端支持才可以,将编辑好的代码保存到数据库,或者取出
  7. 其他的参数,例如绘制参数,建议直接编辑代码完成,并且建议以变量的形式填写在最上方,以提供出来


页面代码 commonChart.vue

html 复制代码
<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

js 复制代码
// 单散点分布 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

js 复制代码
// 双散点分布 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

js 复制代码
// 单正态分布图 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();
相关推荐
极小狐5 分钟前
极狐GitLab 容器镜像仓库功能介绍
java·前端·数据库·npm·gitlab
程序猿阿伟17 分钟前
《Flutter社交应用暗黑奥秘:模式适配与色彩的艺术》
前端·flutter
rafael(一只小鱼)21 分钟前
黑马点评实战笔记
前端·firefox
weifont21 分钟前
React中的useSyncExternalStore使用
前端·javascript·react.js
初遇你时动了情25 分钟前
js fetch流式请求 AI动态生成文本,实现逐字生成渲染效果
前端·javascript·react.js
影子信息40 分钟前
css 点击后改变样式
前端·css
几何心凉1 小时前
如何使用 React Hooks 替代类组件的生命周期方法?
前端·javascript·react.js
小堃学编程1 小时前
前端学习(1)—— 使用HTML编写一个简单的个人简历展示页面
前端·javascript·html
hnlucky2 小时前
通俗易懂版知识点:Keepalived + LVS + Web + NFS 高可用集群到底是干什么的?
linux·前端·学习·github·web·可用性测试·lvs
懒羊羊我小弟2 小时前
使用 ECharts GL 实现交互式 3D 饼图:技术解析与实践
前端·vue.js·3d·前端框架·echarts