使用echarts,elementUi,vue编写的spc分析的demo示例.
含x-bar和正态分布图,同一数据可以互转
chart.vue
<template><div class="app-container"><el-row><el-col :span="4" class="button-container"><el-button @click="redrawChart">重新绘制</el-button><el-button type="warning" @click="analyzeData">重新解析数据并绘制</el-button><el-button type="primary" @click="drawNormalChart">正态分布(X)</el-button><el-button type="success" @click="showSpecDialog">更新SPEC(X)</el-button><el-button type="info">计算cpk(X)</el-button><el-button type="danger">危险按钮(X)</el-button></el-col><el-col :span="20"><div id="mainChart" style="height: 720px;width: 1080px;"/></el-col></el-row><div>min:{{ dData.min.toFixed(3) }}max:{{ dData.min.toFixed(3) }}mean:{{ dData.min.toFixed(3) }}std:{{ dData.min.toFixed(3) }}</div><div>说明1.x-bar等图的上线边界,计算上下边界,如果数据过于集中,自动图表缩放会导致usl甚至是ucl超出y轴范围,不能正常绘制,那么需要手动计算上下边界<br/>上边界取max值和usl+0.5*(usl-ucl)更大的那个<br/>下边界取min值和lsl-0.5*(lcl-lsl)更小的那个<br/>为避免出现小数位数过多/精度不足 取3位小数<br/>2.关闭SPEC弹框时由于数据已经被更新,所以在关闭弹框时间上直接绑定了重绘事件.但是由于上下边界是在解析数据阶段完成的,所以不会重新解析上下边界</div><el-dialog title="更新SPEC" :visible.sync="visibleSpecDialog" width="800px" top="5vh" @closed="onSpecDialogClose" append-to-body><el-form ref="form" :model="dData" label-width="200px"><el-form-item label="usl"><el-input v-model="dData.usl"/></el-form-item><el-form-item label="ucl"><el-input v-model="dData.ucl"/></el-form-item><el-form-item label="lcl"><el-input v-model="dData.lcl"/></el-form-item><el-form-item label="lsl"><el-input v-model="dData.lsl"/></el-form-item></el-form></el-dialog></div>
</template><script>
import * as echarts from "echarts";
import * as utils from './utils';//定义显示的颜色
let red="#FF0000"
let yellow="#CCCC00"
let green="#009000"export default {data() {//参数定义return {//主chart实例对象mainChart: {},//从服务器查询数据参数对象queryParams: {},//从服务器获取的原始数据(originalData)oData: {usl: 0,ucl: 0,cl: 0,lcl: 0,lsl: 0,dataList: []},//预定义维度数组,注意顺序,不要轻易改变,注意需要调整analyzeData的转二维数组的内容columns: ["dataTime", "paramValue", "remark"],//绘制图需要的数据格式,(drawData)dData: {//控制参数usl: 0,ucl: 0,cl: 0,lcl: 0,lsl: 0,//计算参数min: 0,max: 0,mean: 0,std: 0,upperY: 0,lowerY: 0,//仅提取paramValue作为值数组valueData: [],//x-barData 数据格式,就是二维数组xBarData: [],//正态分布数据格式normalData: {//正态分布前后距离dataRangeMinOP: 1,dataRangeMaxOP: 1.1,//组间距interval: 1,// 这3个就不构建datasource来处理了,直接用好了// 构建离散值x轴xAxis: [],// 构建柱状y轴barYaxis: [],// 构建线性y轴lineYaxis: []}},// 当前图表类型 1.XR 2.XS 3.WR 4.ZT FIXMEshowChartType: 1,//显示更新SPEC弹框标记visibleSpecDialog: false};},mounted() {this.init();},methods: {/*** 初始化方法* 调用过程如下:* init->queryData->analyzeData->redrawChart->createOption->this.chart.setOption*/init() {//从dom加载echarts对象let chartDom = document.getElementById("mainChart");this.chart = echarts.init(chartDom);this.queryData();},/*** 从服务器获取原始数据* 实际参数:this.queryParams.... 返回结果到this.oData* 回调触发redrawChart*/queryData() {this.oData.dataList = []//模拟写入数据for (let n = 0; n < 100; n++) {this.oData.dataList.push({dataTime: n + ":00",paramValue: 30 + Math.random() * 30,remark: (Math.random() * 20).toFixed(3)});}this.oData.usl = 55;this.oData.ucl = 50;this.oData.cl = 45;this.oData.lcl = 40;this.oData.lsl = 35;//解析原始为图表需要的数据this.analyzeData();},/*** 绘制,或重新绘制主chart图*/redrawChart() {//清除图表this.chart.clear()//创建option并绘制let option = this.createOption();this.chart.setOption(option);//弹框说明this.$message.success("绘制图表成功!");},drawNormalChart() {if (this.showChartType === 1) {this.showChartType = 4;} else {this.showChartType = 1;}this.redrawChart();},showSpecDialog() {this.visibleSpecDialog = true;},//Spec弹框被关闭onSpecDialogClose() {this.redrawChart();},/*** 将原始数据解析为x-bar等图需要的格式* 计算mean,std.,cpk..等数据返回* 实际参数:this.oData.... 返回结果到this.dData*/analyzeData() {//copy基础数据this.dData.usl = this.oData.usl;this.dData.ucl = this.oData.ucl;this.dData.cl = this.oData.cl;this.dData.lcl = this.oData.lcl;this.dData.lsl = this.oData.lsl;//仅提取paramValue作为值数组this.dData.valueData = this.oData.dataList.map(obj => obj.paramValue);//计算其他数据,最大最小cpk什么的this.dData.min = Math.min(...this.dData.valueData);this.dData.max = Math.max(...this.dData.valueData);this.dData.mean = this.dData.valueData.reduce((sum, val) => sum + val, 0) / this.dData.valueData.length;this.dData.std = utils.getStd(this.dData.valueData, this.dData.mean)//计算上下边界,如果数据过于集中,自动图表缩放会导致usl甚至是ucl超出y轴范围,不能正常绘制,那么需要手动计算上下边界//取max值和usl+0.5*(usl-ucl)更大的那个//取min值和lsl-0.5*(lcl-lsl)更小的那个this.dData.upperY = Math.max(this.dData.usl + 0.5 * (this.dData.usl - this.dData.ucl), this.dData.max).toFixed(3)this.dData.lowerY = Math.min(this.dData.lsl - 0.5 * (this.dData.lcl - this.dData.lsl), this.dData.min).toFixed(3)// 转x-bar需要数据// 对象数组转二维数组this.dData.xBarData = []this.dData.xBarData = this.oData.dataList.map(obj => [obj.dataTime, obj.paramValue, obj.remark]);// 转正态分布数据// 构建x轴let start = this.dData.min - this.dData.normalData.dataRangeMinOP;let end = this.dData.max + this.dData.normalData.dataRangeMaxOP;// 计算区间数量let numIntervals = Math.ceil((end - start) / this.dData.normalData.interval);// 构建离散值x轴let xAxis = [];for (let i = start; i <= end; i = i + this.dData.normalData.interval) {let str = i.toFixed(1).toString();xAxis.push(str);}this.dData.normalData.xAxis = xAxis;// 构建柱状y轴,遍历数组并计算频数let barYaxis = new Array(numIntervals).fill(0);this.dData.valueData.forEach((value) => {if (value >= start && value <= end) {// 找到值所在的区间let intervalIndex = Math.floor((value - start) / this.dData.normalData.interval);// 增加该区间的频数barYaxis[intervalIndex]++;}});this.dData.normalData.barYaxis = barYaxis;// 构建线性y轴this.dData.normalData.lineYaxis = utils.fxNormalDistribution(xAxis, this.dData.std, this.dData.mean);this.redrawChart();},/*** 构建图图表参数* @returns*/createOption() {//内部匿名方法无法访问到this,用这个处理,下面都采用thatlet that = this;//optionlet option = {};//正态分布if (this.showChartType === 4) {//定义实际数据的频数柱状图let barDataSet = {type: "bar",smooth: true,yAxisIndex: 0,areaStyle: {opacity: 0,},data: that.dData.normalData.barYaxis,name: "实际分布频数",label: {formatter: "{c} %",show: false, //默认显示position: "top", //在上方显示textStyle: {//数值样式fontSize: 16,},},};//计算实际数据的正态分布图let lineDataSet = {type: "line",smooth: true,yAxisIndex: 1,areaStyle: {opacity: 0,},data: that.dData.normalData.lineYaxis,name: "实际正态分布",label: {formatter: "{c} %",show: false, //开启显示position: "top", //在上方显示textStyle: {//数值样式fontSize: 16,},},};option = {title: {text: 'SPC',},//提示框组件tooltip: {trigger: "axis",axisPointer: {type: "shadow",},},xAxis: {boundaryGap: true,type: "category",data: that.dData.normalData.xAxis,},//定义y轴yAxis: [{type: "value",}, {type: "value",}],series: [barDataSet, lineDataSet],};}//x-barelse {option = {title: {text: 'SPC',},tooltip: {trigger: 'axis',//轴数据指示axisPointer: {type: 'cross'},},xAxis: {type: 'category'},yAxis: {type: 'value',max: that.dData.upperY,min: that.dData.lowerY},dataset: [{//定义数据字段dimensions: [{name: "dataTime", type: 'ordinal'}, "paramValue", "remark"],//定义数据内容source: that.dData.xBarData},],series: [{name: 'Dow-Jones index',type: 'line',//密集点数过多是否显示showAllSymbol: true,//定义xy轴取数据那一列值encode: {x: 'dataTime',y: 'paramValue',tooltip: ["paramValue", "dataTime", "remark"],},//定义点样式,依据数据定义点样式以及大小颜色symbol: function (params) {if (params[1] > that.dData.ucl || params[1] < that.dData.lcl) {if (params[1] > that.dData.usl || params[1] < that.dData.lsl) {return 'triangle';}return 'circle';} else {return 'emptyCircle';}},symbolSize: function (params) {if (params[1] > that.dData.ucl || params[1] < that.dData.lcl) {if (params[1] > that.dData.usl || params[1] < that.dData.lsl) {return 10;}return 9;} else {return 8;}},itemStyle: {color: function (params) {if (params.data[1] > that.dData.ucl || params.data[1]< that.dData.lcl) {if (params.data[1] > that.dData.usl || params.data[1] < that.dData.lsl) {return red;}return yellow;} else {return green;}}},//定义规格线markLine: {silent: true,symbol: 'none',data: [{name: 'USL',yAxis: that.dData.usl,lineStyle: {color: red},label: {color: red, formatter: 'USL:' + that.dData.usl, fontSize: 10}},{name: 'UCL',yAxis: that.dData.ucl,lineStyle: {color: yellow},label: {color: yellow, formatter: 'UCL:' + that.dData.ucl, fontSize: 10}},{name: 'CL',yAxis: that.dData.cl,lineStyle: {color: green},label: {color: green, formatter: 'CL:' + that.dData.cl, fontSize: 10}},{name: 'LCL',yAxis: that.dData.lcl,lineStyle: {color: yellow},label: {color: yellow, formatter: 'LCL:' + that.dData.lcl, fontSize: 10}},{name: 'LSL',yAxis: that.dData.lsl,lineStyle: {color: red},label: {color: red, formatter: 'LSL:' + that.dData.lsl, fontSize: 10}}]}}]};}return option;}},};
</script><style scoped>
.button-container {display: grid;grid-template-columns: 1fr; /* 单列 */grid-auto-rows: minmax(auto, auto); /* 自动行高 */row-gap: 10px; /* 行间距 *//* 可以根据需要添加更多样式 */
}.button-container .el-button {margin-right: 10px;margin-left: 10px;
}
</style>
utils.js
//计算正态曲线
export 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;
}//计算标准差
export 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));
}//对有序数组求中位数
export 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;}
}