使用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 FIXME
showChartType: 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,用这个处理,下面都采用that
let that = this;
//option
let 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-bar
else {
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;
}
}