新增SPC基本趋势图页面
This commit is contained in:
@@ -15,6 +15,7 @@ npm run dev
|
|||||||
## 页面路由
|
## 页面路由
|
||||||
|
|
||||||
- `/overview`:SPC 总览和关键指标
|
- `/overview`:SPC 总览和关键指标
|
||||||
|
- `/run-chart`:基本趋势图,显示原始序列、均值线、中位线、规格限和移动平均线
|
||||||
- `/xbar-r`:Xbar 控制图,控制限来自 XR 结果
|
- `/xbar-r`:Xbar 控制图,控制限来自 XR 结果
|
||||||
- `/r-chart`:R 极差控制图
|
- `/r-chart`:R 极差控制图
|
||||||
- `/xbar-s`:Xbar 控制图,控制限来自 XS 结果
|
- `/xbar-s`:Xbar 控制图,控制限来自 XS 结果
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import RChartView from "./views/RChartView.vue";
|
|||||||
import XBarSView from "./views/XBarSView.vue";
|
import XBarSView from "./views/XBarSView.vue";
|
||||||
import SChartView from "./views/SChartView.vue";
|
import SChartView from "./views/SChartView.vue";
|
||||||
import CpkView from "./views/CpkView.vue";
|
import CpkView from "./views/CpkView.vue";
|
||||||
|
import RunChartView from "./views/RunChartView.vue";
|
||||||
|
|
||||||
export const routes = [
|
export const routes = [
|
||||||
{ path: "/", redirect: "/overview" },
|
{ path: "/", redirect: "/overview" },
|
||||||
{ path: "/overview", component: OverviewView, meta: { title: "总览" } },
|
{ path: "/overview", component: OverviewView, meta: { title: "总览" } },
|
||||||
|
{ path: "/run-chart", component: RunChartView, meta: { title: "趋势图" } },
|
||||||
{ path: "/xbar-r", component: XBarRView, meta: { title: "Xbar-R" } },
|
{ path: "/xbar-r", component: XBarRView, meta: { title: "Xbar-R" } },
|
||||||
{ path: "/r-chart", component: RChartView, meta: { title: "R 图" } },
|
{ path: "/r-chart", component: RChartView, meta: { title: "R 图" } },
|
||||||
{ path: "/xbar-s", component: XBarSView, meta: { title: "Xbar-S" } },
|
{ path: "/xbar-s", component: XBarSView, meta: { title: "Xbar-S" } },
|
||||||
|
|||||||
@@ -3,6 +3,14 @@ import type { ControlLimitChart, CpkResult } from "../types/spc";
|
|||||||
|
|
||||||
const chartColors = ["#0f766e", "#b45309", "#991b1b", "#2563eb"];
|
const chartColors = ["#0f766e", "#b45309", "#991b1b", "#2563eb"];
|
||||||
|
|
||||||
|
export interface RunChartInput {
|
||||||
|
values: readonly number[];
|
||||||
|
usl: number;
|
||||||
|
lsl: number;
|
||||||
|
target: number;
|
||||||
|
movingAverageWindow?: number;
|
||||||
|
}
|
||||||
|
|
||||||
// 生成从 1 开始的子组序号,用于控制图横轴。
|
// 生成从 1 开始的子组序号,用于控制图横轴。
|
||||||
export function sequenceLabels(length: number): string[] {
|
export function sequenceLabels(length: number): string[] {
|
||||||
return Array.from({ length }, (_, index) => String(index + 1));
|
return Array.from({ length }, (_, index) => String(index + 1));
|
||||||
@@ -13,6 +21,36 @@ function constantSeries(length: number, value: number): number[] {
|
|||||||
return Array.from({ length }, () => value);
|
return Array.from({ length }, () => value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算算术平均值,用于基本趋势图的均值线。
|
||||||
|
function mean(values: readonly number[]): number {
|
||||||
|
if (!values.length) return 0;
|
||||||
|
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算中位数,用于基本趋势图的中心参考线。
|
||||||
|
function median(values: readonly number[]): number {
|
||||||
|
if (!values.length) return 0;
|
||||||
|
const sorted = [...values].sort((a, b) => a - b);
|
||||||
|
const middle = Math.floor(sorted.length / 2);
|
||||||
|
return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算移动平均线,窗口不足时使用当前已有数据,避免前几项为空。
|
||||||
|
function movingAverage(values: readonly number[], windowSize: number): number[] {
|
||||||
|
return values.map((_, index) => {
|
||||||
|
const start = Math.max(0, index - windowSize + 1);
|
||||||
|
return mean(values.slice(start, index + 1));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找出超出规格限的原始数据点,用于基本趋势图标红。
|
||||||
|
function outOfSpecPoints(values: readonly number[], usl: number, lsl: number): Array<[number, number]> {
|
||||||
|
return values
|
||||||
|
.map((value, index) => ({ value, index }))
|
||||||
|
.filter((point) => point.value > usl || point.value < lsl)
|
||||||
|
.map((point) => [point.index, point.value]);
|
||||||
|
}
|
||||||
|
|
||||||
// 计算直方图 X 轴范围,覆盖分箱、规格限、正态曲线和原始极值,并保留少量边距。
|
// 计算直方图 X 轴范围,覆盖分箱、规格限、正态曲线和原始极值,并保留少量边距。
|
||||||
function calculateHistogramXAxisRange(cpk: CpkResult): { min: number; max: number } {
|
function calculateHistogramXAxisRange(cpk: CpkResult): { min: number; max: number } {
|
||||||
const values = [
|
const values = [
|
||||||
@@ -114,6 +152,95 @@ export function createControlLimitOption(chart: ControlLimitChart): EChartsOptio
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建基本趋势图配置,按原始顺序展示数据并叠加规格限、均值线、中位线和移动平均线。
|
||||||
|
export function createRunChartOption(input: RunChartInput): EChartsOption {
|
||||||
|
const labels = sequenceLabels(input.values.length);
|
||||||
|
const average = mean(input.values);
|
||||||
|
const middle = median(input.values);
|
||||||
|
const windowSize = Math.max(2, Math.floor(input.movingAverageWindow || 5));
|
||||||
|
const outliers = outOfSpecPoints(input.values, input.usl, input.lsl);
|
||||||
|
|
||||||
|
return {
|
||||||
|
color: chartColors,
|
||||||
|
tooltip: { trigger: "axis" },
|
||||||
|
legend: { top: 0, right: 0 },
|
||||||
|
grid: { left: 56, right: 34, top: 46, bottom: 44 },
|
||||||
|
xAxis: {
|
||||||
|
type: "category",
|
||||||
|
name: "样本序号",
|
||||||
|
data: labels,
|
||||||
|
boundaryGap: false,
|
||||||
|
axisLabel: { color: "#667085" }
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: "value",
|
||||||
|
name: "测量值",
|
||||||
|
scale: true,
|
||||||
|
axisLabel: { color: "#667085" },
|
||||||
|
splitLine: { lineStyle: { color: "#e5e7eb" } }
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "原始数据",
|
||||||
|
type: "line",
|
||||||
|
data: [...input.values],
|
||||||
|
symbolSize: 4,
|
||||||
|
lineStyle: { width: 1.8 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `${windowSize}点移动平均`,
|
||||||
|
type: "line",
|
||||||
|
data: movingAverage(input.values, windowSize),
|
||||||
|
smooth: true,
|
||||||
|
symbol: "none",
|
||||||
|
lineStyle: { width: 2 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "均值",
|
||||||
|
type: "line",
|
||||||
|
data: constantSeries(input.values.length, average),
|
||||||
|
symbol: "none",
|
||||||
|
lineStyle: { type: "dashed", width: 1.4 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "中位线",
|
||||||
|
type: "line",
|
||||||
|
data: constantSeries(input.values.length, middle),
|
||||||
|
symbol: "none",
|
||||||
|
lineStyle: { type: "dashed", width: 1.4 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "超规格点",
|
||||||
|
type: "scatter",
|
||||||
|
data: outliers,
|
||||||
|
symbolSize: 10,
|
||||||
|
itemStyle: { color: "#dc2626" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "USL",
|
||||||
|
type: "line",
|
||||||
|
data: constantSeries(input.values.length, input.usl),
|
||||||
|
symbol: "none",
|
||||||
|
lineStyle: { type: "dotted", width: 1.5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "LSL",
|
||||||
|
type: "line",
|
||||||
|
data: constantSeries(input.values.length, input.lsl),
|
||||||
|
symbol: "none",
|
||||||
|
lineStyle: { type: "dotted", width: 1.5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "目标值",
|
||||||
|
type: "line",
|
||||||
|
data: constantSeries(input.values.length, input.target),
|
||||||
|
symbol: "none",
|
||||||
|
lineStyle: { type: "dotted", width: 1.5 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 创建过程能力指标柱状图,便于快速查看 Cp/Cpk/Pp/Ppk 门槛。
|
// 创建过程能力指标柱状图,便于快速查看 Cp/Cpk/Pp/Ppk 门槛。
|
||||||
export function createCapabilityBarOption(cpk: CpkResult): EChartsOption {
|
export function createCapabilityBarOption(cpk: CpkResult): EChartsOption {
|
||||||
const labels = ["Cp", "Cpk", "Pp", "Ppk", "CPU", "CPL"];
|
const labels = ["Cp", "Cpk", "Pp", "Ppk", "CPU", "CPL"];
|
||||||
|
|||||||
49
spc-vue/src/views/RunChartView.vue
Normal file
49
spc-vue/src/views/RunChartView.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import EChartPanel from "../components/EChartPanel.vue";
|
||||||
|
import MetricStrip from "../components/MetricStrip.vue";
|
||||||
|
import { useSpc } from "../composables/useSpc";
|
||||||
|
import { createRunChartOption } from "../utils/chartOptions";
|
||||||
|
|
||||||
|
const { currentInput } = useSpc();
|
||||||
|
|
||||||
|
const option = computed(() => {
|
||||||
|
const input = currentInput.value;
|
||||||
|
if (!input) return null;
|
||||||
|
return createRunChartOption({
|
||||||
|
values: input.x,
|
||||||
|
usl: input.usl,
|
||||||
|
lsl: input.lsl,
|
||||||
|
target: (input.usl + input.lsl) / 2,
|
||||||
|
movingAverageWindow: input.n
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const metrics = computed(() => {
|
||||||
|
const input = currentInput.value;
|
||||||
|
if (!input) return [];
|
||||||
|
const overSpecCount = input.x.filter((value) => value > input.usl || value < input.lsl).length;
|
||||||
|
const average = input.x.reduce((sum, value) => sum + value, 0) / input.x.length;
|
||||||
|
|
||||||
|
return [
|
||||||
|
{ label: "样本数", value: input.x.length },
|
||||||
|
{ label: "移动平均窗口", value: input.n },
|
||||||
|
{ label: "均值", value: Number(average.toFixed(6)) },
|
||||||
|
{ label: "目标值", value: Number(((input.usl + input.lsl) / 2).toFixed(6)) },
|
||||||
|
{ label: "超规格点", value: overSpecCount, tone: overSpecCount === 0 ? ("ok" as const) : ("warn" as const) },
|
||||||
|
{ label: "规格限", value: `${input.lsl} ~ ${input.usl}` }
|
||||||
|
];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article class="page-stack">
|
||||||
|
<MetricStrip v-if="metrics.length" :metrics="metrics" />
|
||||||
|
<EChartPanel
|
||||||
|
title="基本趋势图"
|
||||||
|
eyebrow="Run Chart"
|
||||||
|
description="基本趋势图按原始采集顺序展示测量值,叠加均值线、中位线、目标值、规格限和移动平均线。它不替代控制图,主要用于先观察漂移、跳变、周期波动和超规格点。"
|
||||||
|
:option="option"
|
||||||
|
/>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user