Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/core/src/stats/basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export function calculateStats(data: number[], usl?: number, lsl?: number): Stat

let cp: number | undefined;
let cpk: number | undefined;
let pp: number | undefined;
let ppk: number | undefined;

// Cp/Cpk use σ_within (short-term capability, industry standard)
// Guard: when sigmaWithin=0 (all values identical), Cp/Cpk would be Infinity
Expand All @@ -109,6 +111,20 @@ export function calculateStats(data: number[], usl?: number, lsl?: number): Stat
cpk = (mean - lsl) / (3 * sigmaWithin);
}

// Pp/Ppk use σ_overall (long-term process performance).
if (stdDev !== 0) {
if (usl !== undefined && lsl !== undefined) {
pp = (usl - lsl) / (6 * stdDev);
const ppu = (usl - mean) / (3 * stdDev);
const ppl = (mean - lsl) / (3 * stdDev);
ppk = Math.min(ppu, ppl);
} else if (usl !== undefined) {
ppk = (usl - mean) / (3 * stdDev);
} else if (lsl !== undefined) {
ppk = (mean - lsl) / (3 * stdDev);
}
}

const outOfSpec = data.filter(d => {
if (usl !== undefined && d > usl) return true;
if (lsl !== undefined && d < lsl) return true;
Expand All @@ -127,6 +143,8 @@ export function calculateStats(data: number[], usl?: number, lsl?: number): Stat
lcl,
cp,
cpk,
pp,
ppk,
outOfSpecPercentage,
};
}
4 changes: 4 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ export interface StatsResult {
cp?: number;
/** Process Capability index accounting for centering (uses σ_within) */
cpk?: number;
/** Process Performance index (uses σ_overall) - requires both USL and LSL */
pp?: number;
/** Process Performance index accounting for centering (uses σ_overall) */
ppk?: number;
/** Percentage of values outside specification limits */
outOfSpecPercentage: number;
}
Expand Down
35 changes: 34 additions & 1 deletion packages/hooks/src/__tests__/useCanvasStepCards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
buildCanvasStepCards,
coerceCanvasLens,
enabledCanvasLenses,
isCanvasLensValidAtLevel,
suggestCanvasLevelForLens,
} from '../useCanvasStepCards';

