@datagrok/proteomics 1.0.1 → 1.2.1
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/CHANGELOG.md +52 -3
- package/CLAUDE.md +178 -0
- package/README.md +195 -3
- package/detectors.js +152 -9
- package/dist/package-test.js +1 -1744
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +1 -146
- package/dist/package.js.map +1 -1
- package/docs/personas-and-capabilities.md +165 -0
- package/files/demo/README.md +264 -0
- package/files/demo/cptac-spike-in.txt +1571 -0
- package/files/demo/enrichment-demo.csv +120 -0
- package/files/demo/fragpipe-smoke-test.tsv +11 -0
- package/files/demo/proteinGroups.txt +28 -0
- package/files/demo/spectronaut-hye-candidates.tsv +94 -0
- package/files/demo/spectronaut-hye-demo.tsv +8761 -0
- package/files/demo/spectronaut-hye-mix.tsv +8761 -0
- package/files/demo/spectronaut-hye-precursor-golden.json +938 -0
- package/files/demo/spectronaut-hye-precursor-golden.tsv +235 -0
- package/files/demo/spectronaut-hye-precursor.tsv +493 -0
- package/images/enrichment-crosslink.png +0 -0
- package/images/enrichment-term-selected.png +0 -0
- package/images/hero.png +0 -0
- package/images/pipeline.svg +80 -0
- package/package.json +88 -63
- package/scripts/deqms_de.R +60 -0
- package/scripts/limma_de.R +42 -0
- package/scripts/vsn_normalize.R +19 -0
- package/src/analysis/differential-expression.ts +450 -0
- package/src/analysis/enrichment-export.ts +101 -0
- package/src/analysis/enrichment.ts +602 -0
- package/src/analysis/experiment-setup.ts +199 -0
- package/src/analysis/imputation.ts +407 -0
- package/src/analysis/log2-scale.ts +139 -0
- package/src/analysis/normalization.ts +255 -0
- package/src/analysis/pca.ts +254 -0
- package/src/analysis/spc-storage.ts +515 -0
- package/src/analysis/spc.ts +544 -0
- package/src/analysis/subcellular-location.ts +431 -0
- package/src/demo/enrichment-demo.ts +94 -0
- package/src/demo/proteomics-demo.ts +123 -0
- package/src/global.d.ts +15 -0
- package/src/menu.ts +133 -0
- package/src/package-api.ts +136 -14
- package/src/package-test.ts +45 -20
- package/src/package.g.ts +161 -0
- package/src/package.ts +1029 -17
- package/src/panels/protein-focus.ts +63 -0
- package/src/panels/published-analysis-panel.ts +151 -0
- package/src/panels/uniprot-panel.ts +349 -0
- package/src/parsers/fragpipe-parser.ts +200 -0
- package/src/parsers/generic-parser.ts +197 -0
- package/src/parsers/maxquant-parser.ts +162 -0
- package/src/parsers/shared-utils.ts +163 -0
- package/src/parsers/spectronaut-candidates-parser.ts +307 -0
- package/src/parsers/spectronaut-parser.ts +604 -0
- package/src/publishing/assert-published-shape.ts +260 -0
- package/src/publishing/post-open-recovery.ts +104 -0
- package/src/publishing/publish-project.ts +515 -0
- package/src/publishing/publish-settings.ts +59 -0
- package/src/publishing/publish-state.ts +316 -0
- package/src/publishing/share-dialog.ts +171 -0
- package/src/publishing/trim-dataframe.ts +247 -0
- package/src/tests/analysis.ts +658 -0
- package/src/tests/enrichment-export.ts +61 -0
- package/src/tests/enrichment-visualization.ts +340 -0
- package/src/tests/enrichment.ts +224 -0
- package/src/tests/fragpipe-e2e.ts +74 -0
- package/src/tests/fragpipe-parser.ts +147 -0
- package/src/tests/gene-label-resolver.ts +387 -0
- package/src/tests/generic-parser.ts +152 -0
- package/src/tests/group-mean-correlation.ts +139 -0
- package/src/tests/log2-scale.ts +93 -0
- package/src/tests/organisms.ts +56 -0
- package/src/tests/parsers.ts +182 -0
- package/src/tests/publish-roundtrip.ts +584 -0
- package/src/tests/publish-spike.ts +377 -0
- package/src/tests/qc-dashboard.ts +210 -0
- package/src/tests/smart-pathway-filter.ts +193 -0
- package/src/tests/spc-formula-lines-spike.ts +129 -0
- package/src/tests/spc.ts +640 -0
- package/src/tests/spectronaut-candidates-e2e.ts +140 -0
- package/src/tests/spectronaut-candidates-parser.ts +398 -0
- package/src/tests/spectronaut-parser.ts +668 -0
- package/src/tests/subcellular-location.ts +361 -0
- package/src/tests/uniprot-panel.ts +202 -0
- package/src/tests/volcano.ts +603 -0
- package/src/utils/column-detection.ts +28 -0
- package/src/utils/gene-label-resolver.ts +443 -0
- package/src/utils/organisms.ts +82 -0
- package/src/utils/proteomics-types.ts +30 -0
- package/src/viewers/enrichment-viewers.ts +274 -0
- package/src/viewers/group-mean-correlation.ts +218 -0
- package/src/viewers/heatmap.ts +168 -0
- package/src/viewers/pca-plot.ts +169 -0
- package/src/viewers/qc-computations.ts +266 -0
- package/src/viewers/qc-dashboard.ts +176 -0
- package/src/viewers/spc-dashboard.ts +755 -0
- package/src/viewers/volcano.ts +690 -0
- package/test-console-output-1.log +2073 -0
- package/test-record-1.mp4 +0 -0
- package/tools/derive-precursor-golden-sidecar.mjs +81 -0
- package/tools/generate-enrichment-fixture.sh +160 -0
- package/tools/generate-spectronaut-candidates-fixture.mjs +212 -0
- package/tools/generate-spectronaut-precursor-fixture.mjs +128 -0
- package/tools/spectronaut-aggregate.sh +46 -0
- package/tools/spectronaut-aggregate.sql +80 -0
- package/tsconfig.json +18 -71
- package/webpack.config.js +86 -45
- package/.eslintignore +0 -1
- package/.eslintrc.json +0 -56
- package/LICENSE +0 -674
- package/package.png +0 -0
- package/scripts/number_antibody.py +0 -190
- package/scripts/number_antibody_abnumber.py +0 -177
- package/scripts/number_antibody_anarci.py +0 -200
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import {category, test, expect} from '@datagrok-libraries/test/src/test';
|
|
2
|
+
import {
|
|
3
|
+
applySmartPathwayFilter, GENERIC_PARENT_TERMS, SPECIFIC_CHILD_TERMS,
|
|
4
|
+
GostResult,
|
|
5
|
+
} from '../analysis/enrichment';
|
|
6
|
+
|
|
7
|
+
/** Build a minimally-shaped GostResult — only the fields the smart filter
|
|
8
|
+
* reads are meaningful; the rest are filler so the type lines up. */
|
|
9
|
+
function gost(opts: Partial<GostResult> & {name: string; source: string; p_value: number}): GostResult {
|
|
10
|
+
return {
|
|
11
|
+
native: opts.native ?? `NS:${opts.name}`,
|
|
12
|
+
name: opts.name,
|
|
13
|
+
source: opts.source,
|
|
14
|
+
p_value: opts.p_value,
|
|
15
|
+
significant: opts.significant ?? true,
|
|
16
|
+
term_size: opts.term_size ?? 100,
|
|
17
|
+
query_size: opts.query_size ?? 50,
|
|
18
|
+
intersection_size: opts.intersection_size ?? 5,
|
|
19
|
+
effective_domain_size: opts.effective_domain_size ?? 20000,
|
|
20
|
+
precision: opts.precision ?? 0.1,
|
|
21
|
+
recall: opts.recall ?? 0.05,
|
|
22
|
+
intersections: opts.intersections ?? [],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
category('Proteomics: 14-05', () => {
|
|
27
|
+
test('smartPathwayFilter constants match CK-omics literal lists', async () => {
|
|
28
|
+
expect(GENERIC_PARENT_TERMS.length, 6);
|
|
29
|
+
expect(GENERIC_PARENT_TERMS.includes('localization'), true);
|
|
30
|
+
expect(GENERIC_PARENT_TERMS.includes('cellular component organization'), true);
|
|
31
|
+
expect(GENERIC_PARENT_TERMS.includes('transport'), true);
|
|
32
|
+
expect(GENERIC_PARENT_TERMS.includes('cellular process'), true);
|
|
33
|
+
expect(GENERIC_PARENT_TERMS.includes('biological process'), true);
|
|
34
|
+
expect(GENERIC_PARENT_TERMS.includes('metabolic process'), true);
|
|
35
|
+
|
|
36
|
+
expect(SPECIFIC_CHILD_TERMS.length, 4);
|
|
37
|
+
expect(SPECIFIC_CHILD_TERMS.includes('actin'), true);
|
|
38
|
+
expect(SPECIFIC_CHILD_TERMS.includes('vesicle'), true);
|
|
39
|
+
expect(SPECIFIC_CHILD_TERMS.includes('endocytosis'), true);
|
|
40
|
+
expect(SPECIFIC_CHILD_TERMS.includes('cytoskeleton'), true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('smartFilterDropsGenericParentWhenChildKept', async () => {
|
|
44
|
+
// NB: substring matching is literal per CK-omics. A name containing
|
|
45
|
+
// 'transport' is classified as generic even if it also contains 'vesicle'
|
|
46
|
+
// — pick fixture names that don't accidentally collide with a generic
|
|
47
|
+
// substring when the intent is "specific child kept".
|
|
48
|
+
const results: GostResult[] = [
|
|
49
|
+
gost({name: 'actin cytoskeleton organization', source: 'GO:BP', p_value: 0.001}),
|
|
50
|
+
gost({name: 'cellular component organization', source: 'GO:BP', p_value: 0.002}),
|
|
51
|
+
gost({name: 'vesicle docking', source: 'GO:BP', p_value: 0.003}),
|
|
52
|
+
gost({name: 'transport', source: 'GO:BP', p_value: 0.004}),
|
|
53
|
+
gost({name: 'protein phosphorylation', source: 'GO:BP', p_value: 0.005}),
|
|
54
|
+
];
|
|
55
|
+
const {kept, stats} = applySmartPathwayFilter(results, 15);
|
|
56
|
+
// Specific children + unrelated kept; the 2 generic-parents dropped.
|
|
57
|
+
expect(kept.length, 3);
|
|
58
|
+
expect(stats.total, 5);
|
|
59
|
+
expect(stats.kept, 3);
|
|
60
|
+
expect(stats.droppedParents, 2);
|
|
61
|
+
expect(stats.cappedAtN, 15);
|
|
62
|
+
const names = kept.map((r) => r.name);
|
|
63
|
+
expect(names.includes('actin cytoskeleton organization'), true);
|
|
64
|
+
expect(names.includes('vesicle docking'), true);
|
|
65
|
+
expect(names.includes('protein phosphorylation'), true);
|
|
66
|
+
expect(names.includes('cellular component organization'), false);
|
|
67
|
+
expect(names.includes('transport'), false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('smartFilterKeepsGenericParentWhenNoChildFirst', async () => {
|
|
71
|
+
// CK-omics behavior: generic parent at the lowest p is evaluated first,
|
|
72
|
+
// when the kept list is still empty → kept unconditionally. Later child
|
|
73
|
+
// does not retroactively drop it.
|
|
74
|
+
const results: GostResult[] = [
|
|
75
|
+
gost({name: 'biological process', source: 'GO:BP', p_value: 0.001}),
|
|
76
|
+
gost({name: 'actin filament organization', source: 'GO:BP', p_value: 0.002}),
|
|
77
|
+
gost({name: 'protein folding', source: 'GO:BP', p_value: 0.003}),
|
|
78
|
+
];
|
|
79
|
+
const {kept, stats} = applySmartPathwayFilter(results, 15);
|
|
80
|
+
expect(kept.length, 3);
|
|
81
|
+
expect(stats.droppedParents, 0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('smartFilterCapsGoBpAt15', async () => {
|
|
85
|
+
const results: GostResult[] = [];
|
|
86
|
+
for (let i = 0; i < 30; i++)
|
|
87
|
+
results.push(gost({name: `pathway ${i}`, source: 'GO:BP', p_value: 0.001 * (i + 1)}));
|
|
88
|
+
const {kept, stats} = applySmartPathwayFilter(results, 15);
|
|
89
|
+
expect(kept.length, 15);
|
|
90
|
+
expect(stats.total, 30);
|
|
91
|
+
expect(stats.kept, 15);
|
|
92
|
+
expect(stats.droppedParents, 0);
|
|
93
|
+
// The 15 lowest-p rows are kept.
|
|
94
|
+
for (const r of kept) {
|
|
95
|
+
const idx = parseInt(r.name.replace('pathway ', ''));
|
|
96
|
+
expect(idx < 15, true);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('smartFilterHonorsCustomCap (M3 — configurable max terms per source)', async () => {
|
|
101
|
+
// 30 GO:BP + 30 non-GO:BP; a custom cap of 5 must apply to each partition.
|
|
102
|
+
const results: GostResult[] = [];
|
|
103
|
+
for (let i = 0; i < 30; i++)
|
|
104
|
+
results.push(gost({name: `bp ${i}`, source: 'GO:BP', p_value: 0.001 * (i + 1)}));
|
|
105
|
+
for (let i = 0; i < 30; i++)
|
|
106
|
+
results.push(gost({name: `kegg ${i}`, source: 'KEGG', p_value: 0.01 * (i + 1)}));
|
|
107
|
+
const {kept, stats} = applySmartPathwayFilter(results, 5);
|
|
108
|
+
// 5 GO:BP + 5 non-GO:BP (combined) = 10.
|
|
109
|
+
expect(kept.length, 10, 'custom cap 5 applied to each partition');
|
|
110
|
+
expect(stats.cappedAtN, 5, 'stats echo the custom cap');
|
|
111
|
+
expect(kept.filter((r) => r.source === 'GO:BP').length, 5, 'GO:BP capped at 5');
|
|
112
|
+
expect(kept.filter((r) => r.source === 'KEGG').length, 5, 'non-GO:BP capped at 5');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('smartFilterCombinedCapForNonGoBp (Assumption A4)', async () => {
|
|
116
|
+
// 5 GO:BP (all kept because <15) + 20 non-GO:BP mixed across KEGG/REAC/WP.
|
|
117
|
+
// CK-omics line 4731: combined .head(maxPerSource), NOT per-source.
|
|
118
|
+
const results: GostResult[] = [];
|
|
119
|
+
for (let i = 0; i < 5; i++)
|
|
120
|
+
results.push(gost({name: `bp ${i}`, source: 'GO:BP', p_value: 0.001 * (i + 1)}));
|
|
121
|
+
const otherSources = ['KEGG', 'REAC', 'WP'];
|
|
122
|
+
for (let i = 0; i < 20; i++) {
|
|
123
|
+
results.push(gost({
|
|
124
|
+
name: `other ${i}`, source: otherSources[i % 3], p_value: 0.01 * (i + 1),
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
const {kept, stats} = applySmartPathwayFilter(results, 15);
|
|
128
|
+
// 5 GO:BP + 15 non-GO:BP combined cap = 20 total
|
|
129
|
+
expect(kept.length, 20);
|
|
130
|
+
expect(stats.total, 25);
|
|
131
|
+
expect(stats.kept, 20);
|
|
132
|
+
const nonBpKept = kept.filter((r) => r.source !== 'GO:BP');
|
|
133
|
+
expect(nonBpKept.length, 15);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('smartFilterPreservesPValueSortOrder', async () => {
|
|
137
|
+
const results: GostResult[] = [
|
|
138
|
+
gost({name: 'a', source: 'GO:BP', p_value: 0.005}),
|
|
139
|
+
gost({name: 'b', source: 'KEGG', p_value: 0.001}),
|
|
140
|
+
gost({name: 'c', source: 'GO:BP', p_value: 0.003}),
|
|
141
|
+
gost({name: 'd', source: 'REAC', p_value: 0.002}),
|
|
142
|
+
];
|
|
143
|
+
const {kept} = applySmartPathwayFilter(results, 15);
|
|
144
|
+
for (let i = 1; i < kept.length; i++)
|
|
145
|
+
expect(kept[i - 1].p_value <= kept[i].p_value, true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('smartFilterEmptyInput', async () => {
|
|
149
|
+
const {kept, stats} = applySmartPathwayFilter([], 15);
|
|
150
|
+
expect(kept.length, 0);
|
|
151
|
+
expect(stats.total, 0);
|
|
152
|
+
expect(stats.kept, 0);
|
|
153
|
+
expect(stats.droppedParents, 0);
|
|
154
|
+
expect(stats.cappedAtN, 15);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('smartFilterStatsCount', async () => {
|
|
158
|
+
const results: GostResult[] = [
|
|
159
|
+
gost({name: 'actin organization', source: 'GO:BP', p_value: 0.001}),
|
|
160
|
+
gost({name: 'biological process', source: 'GO:BP', p_value: 0.002}),
|
|
161
|
+
gost({name: 'metabolic process', source: 'GO:BP', p_value: 0.003}),
|
|
162
|
+
];
|
|
163
|
+
const {kept, stats} = applySmartPathwayFilter(results, 15);
|
|
164
|
+
expect(stats.total, results.length);
|
|
165
|
+
expect(stats.kept, kept.length);
|
|
166
|
+
expect(stats.droppedParents, 2);
|
|
167
|
+
expect(stats.cappedAtN, 15);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('smartFilter empty GO:BP, only other sources', async () => {
|
|
171
|
+
const results: GostResult[] = [];
|
|
172
|
+
for (let i = 0; i < 25; i++)
|
|
173
|
+
results.push(gost({name: `kegg ${i}`, source: 'KEGG', p_value: 0.001 * (i + 1)}));
|
|
174
|
+
const {kept, stats} = applySmartPathwayFilter(results, 15);
|
|
175
|
+
// Combined cap on non-GO:BP only.
|
|
176
|
+
expect(kept.length, 15);
|
|
177
|
+
expect(stats.droppedParents, 0);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('smartFilter non-GO:BP path also sorts by p ASC', async () => {
|
|
181
|
+
// Out-of-order non-GO:BP inputs; cap should keep the lowest-p subset.
|
|
182
|
+
const results: GostResult[] = [
|
|
183
|
+
gost({name: 'kegg_high', source: 'KEGG', p_value: 0.1}),
|
|
184
|
+
gost({name: 'kegg_low', source: 'KEGG', p_value: 0.001}),
|
|
185
|
+
gost({name: 'reac_mid', source: 'REAC', p_value: 0.05}),
|
|
186
|
+
];
|
|
187
|
+
const {kept} = applySmartPathwayFilter(results, 2);
|
|
188
|
+
// Cap=2 → keep the 2 lowest-p (kegg_low p=0.001, reac_mid p=0.05).
|
|
189
|
+
expect(kept.length, 2);
|
|
190
|
+
expect(kept[0].name, 'kegg_low');
|
|
191
|
+
expect(kept[1].name, 'reac_mid');
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import * as grok from 'datagrok-api/grok';
|
|
2
|
+
import * as DG from 'datagrok-api/dg';
|
|
3
|
+
import {category, test, expect} from '@datagrok-libraries/test/src/test';
|
|
4
|
+
|
|
5
|
+
const SPIKE_RESULT_APPDATA_PATH = 'System:AppData/Proteomics/spike/spike-result.md';
|
|
6
|
+
|
|
7
|
+
function buildSpikeDataFrame(): DG.DataFrame {
|
|
8
|
+
const baseMs = Date.UTC(2026, 5, 1);
|
|
9
|
+
const oneDayMs = 24 * 60 * 60 * 1000;
|
|
10
|
+
const dt: number[] = [];
|
|
11
|
+
for (let i = 0; i < 5; i++)
|
|
12
|
+
dt.push(baseMs + i * oneDayMs);
|
|
13
|
+
const dtCol = DG.Column.fromList(DG.COLUMN_TYPE.DATE_TIME, 'acquisition_datetime', dt);
|
|
14
|
+
const metricCol = DG.Column.fromFloat32Array('metric', new Float32Array([4.8, 5.1, 4.9, 5.2, 5.0]));
|
|
15
|
+
return DG.DataFrame.fromColumns([dtCol, metricCol]);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function writeOutcome(body: string): void {
|
|
19
|
+
try {
|
|
20
|
+
grok.dapi.files.writeAsText(SPIKE_RESULT_APPDATA_PATH, body);
|
|
21
|
+
} catch (_e) {
|
|
22
|
+
// swallow — outcome is also logged to console + shell.info
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
grok.shell.info(body.split('\n')[0]);
|
|
26
|
+
} catch (_e) { /* shell may not be addressable in headless test */ }
|
|
27
|
+
// eslint-disable-next-line no-console
|
|
28
|
+
console.log('[SPC spike] outcome\n' + body);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
category('SPC', () => {
|
|
32
|
+
test('SPC:spike_formula_line_dashed_on_datetime', async () => {
|
|
33
|
+
const df = buildSpikeDataFrame();
|
|
34
|
+
const viewer: any = DG.Viewer.lineChart(df, {
|
|
35
|
+
xColumnName: 'acquisition_datetime',
|
|
36
|
+
yColumnNames: ['metric'],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
let outcome: 'PASS' | 'FAIL' = 'FAIL';
|
|
40
|
+
let observation = '';
|
|
41
|
+
let platformError = '';
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const fl: any = (viewer as any).meta?.formulaLines;
|
|
45
|
+
if (!fl || typeof fl.addLine !== 'function') {
|
|
46
|
+
platformError = 'viewer.meta.formulaLines.addLine not addressable on lineChart';
|
|
47
|
+
} else {
|
|
48
|
+
fl.addLine({
|
|
49
|
+
formula: '${metric} = 5',
|
|
50
|
+
style: 'dashed',
|
|
51
|
+
color: '#666666',
|
|
52
|
+
width: 1,
|
|
53
|
+
visible: true,
|
|
54
|
+
title: 'UCL',
|
|
55
|
+
});
|
|
56
|
+
const items = fl.items;
|
|
57
|
+
const item0Style = items && items[0] ? items[0].style : undefined;
|
|
58
|
+
observation = 'items[0].style=' + String(item0Style);
|
|
59
|
+
if (item0Style === 'dashed')
|
|
60
|
+
outcome = 'PASS';
|
|
61
|
+
else
|
|
62
|
+
platformError = 'style honored !== "dashed" (observed: ' + String(item0Style) + ')';
|
|
63
|
+
}
|
|
64
|
+
} catch (e: any) {
|
|
65
|
+
platformError = (e && e.message) ? e.message : String(e);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const body =
|
|
69
|
+
`OUTCOME: ${outcome}\n` +
|
|
70
|
+
`Observation: ${observation}\n` +
|
|
71
|
+
(platformError ? `PlatformError: ${platformError}\n` : '') +
|
|
72
|
+
`Test: SPC:spike_formula_line_dashed_on_datetime\n` +
|
|
73
|
+
`Timestamp: ${new Date().toISOString()}\n`;
|
|
74
|
+
writeOutcome(body);
|
|
75
|
+
|
|
76
|
+
// Spike does not gate. Either outcome is acceptable.
|
|
77
|
+
expect(true, true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('SPC:spike_three_lines_ucl_cl_lcl', async () => {
|
|
81
|
+
const df = buildSpikeDataFrame();
|
|
82
|
+
const viewer: any = DG.Viewer.lineChart(df, {
|
|
83
|
+
xColumnName: 'acquisition_datetime',
|
|
84
|
+
yColumnNames: ['metric'],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const lines = [
|
|
88
|
+
{formula: '${metric} = 5.4', style: 'dashed', color: '#666666', width: 1, visible: true, title: 'UCL'},
|
|
89
|
+
{formula: '${metric} = 5.0', style: 'solid', color: '#666666', width: 1, visible: true, title: 'CL'},
|
|
90
|
+
{formula: '${metric} = 4.6', style: 'dashed', color: '#666666', width: 1, visible: true, title: 'LCL'},
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
let added = 0;
|
|
94
|
+
let lastError = '';
|
|
95
|
+
let dashedHonoredCount = 0;
|
|
96
|
+
try {
|
|
97
|
+
const fl: any = (viewer as any).meta?.formulaLines;
|
|
98
|
+
if (!fl || typeof fl.addLine !== 'function') {
|
|
99
|
+
lastError = 'viewer.meta.formulaLines.addLine not addressable on lineChart';
|
|
100
|
+
} else {
|
|
101
|
+
for (const ln of lines) {
|
|
102
|
+
try {
|
|
103
|
+
fl.addLine(ln);
|
|
104
|
+
added++;
|
|
105
|
+
} catch (e: any) {
|
|
106
|
+
lastError = (e && e.message) ? e.message : String(e);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const items = fl.items ?? [];
|
|
110
|
+
for (const it of items) {
|
|
111
|
+
if (it && it.style === 'dashed') dashedHonoredCount++;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch (e: any) {
|
|
115
|
+
lastError = (e && e.message) ? e.message : String(e);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const body =
|
|
119
|
+
`## Secondary observation: three-lines UCL/CL/LCL\n` +
|
|
120
|
+
`Added: ${added}/3\n` +
|
|
121
|
+
`DashedHonored: ${dashedHonoredCount}\n` +
|
|
122
|
+
(lastError ? `PlatformError: ${lastError}\n` : '') +
|
|
123
|
+
`Test: SPC:spike_three_lines_ucl_cl_lcl\n` +
|
|
124
|
+
`Timestamp: ${new Date().toISOString()}\n`;
|
|
125
|
+
writeOutcome(body);
|
|
126
|
+
|
|
127
|
+
expect(true, true);
|
|
128
|
+
});
|
|
129
|
+
});
|