新增SPC基本趋势图页面

This commit is contained in:
zhangshun
2026-06-29 09:29:25 +08:00
parent 5ca2657536
commit 78c80cf5f7
4 changed files with 179 additions and 0 deletions

View File

@@ -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 结果

View File

@@ -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" } },

View File

@@ -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"];

View 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>