const baseMap = (overrides: Partial<ProcessMap> = {}): ProcessMap => ({
Expand Down Expand Up @@ -39,7 +41,12 @@ const rows: DataRow[] = Array.from({ length: 40 }, (_, i) => ({

describe('canvas lens registry', () => {
it('enables default, capability, and defect while leaving future lenses registered', () => {
expect(enabledCanvasLenses().map(lens => lens.id)).toEqual(['default', 'capability', 'defect']);
expect(enabledCanvasLenses().map(lens => lens.id)).toEqual([
'default',
'capability',
'defect',
'process-flow',
]);
expect(CANVAS_LENS_REGISTRY.performance.enabled).toBe(false);
expect(CANVAS_LENS_REGISTRY.yamazumi.enabled).toBe(false);
});
Expand All @@ -49,6 +56,32 @@ describe('canvas lens registry', () => {
expect(coerceCanvasLens('performance')).toBe('default');
expect(coerceCanvasLens('unknown')).toBe('default');
});

it('declares the supported lens x level matrix for current canvas lenses', () => {
const matrix = {
default: { l1: true, l2: true, l3: true },
capability: { l1: true, l2: true, l3: true },
defect: { l1: true, l2: true, l3: true },
performance: { l1: true, l2: true, l3: true },
yamazumi: { l1: false, l2: true, l3: true },
'process-flow': { l1: false, l2: true, l3: false },
} as const;

for (const [lens, levels] of Object.entries(matrix)) {
for (const [level, expected] of Object.entries(levels)) {
expect(
isCanvasLensValidAtLevel(lens as keyof typeof matrix, level as 'l1' | 'l2' | 'l3')
).toBe(expected);
}
}
});

it('suggests the nearest valid level for disabled lens x level cells', () => {
expect(suggestCanvasLevelForLens('yamazumi', 'l1')).toBe('l2');
expect(suggestCanvasLevelForLens('process-flow', 'l1')).toBe('l2');
expect(suggestCanvasLevelForLens('process-flow', 'l3')).toBe('l2');
expect(suggestCanvasLevelForLens('default', 'l1')).toBe('l1');
});
});

describe('buildCanvasStepCards drift integration', () => {
Expand Down
19 changes: 19 additions & 0 deletions packages/hooks/src/__tests__/useCanvasViewportShortcuts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,25 @@ describe('useCanvasViewportShortcuts', () => {
});
});

it('uses the caller-provided fit implementation when fitting with shortcuts', () => {
const fitToContent = (hubId: ProcessHubId, targetLevel?: 'l1' | 'l2' | 'l3') => {
useCanvasViewportStore.getState().fitToContent(hubId, targetLevel, {
zoom: 1.9,
pan: { x: 25, y: 12.5 },
});
};
renderHook(() => useCanvasViewportShortcuts({ hubId: HUB_ID, fitToContent }));

const event = keydown('1', { metaKey: true });

expect(event.defaultPrevented).toBe(true);
expect(viewport()).toMatchObject({
currentLevel: 'l1',
zoom: 1.9,
pan: { x: 25, y: 12.5 },
});
});

it('maps Cmd/Ctrl+3 to l3 only when the current viewport has a focal step', () => {
renderHook(() => useCanvasViewportShortcuts({ hubId: HUB_ID }));

Expand Down
2 changes: 2 additions & 0 deletions packages/hooks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export {
buildCanvasStepCards,
coerceCanvasLens,
enabledCanvasLenses,
isCanvasLensValidAtLevel,
suggestCanvasLevelForLens,
useCanvasStepCards,
type BuildCanvasStepCardsArgs,
type CanvasLensDefinition,
Expand Down
28 changes: 27 additions & 1 deletion packages/hooks/src/useCanvasStepCards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,18 @@ import {
type DriftResult,
type StepCapabilityStamp,
} from '@variscout/core/canvas';
import type { CanvasLevel } from '@variscout/core/canvas';
import type { ProcessMap } from '@variscout/core/frame';
import { detectColumns } from '@variscout/core/parser';
import { parseTimeValue } from '@variscout/core/time';

export type CanvasLensId = 'default' | 'capability' | 'defect' | 'performance' | 'yamazumi';
export type CanvasLensId =
| 'default'
| 'capability'
| 'defect'
| 'performance'
| 'yamazumi'
| 'process-flow';

export interface CanvasLensDefinition {
id: CanvasLensId;
Expand Down Expand Up @@ -59,6 +66,12 @@ export const CANVAS_LENS_REGISTRY: Record<CanvasLensId, CanvasLensDefinition> =
enabled: false,
description: 'Future time-study lens.',
},
'process-flow': {
id: 'process-flow',
label: 'Process flow',
enabled: true,
description: 'Plain process structure without per-card analytics.',
},
};

export function enabledCanvasLenses(): CanvasLensDefinition[] {
Expand All @@ -71,6 +84,19 @@ export function coerceCanvasLens(value: unknown): CanvasLensId {
return lens?.enabled ? lens.id : 'default';
}

export function isCanvasLensValidAtLevel(lens: CanvasLensId, level: CanvasLevel): boolean {
if (lens === 'yamazumi' && level === 'l1') return false;
if (lens === 'process-flow' && (level === 'l1' || level === 'l3')) return false;
return true;
}

export function suggestCanvasLevelForLens(lens: CanvasLensId, level: CanvasLevel): CanvasLevel {
if (isCanvasLensValidAtLevel(lens, level)) return level;
if (lens === 'yamazumi') return 'l2';
if (lens === 'process-flow') return 'l2';
return 'l2';
}

export interface CanvasStepCategory {
label: string;
count: number;
Expand Down
28 changes: 23 additions & 5 deletions packages/hooks/src/useCanvasViewportShortcuts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect } from 'react';
import { useCanvasViewportStore, type ProcessHubId } from '@variscout/stores';
import type { CanvasLevel } from '@variscout/core/canvas';

function isEditableTarget(target: EventTarget | null): boolean {
if (!(target instanceof HTMLElement)) return false;
Expand All @@ -14,14 +15,31 @@ function isEditableTarget(target: EventTarget | null): boolean {
);
}

function fitNowAndAfterRender(
fitToContent: (hubId: ProcessHubId, targetLevel?: CanvasLevel) => void,
hubId: ProcessHubId,
targetLevel?: CanvasLevel
): void {
fitToContent(hubId, targetLevel);
if (typeof window.requestAnimationFrame !== 'function') return;
window.requestAnimationFrame(() => {
const viewport = useCanvasViewportStore.getState().getViewport(hubId);
if (targetLevel === 'l3' && !viewport.focalStepId) return;
fitToContent(hubId, targetLevel);
});
}

export function useCanvasViewportShortcuts({
hubId,
disabled = false,
fitToContent: fitToContentOverride,
}: {
hubId: ProcessHubId;
disabled?: boolean;
fitToContent?: (hubId: ProcessHubId, targetLevel?: CanvasLevel) => void;
}): void {
const fitToContent = useCanvasViewportStore(s => s.fitToContent);
const storeFitToContent = useCanvasViewportStore(s => s.fitToContent);
const fitToContent = fitToContentOverride ?? storeFitToContent;

useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
Expand All @@ -31,13 +49,13 @@ export function useCanvasViewportShortcuts({

if (event.key === '1') {
event.preventDefault();
fitToContent(hubId, 'l1');
fitNowAndAfterRender(fitToContent, hubId, 'l1');
return;
}

if (event.key === '2') {
event.preventDefault();
fitToContent(hubId, 'l2');
fitNowAndAfterRender(fitToContent, hubId, 'l2');
return;
}

Expand All @@ -46,13 +64,13 @@ export function useCanvasViewportShortcuts({
if (!viewport.focalStepId) return;

event.preventDefault();
fitToContent(hubId, 'l3');
fitNowAndAfterRender(fitToContent, hubId, 'l3');
return;
}

if (event.key === '0') {
event.preventDefault();
fitToContent(hubId);
fitNowAndAfterRender(fitToContent, hubId);
}
};

Expand Down
9 changes: 5 additions & 4 deletions packages/stores/src/canvasViewportStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export type ProcessHubId = ProcessHub['id'];
export type NodeId = string;
export type TributaryId = string;
export type GateNodePath = string;
export type CanvasViewportFit = { zoom: number; pan: { x: number; y: number } };

export interface CanvasViewportSnapshot {
zoom: number;
Expand Down Expand Up @@ -88,7 +89,7 @@ export interface CanvasViewportActions {
setZoom: (hubId: ProcessHubId, zoom: number) => void;
setPan: (hubId: ProcessHubId, pan: { x: number; y: number }) => void;
setLevel: (hubId: ProcessHubId, level: CanvasLevel, focalStepId?: string) => void;
fitToContent: (hubId: ProcessHubId, level?: CanvasLevel) => void;
fitToContent: (hubId: ProcessHubId, level?: CanvasLevel, fit?: CanvasViewportFit) => void;
toggleRail: () => void;
setRailOpen: (open: boolean) => void;
setGroupByTributary: (hubId: ProcessHubId, on: boolean) => void;
Expand Down Expand Up @@ -185,7 +186,7 @@ export const useCanvasViewportStore = create<CanvasViewportState & CanvasViewpor
setViewportLevel(viewport, level, focalStepId)
),
})),
fitToContent: (hubId, level) =>
fitToContent: (hubId, level, fit) =>
set(s => ({
viewports: withViewport(s.viewports, hubId, viewport => {
const targetLevel =
Expand All @@ -196,8 +197,8 @@ export const useCanvasViewportStore = create<CanvasViewportState & CanvasViewpor
const nextViewport = setViewportLevel(viewport, targetLevel, viewport.focalStepId);
return {
...nextViewport,
zoom: FIT_TO_CONTENT_ZOOM_BY_LEVEL[targetLevel],
pan: { x: 0, y: 0 },
zoom: fit?.zoom ?? FIT_TO_CONTENT_ZOOM_BY_LEVEL[targetLevel],
pan: fit?.pan ?? { x: 0, y: 0 },
};
}),
})),
Expand Down
1 change: 1 addition & 0 deletions packages/stores/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export type {
CanvasViewportState,
CanvasViewportActions,
CanvasViewportSnapshot,
CanvasViewportFit,
ChartClusterState,
AndCheckSnapshot,
PendingComment,
Expand Down
Loading