@adcops/autocore-react 3.3.89 → 3.3.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/JogXNeg.d.ts +4 -0
- package/dist/assets/JogXNeg.d.ts.map +1 -0
- package/dist/assets/JogXNeg.js +1 -0
- package/dist/assets/JogXPos.d.ts +4 -0
- package/dist/assets/JogXPos.d.ts.map +1 -0
- package/dist/assets/JogXPos.js +1 -0
- package/dist/assets/JogYNeg.d.ts +4 -0
- package/dist/assets/JogYNeg.d.ts.map +1 -0
- package/dist/assets/JogYNeg.js +1 -0
- package/dist/assets/JogYPos.d.ts +4 -0
- package/dist/assets/JogYPos.d.ts.map +1 -0
- package/dist/assets/JogYPos.js +1 -0
- package/dist/assets/JogZNeg.d.ts +4 -0
- package/dist/assets/JogZNeg.d.ts.map +1 -0
- package/dist/assets/JogZNeg.js +1 -0
- package/dist/assets/JogZPos.d.ts +4 -0
- package/dist/assets/JogZPos.d.ts.map +1 -0
- package/dist/assets/JogZPos.js +1 -0
- package/dist/assets/Off.d.ts +4 -0
- package/dist/assets/Off.d.ts.map +1 -0
- package/dist/assets/Off.js +1 -0
- package/dist/assets/On.d.ts +4 -0
- package/dist/assets/On.d.ts.map +1 -0
- package/dist/assets/On.js +1 -0
- package/dist/assets/index.d.ts +8 -0
- package/dist/assets/index.d.ts.map +1 -1
- package/dist/assets/index.js +1 -1
- package/dist/assets/svg/off.svg +2 -0
- package/dist/assets/svg/on.svg +11 -0
- package/dist/components/JogPanel.d.ts +2 -2
- package/dist/components/JogPanel.d.ts.map +1 -1
- package/dist/components/JogPanel.js +1 -1
- package/dist/components/ams/AssetDetailView.js +1 -1
- package/dist/components/ams/AssetEditDialog.d.ts.map +1 -1
- package/dist/components/ams/AssetEditDialog.js +1 -1
- package/dist/components/ams/AssetRegistryTable.css +12 -0
- package/dist/components/ams/AssetRegistryTable.d.ts +1 -0
- package/dist/components/ams/AssetRegistryTable.d.ts.map +1 -1
- package/dist/components/ams/AssetRegistryTable.js +1 -1
- package/dist/components/tis/ConfigurationDialog.d.ts +21 -0
- package/dist/components/tis/ConfigurationDialog.d.ts.map +1 -0
- package/dist/components/tis/ConfigurationDialog.js +1 -0
- package/dist/components/tis/ResultHistoryTable.js +1 -1
- package/dist/components/tis/TestDataView.d.ts +47 -0
- package/dist/components/tis/TestDataView.d.ts.map +1 -1
- package/dist/components/tis/TestDataView.js +1 -1
- package/dist/components/tis/TestSetupForm.d.ts +37 -0
- package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
- package/dist/components/tis/TestSetupForm.js +1 -1
- package/dist/components/tis/TisProvider.d.ts +25 -0
- package/dist/components/tis/TisProvider.d.ts.map +1 -1
- package/dist/components/tis/TisProvider.js +1 -1
- package/dist/components/tis/useRawCycleData.d.ts.map +1 -1
- package/dist/components/tis/useRawCycleData.js +1 -1
- package/dist/components/tis-editor/TisConfigEditor.css +20 -0
- package/dist/components/tis-editor/editor/ConfigurationsEditor.d.ts +19 -0
- package/dist/components/tis-editor/editor/ConfigurationsEditor.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/ConfigurationsEditor.js +1 -0
- package/dist/components/tis-editor/editor/MethodFormEditor.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/MethodFormEditor.js +1 -1
- package/dist/components/tis-editor/types.d.ts +13 -0
- package/dist/components/tis-editor/types.d.ts.map +1 -1
- package/dist/components/tis-editor/validation.d.ts.map +1 -1
- package/dist/components/tis-editor/validation.js +1 -1
- package/dist/themes/adc-dark/blue/theme.css +17 -2
- package/dist/themes/adc-dark/blue/theme.css.map +1 -1
- package/package.json +2 -1
- package/src/assets/JogXNeg.tsx +30 -0
- package/src/assets/JogXPos.tsx +30 -0
- package/src/assets/JogYNeg.tsx +30 -0
- package/src/assets/JogYPos.tsx +30 -0
- package/src/assets/JogZNeg.tsx +30 -0
- package/src/assets/JogZPos.tsx +30 -0
- package/src/assets/Off.tsx +14 -0
- package/src/assets/On.tsx +26 -0
- package/src/assets/index.ts +8 -0
- package/src/assets/svg/off.svg +2 -0
- package/src/assets/svg/on.svg +11 -0
- package/src/components/JogPanel.tsx +18 -28
- package/src/components/ams/AssetDetailView.tsx +1 -1
- package/src/components/ams/AssetEditDialog.tsx +25 -10
- package/src/components/ams/AssetRegistryTable.css +12 -0
- package/src/components/ams/AssetRegistryTable.tsx +15 -4
- package/src/components/tis/ConfigurationDialog.tsx +128 -0
- package/src/components/tis/ResultHistoryTable.tsx +2 -2
- package/src/components/tis/TestDataView.tsx +270 -12
- package/src/components/tis/TestSetupForm.tsx +167 -10
- package/src/components/tis/TisProvider.tsx +53 -0
- package/src/components/tis/useRawCycleData.ts +22 -3
- package/src/components/tis-editor/TisConfigEditor.css +20 -0
- package/src/components/tis-editor/editor/ConfigurationsEditor.tsx +242 -0
- package/src/components/tis-editor/editor/MethodFormEditor.tsx +4 -0
- package/src/components/tis-editor/types.ts +14 -0
- package/src/components/tis-editor/validation.ts +29 -0
- package/src/themes/adc-dark/_extensions.scss +20 -0
- package/src/themes/theme-base/components/panel/_fieldset.scss +2 -2
|
@@ -37,6 +37,7 @@ import { Chart as ChartJS,
|
|
|
37
37
|
Title, Tooltip, Legend,
|
|
38
38
|
} from 'chart.js';
|
|
39
39
|
import zoomPlugin from 'chartjs-plugin-zoom';
|
|
40
|
+
import annotationPlugin from 'chartjs-plugin-annotation';
|
|
40
41
|
import { Line } from 'react-chartjs-2';
|
|
41
42
|
|
|
42
43
|
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
@@ -46,7 +47,7 @@ import { useRawCycleData } from './useRawCycleData';
|
|
|
46
47
|
|
|
47
48
|
ChartJS.register(
|
|
48
49
|
CategoryScale, LinearScale, PointElement, LineElement,
|
|
49
|
-
Title, Tooltip, Legend, zoomPlugin,
|
|
50
|
+
Title, Tooltip, Legend, zoomPlugin, annotationPlugin,
|
|
50
51
|
);
|
|
51
52
|
|
|
52
53
|
// -------------------------------------------------------------------------
|
|
@@ -69,11 +70,58 @@ export interface TestFieldDef {
|
|
|
69
70
|
|
|
70
71
|
export interface ChartAxis { field?: string; column?: string; label?: string; }
|
|
71
72
|
export interface ChartSeries { field?: string; column?: string; label?: string; y_axis?: 'left' | 'right'; }
|
|
73
|
+
/**
|
|
74
|
+
* A shaded X-range band drawn over the plot (rendered as a
|
|
75
|
+
* chartjs-plugin-annotation `box` annotation spanning the full Y height).
|
|
76
|
+
* `xMin`/`xMax` are in the same units as the view's x data — for a
|
|
77
|
+
* `raw_trace` that's the x column's value, for a `cycle_scatter` it's the
|
|
78
|
+
* category index. Used to indicate a processed / region-of-interest span.
|
|
79
|
+
*/
|
|
80
|
+
export interface ChartRegion {
|
|
81
|
+
/** Start of the band on the X axis. */
|
|
82
|
+
xMin: number;
|
|
83
|
+
/** End of the band on the X axis. */
|
|
84
|
+
xMax: number;
|
|
85
|
+
/** Optional caption drawn at the top-center of the band. */
|
|
86
|
+
label?: string;
|
|
87
|
+
/** Fill color (any CSS color). Default: translucent theme accent. */
|
|
88
|
+
color?: string;
|
|
89
|
+
/** Optional band-edge border color. Default: no border. */
|
|
90
|
+
borderColor?: string;
|
|
91
|
+
}
|
|
72
92
|
export interface ChartView {
|
|
73
93
|
title?: string;
|
|
74
94
|
type: 'cycle_scatter' | 'raw_trace';
|
|
75
95
|
x: ChartAxis;
|
|
76
96
|
y: ChartSeries[];
|
|
97
|
+
/**
|
|
98
|
+
* Display-only line smoothing for `raw_trace` views. `undefined ⇒ true`.
|
|
99
|
+
*
|
|
100
|
+
* When on, each series is passed through a Savitzky–Golay filter (which
|
|
101
|
+
* removes sample-to-sample noise while keeping peak height/shape),
|
|
102
|
+
* lightly decimated, and drawn with monotone-cubic interpolation — a
|
|
103
|
+
* smooth curve that traces the profile instead of a squiggly polyline.
|
|
104
|
+
* The underlying `raw` data is never altered (table, CSV, and any
|
|
105
|
+
* calculations still see the exact samples). Set `false` to render the
|
|
106
|
+
* raw polyline through every sample.
|
|
107
|
+
*/
|
|
108
|
+
smooth?: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Optional Savitzky–Golay window size in samples (odd; clamped to the
|
|
111
|
+
* series length). Larger = smoother. Default is adaptive to the sample
|
|
112
|
+
* count. Ignored when `smooth` is false.
|
|
113
|
+
*/
|
|
114
|
+
smoothWindow?: number;
|
|
115
|
+
/**
|
|
116
|
+
* Optional shaded X-range bands drawn over the plot, declared per view
|
|
117
|
+
* in project.json (static). These are drawn on every cycle.
|
|
118
|
+
*
|
|
119
|
+
* For per-cycle dynamic bands (computed at runtime from processed
|
|
120
|
+
* data), the producer instead attaches a `regions` array to the
|
|
121
|
+
* raw-data envelope; the component merges those with these and renders
|
|
122
|
+
* both. See `dynamicRegions` in the component body.
|
|
123
|
+
*/
|
|
124
|
+
regions?: ChartRegion[];
|
|
77
125
|
}
|
|
78
126
|
export interface RawColumn { source: string; }
|
|
79
127
|
export interface RawDataShape {
|
|
@@ -325,16 +373,28 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
325
373
|
const xCol = selectedViewDef.x.column;
|
|
326
374
|
if (!xCol) return null;
|
|
327
375
|
const xs = traceFetch.raw[xCol] ?? [];
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
376
|
+
// Display-only smoothing (default on). Leaves traceFetch.raw
|
|
377
|
+
// untouched — only what chart.js draws changes.
|
|
378
|
+
const smoothOn = selectedViewDef.smooth !== false;
|
|
379
|
+
const datasets = selectedViewDef.y.map((s, idx) => {
|
|
380
|
+
const ys = (traceFetch.raw![s.column!] ?? []) as number[];
|
|
381
|
+
const data = smoothOn
|
|
382
|
+
? smoothTraceForDisplay(xs, ys, selectedViewDef.smoothWindow)
|
|
383
|
+
: ys.map((y, i) => ({ x: xs[i], y }));
|
|
384
|
+
return {
|
|
385
|
+
label: s.label ?? s.column,
|
|
386
|
+
data,
|
|
387
|
+
yAxisID: s.y_axis === 'right' ? 'y1' : 'y',
|
|
388
|
+
borderColor: palette(idx),
|
|
389
|
+
backgroundColor: palette(idx),
|
|
390
|
+
pointRadius: 0,
|
|
391
|
+
borderWidth: 1.5,
|
|
392
|
+
showLine: true,
|
|
393
|
+
// Smooth curve through the (already de-noised) points;
|
|
394
|
+
// 'monotone' avoids the overshoot a plain tension adds.
|
|
395
|
+
...(smoothOn ? { cubicInterpolationMode: 'monotone' as const } : {}),
|
|
396
|
+
};
|
|
397
|
+
});
|
|
338
398
|
return { datasets };
|
|
339
399
|
}
|
|
340
400
|
return null;
|
|
@@ -342,6 +402,28 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
342
402
|
|
|
343
403
|
const usesRightAxis = selectedViewDef?.y.some(s => s.y_axis === 'right') ?? false;
|
|
344
404
|
|
|
405
|
+
// Per-cycle region bands carried in the raw-data envelope — the dynamic
|
|
406
|
+
// case: bands computed at runtime from processed data, changing each
|
|
407
|
+
// cycle. The hook re-fetches the envelope whenever the selected cycle
|
|
408
|
+
// changes, so these update automatically with the cycle picker.
|
|
409
|
+
//
|
|
410
|
+
// Read a top-level `regions` field first; fall back to `context.regions`
|
|
411
|
+
// so a producer can attach bands today (the envelope's `context` is
|
|
412
|
+
// already passed straight through by the server) without waiting on a
|
|
413
|
+
// server-side envelope-shape change.
|
|
414
|
+
const dynamicRegions = useMemo<ChartRegion[]>(() => {
|
|
415
|
+
if (!isRawTraceView) return [];
|
|
416
|
+
const env: any = traceFetch.envelope;
|
|
417
|
+
const r = env?.regions ?? env?.context?.regions;
|
|
418
|
+
return Array.isArray(r) ? r : [];
|
|
419
|
+
}, [isRawTraceView, traceFetch.envelope]);
|
|
420
|
+
|
|
421
|
+
// Static (schema-declared) bands + dynamic (per-cycle) bands, drawn together.
|
|
422
|
+
const activeRegions = useMemo<ChartRegion[]>(
|
|
423
|
+
() => [...(selectedViewDef?.regions ?? []), ...dynamicRegions],
|
|
424
|
+
[selectedViewDef, dynamicRegions],
|
|
425
|
+
);
|
|
426
|
+
|
|
345
427
|
const chartOptions = useMemo(() => {
|
|
346
428
|
const isTrace = selectedViewDef?.type === 'raw_trace';
|
|
347
429
|
return {
|
|
@@ -375,9 +457,15 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
375
457
|
mode: 'xy' as const,
|
|
376
458
|
},
|
|
377
459
|
},
|
|
460
|
+
// Region bands: the view's static `regions` declaration plus
|
|
461
|
+
// any per-cycle bands carried in the raw-data envelope, merged
|
|
462
|
+
// upstream into `activeRegions`.
|
|
463
|
+
annotation: {
|
|
464
|
+
annotations: buildRegionAnnotations(activeRegions),
|
|
465
|
+
},
|
|
378
466
|
},
|
|
379
467
|
};
|
|
380
|
-
}, [selectedViewDef, usesRightAxis]);
|
|
468
|
+
}, [selectedViewDef, usesRightAxis, activeRegions]);
|
|
381
469
|
|
|
382
470
|
// -----------------------------------------------------------------
|
|
383
471
|
// View Raw Data dialog: lazy-fetch raw + filtered blobs the first
|
|
@@ -1010,6 +1098,176 @@ const CHART_COLORS = [
|
|
|
1010
1098
|
];
|
|
1011
1099
|
const palette = (i: number) => CHART_COLORS[i % CHART_COLORS.length];
|
|
1012
1100
|
|
|
1101
|
+
// =========================================================================
|
|
1102
|
+
// Display-only trace smoothing (Savitzky–Golay + decimation)
|
|
1103
|
+
//
|
|
1104
|
+
// Why SG: a sliding low-order polynomial fit removes sample-to-sample noise
|
|
1105
|
+
// while preserving the height and width of real features (peaks) far better
|
|
1106
|
+
// than a moving average, which pulls peaks toward the local mean. This is
|
|
1107
|
+
// purely cosmetic — it runs on a copy of the y-values when building the
|
|
1108
|
+
// chart dataset and never touches `traceFetch.raw`, so the raw table, CSV
|
|
1109
|
+
// export, and any downstream math see the exact samples.
|
|
1110
|
+
// =========================================================================
|
|
1111
|
+
|
|
1112
|
+
// Cubic fit: preserves peak curvature better than linear/quadratic.
|
|
1113
|
+
const SG_ORDER = 3;
|
|
1114
|
+
// Target point count after decimation. The smoothed series is broad, so
|
|
1115
|
+
// uniform decimation to ~this many points renders cheaply and looks
|
|
1116
|
+
// identical to drawing all 5000.
|
|
1117
|
+
const SMOOTH_TARGET_POINTS = 800;
|
|
1118
|
+
|
|
1119
|
+
// Invert a small square matrix via Gauss–Jordan. Returns null if singular.
|
|
1120
|
+
function invertMatrix(M: number[][]): number[][] | null {
|
|
1121
|
+
const n = M.length;
|
|
1122
|
+
const A = M.map((row, i) =>
|
|
1123
|
+
[...row, ...Array.from({ length: n }, (_, j) => (i === j ? 1 : 0))]);
|
|
1124
|
+
for (let col = 0; col < n; col++) {
|
|
1125
|
+
let piv = col;
|
|
1126
|
+
for (let r = col + 1; r < n; r++)
|
|
1127
|
+
if (Math.abs(A[r][col]) > Math.abs(A[piv][col])) piv = r;
|
|
1128
|
+
if (Math.abs(A[piv][col]) < 1e-12) return null;
|
|
1129
|
+
[A[col], A[piv]] = [A[piv], A[col]];
|
|
1130
|
+
const d = A[col][col];
|
|
1131
|
+
for (let j = 0; j < 2 * n; j++) A[col][j] /= d;
|
|
1132
|
+
for (let r = 0; r < n; r++) {
|
|
1133
|
+
if (r === col) continue;
|
|
1134
|
+
const f = A[r][col];
|
|
1135
|
+
if (f === 0) continue;
|
|
1136
|
+
for (let j = 0; j < 2 * n; j++) A[r][j] -= f * A[col][j];
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
return A.map(row => row.slice(n));
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// Savitzky–Golay smoothing weights for a window of half-width `h`
|
|
1143
|
+
// (size 2h+1) and polynomial `order`. The center smoothed value is the
|
|
1144
|
+
// constant term of the local least-squares polynomial fit, i.e. row 0 of
|
|
1145
|
+
// (AᵀA)⁻¹Aᵀ. Weights depend only on (h, order) → computed once, cached.
|
|
1146
|
+
function sgWeights(h: number, order: number): number[] {
|
|
1147
|
+
const win = 2 * h + 1;
|
|
1148
|
+
const ord = Math.min(order, win - 1);
|
|
1149
|
+
const A: number[][] = [];
|
|
1150
|
+
for (let z = -h; z <= h; z++) {
|
|
1151
|
+
const row: number[] = [];
|
|
1152
|
+
let zp = 1;
|
|
1153
|
+
for (let c = 0; c <= ord; c++) { row.push(zp); zp *= z; }
|
|
1154
|
+
A.push(row);
|
|
1155
|
+
}
|
|
1156
|
+
const m = ord + 1;
|
|
1157
|
+
const M: number[][] = Array.from({ length: m }, () => new Array(m).fill(0));
|
|
1158
|
+
for (let a = 0; a < m; a++)
|
|
1159
|
+
for (let b = 0; b < m; b++) {
|
|
1160
|
+
let s = 0;
|
|
1161
|
+
for (let r = 0; r < win; r++) s += A[r][a] * A[r][b];
|
|
1162
|
+
M[a][b] = s;
|
|
1163
|
+
}
|
|
1164
|
+
const Minv = invertMatrix(M);
|
|
1165
|
+
if (!Minv) return new Array(win).fill(1 / win); // degenerate → flat mean
|
|
1166
|
+
const w = new Array<number>(win);
|
|
1167
|
+
for (let r = 0; r < win; r++) {
|
|
1168
|
+
let s = 0;
|
|
1169
|
+
for (let a = 0; a < m; a++) s += Minv[0][a] * A[r][a];
|
|
1170
|
+
w[r] = s;
|
|
1171
|
+
}
|
|
1172
|
+
return w;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Apply SG smoothing to a y-series. Near the ends the window shrinks
|
|
1176
|
+
// symmetrically (h = distance to the nearest edge) so the filter stays
|
|
1177
|
+
// centered and no phantom data is invented past the trace boundaries.
|
|
1178
|
+
function savitzkyGolay(ys: number[], halfWidth: number): number[] {
|
|
1179
|
+
const n = ys.length;
|
|
1180
|
+
if (n === 0 || halfWidth < 1) return ys.slice();
|
|
1181
|
+
const cache = new Map<number, number[]>();
|
|
1182
|
+
const weightsFor = (h: number) => {
|
|
1183
|
+
let w = cache.get(h);
|
|
1184
|
+
if (!w) { w = sgWeights(h, SG_ORDER); cache.set(h, w); }
|
|
1185
|
+
return w;
|
|
1186
|
+
};
|
|
1187
|
+
const out = new Array<number>(n);
|
|
1188
|
+
for (let i = 0; i < n; i++) {
|
|
1189
|
+
const h = Math.min(halfWidth, i, n - 1 - i);
|
|
1190
|
+
if (h < 1) { out[i] = ys[i]; continue; }
|
|
1191
|
+
const w = weightsFor(h);
|
|
1192
|
+
let s = 0;
|
|
1193
|
+
for (let k = -h; k <= h; k++) s += w[k + h] * ys[i + k];
|
|
1194
|
+
out[i] = s;
|
|
1195
|
+
}
|
|
1196
|
+
return out;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// Build the {x, y} points chart.js draws for a smoothed raw_trace series:
|
|
1200
|
+
// SG-denoise, then uniformly decimate the broad result to SMOOTH_TARGET_POINTS.
|
|
1201
|
+
function smoothTraceForDisplay(
|
|
1202
|
+
xs: number[],
|
|
1203
|
+
ys: number[],
|
|
1204
|
+
window?: number,
|
|
1205
|
+
): { x: number; y: number }[] {
|
|
1206
|
+
const n = Math.min(xs.length, ys.length);
|
|
1207
|
+
if (n === 0) return [];
|
|
1208
|
+
// Adaptive default half-width: wide enough to suppress noise, narrow
|
|
1209
|
+
// enough to keep features. An explicit `smoothWindow` (full odd size)
|
|
1210
|
+
// overrides it.
|
|
1211
|
+
const half = window && window > 1
|
|
1212
|
+
? Math.max(1, Math.floor((Math.min(window, n) - 1) / 2))
|
|
1213
|
+
: Math.max(2, Math.min(40, Math.round(n / 250)));
|
|
1214
|
+
const sm = savitzkyGolay(ys.slice(0, n), half);
|
|
1215
|
+
const stride = Math.max(1, Math.ceil(n / SMOOTH_TARGET_POINTS));
|
|
1216
|
+
const out: { x: number; y: number }[] = [];
|
|
1217
|
+
for (let i = 0; i < n; i += stride) out.push({ x: xs[i], y: sm[i] });
|
|
1218
|
+
// Always anchor the final sample so the curve reaches the trace end.
|
|
1219
|
+
if ((n - 1) % stride !== 0) out.push({ x: xs[n - 1], y: sm[n - 1] });
|
|
1220
|
+
return out;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// Default translucent fill for region bands (theme accent at low alpha).
|
|
1224
|
+
const DEFAULT_REGION_FILL = 'rgba(78, 168, 222, 0.15)';
|
|
1225
|
+
// Label color must be a concrete canvas color — CSS variables don't
|
|
1226
|
+
// resolve when chart.js paints to the 2D context.
|
|
1227
|
+
const REGION_LABEL_COLOR = 'rgba(226, 232, 240, 0.85)';
|
|
1228
|
+
|
|
1229
|
+
/**
|
|
1230
|
+
* Translate the view's `regions` into chartjs-plugin-annotation `box`
|
|
1231
|
+
* annotations keyed by a stable id. Each band spans the full Y height
|
|
1232
|
+
* (yMin/yMax left unset) between `xMin` and `xMax` on the x scale.
|
|
1233
|
+
* Returns an empty map when there are no regions, which the plugin
|
|
1234
|
+
* treats as "draw nothing".
|
|
1235
|
+
*
|
|
1236
|
+
* This is the single seam through which both the static schema path and a
|
|
1237
|
+
* future dynamic (processed-data) path produce annotations.
|
|
1238
|
+
*/
|
|
1239
|
+
function buildRegionAnnotations(regions?: ChartRegion[]): Record<string, any> {
|
|
1240
|
+
if (!regions || regions.length === 0) return {};
|
|
1241
|
+
const out: Record<string, any> = {};
|
|
1242
|
+
regions.forEach((r, i) => {
|
|
1243
|
+
const hasBorder = !!r.borderColor;
|
|
1244
|
+
out[`region-${i}`] = {
|
|
1245
|
+
type: 'box',
|
|
1246
|
+
xScaleID: 'x',
|
|
1247
|
+
xMin: r.xMin,
|
|
1248
|
+
xMax: r.xMax,
|
|
1249
|
+
// yMin/yMax intentionally omitted → band spans the plot height.
|
|
1250
|
+
backgroundColor: r.color ?? DEFAULT_REGION_FILL,
|
|
1251
|
+
borderColor: hasBorder ? r.borderColor : 'transparent',
|
|
1252
|
+
borderWidth: hasBorder ? 1 : 0,
|
|
1253
|
+
// Sit behind the data line so the trace stays readable.
|
|
1254
|
+
drawTime: 'beforeDatasetsDraw' as const,
|
|
1255
|
+
...(r.label
|
|
1256
|
+
? {
|
|
1257
|
+
label: {
|
|
1258
|
+
display: true,
|
|
1259
|
+
content: r.label,
|
|
1260
|
+
position: { x: 'center' as const, y: 'start' as const },
|
|
1261
|
+
color: REGION_LABEL_COLOR,
|
|
1262
|
+
font: { size: 11 },
|
|
1263
|
+
},
|
|
1264
|
+
}
|
|
1265
|
+
: {}),
|
|
1266
|
+
};
|
|
1267
|
+
});
|
|
1268
|
+
return out;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1013
1271
|
// Loading / error wash drawn over the chart area while a raw_trace
|
|
1014
1272
|
// fetch is in flight. Centered, pointer-events-none so the operator
|
|
1015
1273
|
// can still interact with the dropdown above.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect, useContext, useMemo
|
|
1
|
+
import React, { useState, useEffect, useContext, useMemo } from 'react';
|
|
2
2
|
import { Button } from 'primereact/button';
|
|
3
3
|
import { InputText } from 'primereact/inputtext';
|
|
4
4
|
import { Dropdown } from 'primereact/dropdown';
|
|
@@ -11,6 +11,7 @@ import { TextInput } from '../TextInput';
|
|
|
11
11
|
import { useTis } from './TisProvider';
|
|
12
12
|
import { useAmsAssets, useAmsRoles, type AmsAssetEntry } from '../ams/AmsProvider';
|
|
13
13
|
import { TestMethodDialog } from './TestMethodDialog';
|
|
14
|
+
import { ConfigurationDialog, configLabelOf } from './ConfigurationDialog';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* One asset_ref declared on a test method. We only consume the subset
|
|
@@ -59,6 +60,19 @@ export interface TestFieldDef {
|
|
|
59
60
|
* Cycle and results values are scaled by the corresponding paths
|
|
60
61
|
* in TestDataView; the server scales CSV exports too. */
|
|
61
62
|
scale?: number;
|
|
63
|
+
/** Optional fixed set of choices. When present, the field renders
|
|
64
|
+
* as a dropdown and the operator must pick one of the declared
|
|
65
|
+
* values rather than typing freely. Each entry is either a bare
|
|
66
|
+
* scalar (label === value) or an explicit `{ label, value }` pair
|
|
67
|
+
* when the displayed text should differ from the stored value.
|
|
68
|
+
* Works with any `type`; like `default`, values are authored in
|
|
69
|
+
* display units when `scale` is set. */
|
|
70
|
+
options?: Array<
|
|
71
|
+
| string
|
|
72
|
+
| number
|
|
73
|
+
| boolean
|
|
74
|
+
| { label?: string; value: string | number | boolean }
|
|
75
|
+
>;
|
|
62
76
|
}
|
|
63
77
|
|
|
64
78
|
export interface TestMethod {
|
|
@@ -90,6 +104,33 @@ export interface TestMethod {
|
|
|
90
104
|
* as `analysis` — the form ignores them; the type just has to
|
|
91
105
|
* accept them so generated schemas typecheck. */
|
|
92
106
|
views?: Record<string, any>;
|
|
107
|
+
/**
|
|
108
|
+
* Optional named configurations for this method. When one or more
|
|
109
|
+
* are declared, the form renders a Configuration selector beneath
|
|
110
|
+
* the Test Method picker. Selecting (or accepting) a configuration
|
|
111
|
+
* writes its `defaults` into the matching config_fields. Each
|
|
112
|
+
* configuration only needs to declare the fields whose value is
|
|
113
|
+
* specific to it — unlisted fields keep the method's base default.
|
|
114
|
+
*/
|
|
115
|
+
configurations?: TestConfiguration[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* One named, ready-to-use set of config_field overrides for a method —
|
|
120
|
+
* e.g. translational_traction's "Plaque" vs "Shoe". `defaults` is a
|
|
121
|
+
* sparse map of `config_field.name → value`; values are authored in
|
|
122
|
+
* display units (the form converts via the field's `scale` before
|
|
123
|
+
* writing to stagedConfig / GM, same convention as `TestFieldDef.default`).
|
|
124
|
+
*/
|
|
125
|
+
export interface TestConfiguration {
|
|
126
|
+
/** Canonical key — unique within the method. */
|
|
127
|
+
name: string;
|
|
128
|
+
/** Pretty label shown in the Configuration picker. Falls back to `name`. */
|
|
129
|
+
label?: string;
|
|
130
|
+
/** Long-form guidance shown beside the dropdown in the picker dialog. */
|
|
131
|
+
description?: string;
|
|
132
|
+
/** Sparse `config_field.name → value` overrides applied on selection. */
|
|
133
|
+
defaults?: Record<string, any>;
|
|
93
134
|
}
|
|
94
135
|
|
|
95
136
|
/**
|
|
@@ -145,6 +186,20 @@ const displayToRaw = (display: any, scale: number | undefined): any => {
|
|
|
145
186
|
const hasDescription = (f: TestFieldDef): boolean =>
|
|
146
187
|
typeof f.description === 'string' && f.description.length > 0;
|
|
147
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Normalise a field's `options` (bare scalars and/or `{label, value}`
|
|
191
|
+
* pairs) into the `{ label, value }[]` shape PrimeReact's Dropdown
|
|
192
|
+
* wants. Bare scalars use their stringified form as the label.
|
|
193
|
+
*/
|
|
194
|
+
const normalizeOptions = (
|
|
195
|
+
opts: TestFieldDef['options'],
|
|
196
|
+
): { label: string; value: string | number | boolean }[] =>
|
|
197
|
+
(opts ?? []).map((o) =>
|
|
198
|
+
o !== null && typeof o === 'object'
|
|
199
|
+
? { label: String(o.label ?? o.value), value: o.value }
|
|
200
|
+
: { label: String(o), value: o },
|
|
201
|
+
);
|
|
202
|
+
|
|
148
203
|
const methodLabelOf = (methodId: string, schema: TestMethod | undefined): string =>
|
|
149
204
|
(schema?.label && schema.label.length > 0) ? schema.label : methodId;
|
|
150
205
|
|
|
@@ -240,6 +295,28 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
240
295
|
|
|
241
296
|
const schema = schemaOverride ?? (methodId ? tis.schemas[methodId] : undefined);
|
|
242
297
|
|
|
298
|
+
// Write one configuration's sparse `defaults` overrides into a config
|
|
299
|
+
// object in place (returns the same object for chaining). Mirrors the
|
|
300
|
+
// base-default seeding below: values are authored in display units, so
|
|
301
|
+
// we convert to raw before storing; source-bound fields are also
|
|
302
|
+
// pushed to GM so the control program sees them. Used both by the
|
|
303
|
+
// auto-apply-first effect and by the picker dialog's accept handler.
|
|
304
|
+
const applyConfigOverridesInto = (cfg: TestConfiguration, target: any): any => {
|
|
305
|
+
for (const [fieldName, val] of Object.entries(cfg.defaults ?? {})) {
|
|
306
|
+
if (fieldName === 'sample_id') continue;
|
|
307
|
+
const field = schema?.config_fields.find((f: TestFieldDef) => f.name === fieldName);
|
|
308
|
+
const rawVal = displayToRaw(val, field?.scale);
|
|
309
|
+
target[fieldName] = rawVal;
|
|
310
|
+
if (field?.source) {
|
|
311
|
+
void Promise.resolve()
|
|
312
|
+
.then(() => write(field.source!, rawVal))
|
|
313
|
+
.catch(e => console.error(
|
|
314
|
+
`[TestSetupForm] Failed to apply configuration override for ${fieldName}:`, e));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return target;
|
|
318
|
+
};
|
|
319
|
+
|
|
243
320
|
useEffect(() => {
|
|
244
321
|
if (tis.selection.methodId !== methodId && methodId) {
|
|
245
322
|
tis.setSelection({ methodId });
|
|
@@ -274,6 +351,19 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
274
351
|
|
|
275
352
|
const [isValid, setIsValid] = useState(false);
|
|
276
353
|
const [methodPickerOpen, setMethodPickerOpen] = useState(false);
|
|
354
|
+
const [configPickerOpen, setConfigPickerOpen] = useState(false);
|
|
355
|
+
|
|
356
|
+
const configurations = (schema?.configurations ?? []) as TestConfiguration[];
|
|
357
|
+
const selectedConfig = configurations.find(c => c.name === tis.configurationName);
|
|
358
|
+
|
|
359
|
+
// Operator accepted a configuration in the picker dialog — write its
|
|
360
|
+
// overrides into the fields and remember the selection.
|
|
361
|
+
const handleConfigurationSelected = (configName: string) => {
|
|
362
|
+
const cfg = configurations.find(c => c.name === configName);
|
|
363
|
+
if (!cfg) return;
|
|
364
|
+
setConfig((prev: any) => applyConfigOverridesInto(cfg, { ...prev }));
|
|
365
|
+
tis.setConfigurationName(configName);
|
|
366
|
+
};
|
|
277
367
|
|
|
278
368
|
// Single shared "info" dialog used by:
|
|
279
369
|
// - the Test Setup status button (shows what's wrong, including
|
|
@@ -288,14 +378,23 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
288
378
|
// Apply schema-declared defaults when the operator picks a method.
|
|
289
379
|
// Source-bound fields write the default to GM (the control program
|
|
290
380
|
// is the consumer of record); non-source fields land directly in
|
|
291
|
-
// stagedConfig.
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
|
|
381
|
+
// stagedConfig. The "already seeded" marker lives on the provider
|
|
382
|
+
// (tis.defaultsAppliedForMethod), not in a local ref, so it
|
|
383
|
+
// survives this form unmounting when the operator switches tabs —
|
|
384
|
+
// otherwise defaults would re-apply on every Test-tab return and
|
|
385
|
+
// clobber the operator's edits. Only an actual method change (or
|
|
386
|
+
// clearStagedConfig() after start_test) re-applies the defaults.
|
|
295
387
|
useEffect(() => {
|
|
296
388
|
if (!schema || !methodId) return;
|
|
297
|
-
if (
|
|
298
|
-
|
|
389
|
+
if (tis.defaultsAppliedForMethod === methodId) return;
|
|
390
|
+
tis.markDefaultsAppliedForMethod(methodId);
|
|
391
|
+
|
|
392
|
+
// Auto-apply the method's first configuration (if any) on top of
|
|
393
|
+
// the base defaults. '' when the method declares none, which also
|
|
394
|
+
// clears any stale selection carried over from a previous method.
|
|
395
|
+
const firstConfig = (schema.configurations && schema.configurations.length > 0)
|
|
396
|
+
? schema.configurations[0] as TestConfiguration
|
|
397
|
+
: undefined;
|
|
299
398
|
|
|
300
399
|
setConfig((prev: any) => {
|
|
301
400
|
let next = prev;
|
|
@@ -321,9 +420,17 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
321
420
|
`[TestSetupForm] Failed to seed default for ${field.name}:`, e));
|
|
322
421
|
}
|
|
323
422
|
}
|
|
423
|
+
// Configuration overrides win over base defaults for the
|
|
424
|
+
// fields they name (e.g. Plaque's z_start_position).
|
|
425
|
+
if (firstConfig) {
|
|
426
|
+
if (next === prev) next = { ...prev };
|
|
427
|
+
next = applyConfigOverridesInto(firstConfig, next);
|
|
428
|
+
}
|
|
324
429
|
return next;
|
|
325
430
|
});
|
|
326
|
-
|
|
431
|
+
|
|
432
|
+
tis.setConfigurationName(firstConfig ? firstConfig.name : '');
|
|
433
|
+
}, [schema, methodId, write, tis.defaultsAppliedForMethod, tis.markDefaultsAppliedForMethod, tis.setConfigurationName]);
|
|
327
434
|
|
|
328
435
|
// Seed and live-update config_fields that declare a `source`.
|
|
329
436
|
useEffect(() => {
|
|
@@ -451,7 +558,14 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
451
558
|
if (field.name === 'sample_id') return null;
|
|
452
559
|
const valid = isFieldValid(field);
|
|
453
560
|
const assetType = assetTypeForField(field);
|
|
454
|
-
|
|
561
|
+
// A schema-declared `options` list turns the field into a
|
|
562
|
+
// dropdown (skipped when an asset_ref already claims the field
|
|
563
|
+
// — that picker is sourced from AMS, not the static list).
|
|
564
|
+
const dropdownOptions = !assetType && Array.isArray(field.options) && field.options.length > 0
|
|
565
|
+
? normalizeOptions(field.options)
|
|
566
|
+
: null;
|
|
567
|
+
const isNum = !assetType && !dropdownOptions
|
|
568
|
+
&& field.type !== 'string' && field.type !== 'bool';
|
|
455
569
|
return (
|
|
456
570
|
<React.Fragment key={field.name}>
|
|
457
571
|
<span className="ac-form-label">{labelOf(field)}</span>
|
|
@@ -462,6 +576,15 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
462
576
|
onChange={(val) => handleFieldChange(field, val)}
|
|
463
577
|
invalid={!valid}
|
|
464
578
|
/>
|
|
579
|
+
) : dropdownOptions ? (
|
|
580
|
+
<Dropdown
|
|
581
|
+
value={config[field.name] ?? null}
|
|
582
|
+
options={dropdownOptions}
|
|
583
|
+
onChange={(e) => handleFieldChange(field, e.value)}
|
|
584
|
+
placeholder={`Select ${field.label ?? field.name}…`}
|
|
585
|
+
className={!valid ? 'p-invalid' : ''}
|
|
586
|
+
showClear={!field.required}
|
|
587
|
+
/>
|
|
465
588
|
) : isNum ? (
|
|
466
589
|
<ValueInput
|
|
467
590
|
label={undefined}
|
|
@@ -664,7 +787,7 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
664
787
|
tabIndex={-1}
|
|
665
788
|
/>
|
|
666
789
|
<Button
|
|
667
|
-
icon="pi pi-
|
|
790
|
+
icon="pi pi-folder"
|
|
668
791
|
type="button"
|
|
669
792
|
onClick={() => setMethodPickerOpen(true)}
|
|
670
793
|
tooltip={methodIds.length > 1
|
|
@@ -680,6 +803,32 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
680
803
|
</>
|
|
681
804
|
)}
|
|
682
805
|
|
|
806
|
+
{configurations.length > 0 && (
|
|
807
|
+
<>
|
|
808
|
+
<span className="ac-form-label">Configuration</span>
|
|
809
|
+
<div className="p-inputgroup" style={{ flex: 1 }}>
|
|
810
|
+
<InputText
|
|
811
|
+
value={selectedConfig ? configLabelOf(selectedConfig) : ''}
|
|
812
|
+
placeholder="No configuration selected"
|
|
813
|
+
readOnly
|
|
814
|
+
style={{ flex: 1 }}
|
|
815
|
+
tabIndex={-1}
|
|
816
|
+
/>
|
|
817
|
+
<Button
|
|
818
|
+
icon="pi pi-folder"
|
|
819
|
+
type="button"
|
|
820
|
+
onClick={() => setConfigPickerOpen(true)}
|
|
821
|
+
tooltip="Change configuration"
|
|
822
|
+
tooltipOptions={{ position: 'top' }}
|
|
823
|
+
/>
|
|
824
|
+
</div>
|
|
825
|
+
<span aria-hidden="true" />
|
|
826
|
+
<span style={{ color: selectedConfig ? 'var(--green-500)' : 'var(--text-secondary-color)', display: 'flex', alignItems: 'center' }}>
|
|
827
|
+
<i className={selectedConfig ? 'pi pi-check' : 'pi pi-minus'} />
|
|
828
|
+
</span>
|
|
829
|
+
</>
|
|
830
|
+
)}
|
|
831
|
+
|
|
683
832
|
<h3 className="ac-form-section" style={{ marginTop: '1rem' }}>Test Configuration</h3>
|
|
684
833
|
{schema.config_fields.map(renderConfigField)}
|
|
685
834
|
|
|
@@ -690,6 +839,14 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
690
839
|
onSelected={(picked) => setMethodIdLocal(picked)}
|
|
691
840
|
/>
|
|
692
841
|
|
|
842
|
+
<ConfigurationDialog
|
|
843
|
+
visible={configPickerOpen}
|
|
844
|
+
onHide={() => setConfigPickerOpen(false)}
|
|
845
|
+
configurations={configurations}
|
|
846
|
+
currentConfigName={tis.configurationName}
|
|
847
|
+
onSelected={handleConfigurationSelected}
|
|
848
|
+
/>
|
|
849
|
+
|
|
693
850
|
{/* Shared info dialog. Driven by the Test Setup status
|
|
694
851
|
button (validation + server errors) and by per-field
|
|
695
852
|
info buttons (replaces the old hover Tooltip). One
|