@platforma-sdk/model 1.68.5 → 1.68.7
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/columns/column_collection_builder.cjs +8 -2
- package/dist/columns/column_collection_builder.cjs.map +1 -1
- package/dist/columns/column_collection_builder.d.ts +14 -3
- package/dist/columns/column_collection_builder.d.ts.map +1 -1
- package/dist/columns/column_collection_builder.js +8 -2
- package/dist/columns/column_collection_builder.js.map +1 -1
- package/dist/columns/ctx_column_sources.d.ts +1 -1
- package/dist/columns/index.d.ts +1 -1
- package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs +50 -50
- package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs.map +1 -1
- package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.d.ts +5 -10
- package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.d.ts.map +1 -1
- package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js +50 -50
- package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js.map +1 -1
- package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs +16 -17
- package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs.map +1 -1
- package/dist/components/PlDataTable/createPlDataTable/discoverColumns.d.ts +4 -4
- package/dist/components/PlDataTable/createPlDataTable/discoverColumns.d.ts.map +1 -1
- package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js +16 -17
- package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js.map +1 -1
- package/dist/components/PlDataTable/createPlDataTable/utils.cjs +8 -2
- package/dist/components/PlDataTable/createPlDataTable/utils.cjs.map +1 -1
- package/dist/components/PlDataTable/createPlDataTable/utils.js +8 -2
- package/dist/components/PlDataTable/createPlDataTable/utils.js.map +1 -1
- package/dist/components/PlDatasetSelector/filter_discovery.d.ts +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/labels/derive_distinct_labels.cjs +121 -50
- package/dist/labels/derive_distinct_labels.cjs.map +1 -1
- package/dist/labels/derive_distinct_labels.d.ts +30 -14
- package/dist/labels/derive_distinct_labels.d.ts.map +1 -1
- package/dist/labels/derive_distinct_labels.js +121 -50
- package/dist/labels/derive_distinct_labels.js.map +1 -1
- package/dist/labels/derive_distinct_tooltips.cjs +0 -10
- package/dist/labels/derive_distinct_tooltips.cjs.map +1 -1
- package/dist/labels/derive_distinct_tooltips.d.ts +2 -3
- package/dist/labels/derive_distinct_tooltips.d.ts.map +1 -1
- package/dist/labels/derive_distinct_tooltips.js +0 -10
- package/dist/labels/derive_distinct_tooltips.js.map +1 -1
- package/dist/labels/index.d.ts +1 -1
- package/dist/package.cjs +1 -1
- package/dist/package.js +1 -1
- package/package.json +4 -4
- package/src/columns/column_collection_builder.test.ts +0 -2
- package/src/columns/column_collection_builder.ts +26 -3
- package/src/components/PlDataTable/createPlDataTable/createPlDataTableV3.ts +90 -75
- package/src/components/PlDataTable/createPlDataTable/discoverColumns.ts +31 -34
- package/src/components/PlDataTable/createPlDataTable/utils.test.ts +1 -1
- package/src/components/PlDataTable/createPlDataTable/utils.ts +11 -4
- package/src/labels/derive_distinct_labels.test.ts +396 -52
- package/src/labels/derive_distinct_labels.ts +205 -103
- package/src/labels/derive_distinct_tooltips.test.ts +1 -22
- package/src/labels/derive_distinct_tooltips.ts +1 -18
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
Annotation,
|
|
3
|
+
type AxisQualification,
|
|
4
|
+
type PColumnSpec,
|
|
5
|
+
type PObjectId,
|
|
6
|
+
} from "@milaboratories/pl-model-common";
|
|
7
|
+
import { describe, expect, test } from "vitest";
|
|
3
8
|
import { deriveDistinctLabels, type Entry, type Trace } from "./derive_distinct_labels";
|
|
4
9
|
|
|
5
10
|
function tracesToSpecs(traces: Trace[]) {
|
|
@@ -284,8 +289,6 @@ test.each<{ name: string; traces: Trace[]; labels: string[]; forceTraceElements:
|
|
|
284
289
|
},
|
|
285
290
|
);
|
|
286
291
|
|
|
287
|
-
// --- Entry with { spec, extraTrace } ---
|
|
288
|
-
|
|
289
292
|
test("Entry with extraTrace (suffix, default) appends to labels", () => {
|
|
290
293
|
const spec = createSpec({
|
|
291
294
|
annotations: {
|
|
@@ -314,92 +317,84 @@ test("Entry with extraTrace position prefix prepends to labels", () => {
|
|
|
314
317
|
expect(labels).toEqual(["P1", "P2"]);
|
|
315
318
|
});
|
|
316
319
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
test("linkerPath appends default 'via' suffix", () => {
|
|
320
|
+
test("linkerPath appends default 'via' suffix when needed for uniqueness", () => {
|
|
320
321
|
const entries: Entry[] = [
|
|
321
322
|
{
|
|
322
323
|
spec: createSpec({
|
|
323
|
-
annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "
|
|
324
|
+
annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Col" }]) },
|
|
324
325
|
}),
|
|
325
|
-
linkerPath: [
|
|
326
|
-
{
|
|
327
|
-
spec: createSpec({
|
|
328
|
-
annotations: { [Annotation.LinkLabel]: "MyLinker" },
|
|
329
|
-
}),
|
|
330
|
-
},
|
|
331
|
-
],
|
|
332
326
|
},
|
|
333
327
|
{
|
|
334
328
|
spec: createSpec({
|
|
335
|
-
annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "
|
|
329
|
+
annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Col" }]) },
|
|
336
330
|
}),
|
|
331
|
+
linkerPath: [{ spec: createSpec({ annotations: { [Annotation.LinkLabel]: "MyLinker" } }) }],
|
|
337
332
|
},
|
|
338
333
|
];
|
|
339
334
|
const labels = deriveDistinctLabels(entries);
|
|
340
|
-
expect(labels).toEqual(["
|
|
335
|
+
expect(labels).toEqual(["Col", "Col via MyLinker"]);
|
|
341
336
|
});
|
|
342
337
|
|
|
343
338
|
test("linkerPath with multiple steps joins with ' > '", () => {
|
|
344
339
|
const entries: Entry[] = [
|
|
345
340
|
{
|
|
346
341
|
spec: createSpec({
|
|
347
|
-
annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "
|
|
342
|
+
annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Col" }]) },
|
|
348
343
|
}),
|
|
349
|
-
linkerPath: [
|
|
350
|
-
{ spec: createSpec({ annotations: { [Annotation.LinkLabel]: "L1" } }) },
|
|
351
|
-
{ spec: createSpec({ annotations: { [Annotation.LinkLabel]: "L2" } }) },
|
|
352
|
-
],
|
|
353
344
|
},
|
|
354
345
|
{
|
|
355
346
|
spec: createSpec({
|
|
356
|
-
annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "
|
|
347
|
+
annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Col" }]) },
|
|
357
348
|
}),
|
|
349
|
+
linkerPath: [
|
|
350
|
+
{ spec: createSpec({ annotations: { [Annotation.LinkLabel]: "L1" } }) },
|
|
351
|
+
{ spec: createSpec({ annotations: { [Annotation.LinkLabel]: "L2" } }) },
|
|
352
|
+
],
|
|
358
353
|
},
|
|
359
354
|
];
|
|
360
355
|
const labels = deriveDistinctLabels(entries);
|
|
361
|
-
expect(labels).toEqual(["
|
|
356
|
+
expect(labels).toEqual(["Col", "Col via L1 > L2"]);
|
|
362
357
|
});
|
|
363
358
|
|
|
364
359
|
test("linkerPath skips steps without labels", () => {
|
|
365
360
|
const entries: Entry[] = [
|
|
366
361
|
{
|
|
367
362
|
spec: createSpec({
|
|
368
|
-
annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "
|
|
363
|
+
annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Col" }]) },
|
|
369
364
|
}),
|
|
370
|
-
linkerPath: [
|
|
371
|
-
{ spec: createSpec() },
|
|
372
|
-
{ spec: createSpec({ annotations: { [Annotation.LinkLabel]: "L2" } }) },
|
|
373
|
-
],
|
|
374
365
|
},
|
|
375
366
|
{
|
|
376
367
|
spec: createSpec({
|
|
377
|
-
annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "
|
|
368
|
+
annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Col" }]) },
|
|
378
369
|
}),
|
|
370
|
+
linkerPath: [
|
|
371
|
+
{ spec: createSpec() },
|
|
372
|
+
{ spec: createSpec({ annotations: { [Annotation.LinkLabel]: "L2" } }) },
|
|
373
|
+
],
|
|
379
374
|
},
|
|
380
375
|
];
|
|
381
376
|
const labels = deriveDistinctLabels(entries);
|
|
382
|
-
expect(labels).toEqual(["
|
|
377
|
+
expect(labels).toEqual(["Col", "Col via L2"]);
|
|
383
378
|
});
|
|
384
379
|
|
|
385
380
|
test("linkerPath with custom linkerLabelFormatter", () => {
|
|
386
381
|
const entries: Entry[] = [
|
|
387
382
|
{
|
|
388
383
|
spec: createSpec({
|
|
389
|
-
annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "
|
|
384
|
+
annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Col" }]) },
|
|
390
385
|
}),
|
|
391
|
-
linkerPath: [{ spec: createSpec({ annotations: { [Annotation.LinkLabel]: "L1" } }) }],
|
|
392
386
|
},
|
|
393
387
|
{
|
|
394
388
|
spec: createSpec({
|
|
395
|
-
annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "
|
|
389
|
+
annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Col" }]) },
|
|
396
390
|
}),
|
|
391
|
+
linkerPath: [{ spec: createSpec({ annotations: { [Annotation.LinkLabel]: "L1" } }) }],
|
|
397
392
|
},
|
|
398
393
|
];
|
|
399
394
|
const labels = deriveDistinctLabels(entries, {
|
|
400
|
-
|
|
395
|
+
formatters: { linker: (linkerLabels) => `[${linkerLabels.join(", ")}]` },
|
|
401
396
|
});
|
|
402
|
-
expect(labels).toEqual(["
|
|
397
|
+
expect(labels).toEqual(["Col", "Col [L1]"]);
|
|
403
398
|
});
|
|
404
399
|
|
|
405
400
|
test("linkerPath with linkerLabelFormatter returning undefined suppresses suffix", () => {
|
|
@@ -417,7 +412,7 @@ test("linkerPath with linkerLabelFormatter returning undefined suppresses suffix
|
|
|
417
412
|
},
|
|
418
413
|
];
|
|
419
414
|
const labels = deriveDistinctLabels(entries, {
|
|
420
|
-
|
|
415
|
+
formatters: { linker: () => undefined },
|
|
421
416
|
});
|
|
422
417
|
expect(labels).toEqual(["Col1", "Col2"]);
|
|
423
418
|
});
|
|
@@ -426,21 +421,140 @@ test("linkerPath falls back to Label when LinkLabel is absent", () => {
|
|
|
426
421
|
const entries: Entry[] = [
|
|
427
422
|
{
|
|
428
423
|
spec: createSpec({
|
|
429
|
-
annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "
|
|
424
|
+
annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Col" }]) },
|
|
430
425
|
}),
|
|
431
|
-
linkerPath: [{ spec: createSpec({ annotations: { [Annotation.Label]: "FallbackLabel" } }) }],
|
|
432
426
|
},
|
|
433
427
|
{
|
|
434
428
|
spec: createSpec({
|
|
435
|
-
annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "
|
|
429
|
+
annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Col" }]) },
|
|
436
430
|
}),
|
|
431
|
+
linkerPath: [{ spec: createSpec({ annotations: { [Annotation.Label]: "FallbackLabel" } }) }],
|
|
437
432
|
},
|
|
438
433
|
];
|
|
439
434
|
const labels = deriveDistinctLabels(entries);
|
|
440
|
-
expect(labels).toEqual(["
|
|
435
|
+
expect(labels).toEqual(["Col", "Col via FallbackLabel"]);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test("formatters.native customizes label rendering", () => {
|
|
439
|
+
const s = ((label: string) =>
|
|
440
|
+
({
|
|
441
|
+
kind: "PColumn",
|
|
442
|
+
name: "n",
|
|
443
|
+
valueType: "Int",
|
|
444
|
+
axesSpec: [],
|
|
445
|
+
annotations: { [Annotation.Label]: label },
|
|
446
|
+
}) as PColumnSpec)("Counts");
|
|
447
|
+
const labels = deriveDistinctLabels([{ spec: s }, { spec: s }], {
|
|
448
|
+
formatters: { native: (l) => `<<${l}>>` },
|
|
449
|
+
});
|
|
450
|
+
expect(labels).toEqual(["<<Counts>>", "<<Counts>>"]);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("formatters.native returning undefined drops label entry", () => {
|
|
454
|
+
const traces: Trace[] = [[{ type: "t1", label: "X" }], [{ type: "t1", label: "Y" }]];
|
|
455
|
+
const labels = deriveDistinctLabels(tracesToSpecs(traces), {
|
|
456
|
+
includeNativeLabel: true,
|
|
457
|
+
formatters: { native: () => undefined },
|
|
458
|
+
});
|
|
459
|
+
expect(labels).toEqual(["X", "Y"]);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("formatters.hitQualification customizes hit zone", () => {
|
|
463
|
+
const s = {
|
|
464
|
+
kind: "PColumn",
|
|
465
|
+
name: "n",
|
|
466
|
+
valueType: "Int",
|
|
467
|
+
axesSpec: [],
|
|
468
|
+
annotations: { [Annotation.Label]: "Expr" },
|
|
469
|
+
} as PColumnSpec;
|
|
470
|
+
const entries: Entry[] = [
|
|
471
|
+
{
|
|
472
|
+
spec: s,
|
|
473
|
+
qualifications: {
|
|
474
|
+
forQueries: {},
|
|
475
|
+
forHit: [{ axis: { name: "gene" }, contextDomain: { gene: "BRCA1" } }],
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
spec: s,
|
|
480
|
+
qualifications: {
|
|
481
|
+
forQueries: {},
|
|
482
|
+
forHit: [{ axis: { name: "gene" }, contextDomain: { gene: "TP53" } }],
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
];
|
|
486
|
+
const labels = deriveDistinctLabels(entries, {
|
|
487
|
+
formatters: { hitQualification: (qs) => `<hit:${qs[0].contextDomain.gene}>` },
|
|
488
|
+
});
|
|
489
|
+
expect(labels).toEqual(["Expr <hit:BRCA1>", "Expr <hit:TP53>"]);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("formatters.anchorQualification receives anchorId", () => {
|
|
493
|
+
const s = {
|
|
494
|
+
kind: "PColumn",
|
|
495
|
+
name: "n",
|
|
496
|
+
valueType: "Int",
|
|
497
|
+
axesSpec: [],
|
|
498
|
+
annotations: { [Annotation.Label]: "Counts" },
|
|
499
|
+
} as PColumnSpec;
|
|
500
|
+
const A = "A" as PObjectId;
|
|
501
|
+
const entries: Entry[] = [
|
|
502
|
+
{
|
|
503
|
+
spec: s,
|
|
504
|
+
qualifications: {
|
|
505
|
+
forQueries: { [A]: [{ axis: { name: "sample" }, contextDomain: { batch: "X" } }] },
|
|
506
|
+
forHit: [],
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
spec: s,
|
|
511
|
+
qualifications: {
|
|
512
|
+
forQueries: { [A]: [{ axis: { name: "sample" }, contextDomain: { batch: "Y" } }] },
|
|
513
|
+
forHit: [],
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
];
|
|
517
|
+
const labels = deriveDistinctLabels(entries, {
|
|
518
|
+
formatters: {
|
|
519
|
+
anchorQualification: (id, qs) => `(${id}=${qs[0].contextDomain.batch})`,
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
expect(labels).toEqual(["Counts (A=X)", "Counts (A=Y)"]);
|
|
441
523
|
});
|
|
442
524
|
|
|
443
|
-
|
|
525
|
+
test("formatters.linkerStepQualification controls inline step quals", () => {
|
|
526
|
+
const s = {
|
|
527
|
+
kind: "PColumn",
|
|
528
|
+
name: "n",
|
|
529
|
+
valueType: "Int",
|
|
530
|
+
axesSpec: [],
|
|
531
|
+
annotations: { [Annotation.Label]: "Counts" },
|
|
532
|
+
} as PColumnSpec;
|
|
533
|
+
const entries: Entry[] = [
|
|
534
|
+
{
|
|
535
|
+
spec: s,
|
|
536
|
+
linkerPath: [
|
|
537
|
+
{
|
|
538
|
+
spec: createSpec({ annotations: { [Annotation.LinkLabel]: "Mapper" } }),
|
|
539
|
+
qualifications: [{ axis: { name: "sample" }, contextDomain: { batch: "X" } }],
|
|
540
|
+
},
|
|
541
|
+
],
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
spec: s,
|
|
545
|
+
linkerPath: [
|
|
546
|
+
{
|
|
547
|
+
spec: createSpec({ annotations: { [Annotation.LinkLabel]: "Mapper" } }),
|
|
548
|
+
qualifications: [{ axis: { name: "sample" }, contextDomain: { batch: "Y" } }],
|
|
549
|
+
},
|
|
550
|
+
],
|
|
551
|
+
},
|
|
552
|
+
];
|
|
553
|
+
const labels = deriveDistinctLabels(entries, {
|
|
554
|
+
formatters: { linkerStepQualification: (qs) => `(${qs[0].contextDomain.batch})` },
|
|
555
|
+
});
|
|
556
|
+
expect(labels).toEqual(["Counts via Mapper (X)", "Counts via Mapper (Y)"]);
|
|
557
|
+
});
|
|
444
558
|
|
|
445
559
|
test("addLabelAsSuffix places native label at the end", () => {
|
|
446
560
|
const specs = tracesToSpecs([[{ type: "t1", label: "L1" }], [{ type: "t1", label: "L2" }]]);
|
|
@@ -451,8 +565,6 @@ test("addLabelAsSuffix places native label at the end", () => {
|
|
|
451
565
|
expect(labels).toEqual(["L1 / Label", "L2 / Label"]);
|
|
452
566
|
});
|
|
453
567
|
|
|
454
|
-
// --- separator ---
|
|
455
|
-
|
|
456
568
|
test("custom separator is used between label parts", () => {
|
|
457
569
|
const specs = tracesToSpecs([
|
|
458
570
|
[
|
|
@@ -472,16 +584,12 @@ test("custom separator is used between label parts", () => {
|
|
|
472
584
|
expect(labels).toEqual(["A - X", "A - Y", "B - Y"]);
|
|
473
585
|
});
|
|
474
586
|
|
|
475
|
-
// --- single value ---
|
|
476
|
-
|
|
477
587
|
test("single value gets its trace label", () => {
|
|
478
588
|
const specs = tracesToSpecs([[{ type: "t1", label: "Only" }]]);
|
|
479
589
|
const labels = deriveDistinctLabels(specs);
|
|
480
590
|
expect(labels).toEqual(["Only"]);
|
|
481
591
|
});
|
|
482
592
|
|
|
483
|
-
// --- Unlabeled fallback ---
|
|
484
|
-
|
|
485
593
|
test("Unlabeled fallback when no trace entries match", () => {
|
|
486
594
|
// Two identical specs with identical traces — fallback path
|
|
487
595
|
const spec = createSpec({
|
|
@@ -502,8 +610,6 @@ test("Unlabeled when no traces and no label", () => {
|
|
|
502
610
|
expect(result.every((r) => r === "Unlabeled")).toBe(true);
|
|
503
611
|
});
|
|
504
612
|
|
|
505
|
-
// --- repeated type occurrences (secondaryTypes path) ---
|
|
506
|
-
|
|
507
613
|
test("repeated type occurrences are used as secondary types", () => {
|
|
508
614
|
// Two records where "t1" appears twice in each, with different labels on 2nd occurrence
|
|
509
615
|
const specs = tracesToSpecs([
|
|
@@ -522,8 +628,6 @@ test("repeated type occurrences are used as secondary types", () => {
|
|
|
522
628
|
expect(labels).toEqual(["A", "B"]);
|
|
523
629
|
});
|
|
524
630
|
|
|
525
|
-
// --- spec without Annotation.Label (only Trace) ---
|
|
526
|
-
|
|
527
631
|
test("spec without native label uses only trace entries", () => {
|
|
528
632
|
const specs = [
|
|
529
633
|
createSpec({
|
|
@@ -557,3 +661,243 @@ test("includeNativeLabel with no native label does not break", () => {
|
|
|
557
661
|
const labels = deriveDistinctLabels(specs, { includeNativeLabel: true });
|
|
558
662
|
expect(labels).toEqual(["X", "Y"]);
|
|
559
663
|
});
|
|
664
|
+
|
|
665
|
+
describe("deriveDistinctLabels v2 — linker path & qualifications", () => {
|
|
666
|
+
function labeledSpec(label: string, name = "col"): PColumnSpec {
|
|
667
|
+
return {
|
|
668
|
+
kind: "PColumn",
|
|
669
|
+
name,
|
|
670
|
+
valueType: "Int",
|
|
671
|
+
axesSpec: [],
|
|
672
|
+
annotations: { [Annotation.Label]: label },
|
|
673
|
+
} as PColumnSpec;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function linkerSpec(label: string, name = "linker"): PColumnSpec {
|
|
677
|
+
return {
|
|
678
|
+
kind: "PColumn",
|
|
679
|
+
name,
|
|
680
|
+
valueType: "Int",
|
|
681
|
+
axesSpec: [],
|
|
682
|
+
annotations: { [Annotation.LinkLabel]: label },
|
|
683
|
+
} as PColumnSpec;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function qual(axis: string, ctx: Record<string, string> = {}): AxisQualification {
|
|
687
|
+
return { axis: { name: axis }, contextDomain: ctx };
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const A = "anchor-main" as PObjectId;
|
|
691
|
+
const B = "anchor-other" as PObjectId;
|
|
692
|
+
|
|
693
|
+
test("linkerPath not appended when name alone is unique", () => {
|
|
694
|
+
const entries: Entry[] = [
|
|
695
|
+
{ spec: labeledSpec("Read counts") },
|
|
696
|
+
{ spec: labeledSpec("Coverage"), linkerPath: [{ spec: linkerSpec("Sample mapper") }] },
|
|
697
|
+
];
|
|
698
|
+
expect(deriveDistinctLabels(entries)).toEqual(["Read counts", "Coverage"]);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
test("linkerPath appended only when needed for uniqueness", () => {
|
|
702
|
+
const entries: Entry[] = [
|
|
703
|
+
{ spec: labeledSpec("Read counts") },
|
|
704
|
+
{ spec: labeledSpec("Read counts"), linkerPath: [{ spec: linkerSpec("Sample mapper") }] },
|
|
705
|
+
];
|
|
706
|
+
expect(deriveDistinctLabels(entries)).toEqual(["Read counts", "Read counts via Sample mapper"]);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
test("two linker paths → both get distinguishing via-suffix", () => {
|
|
710
|
+
const s = labeledSpec("Counts");
|
|
711
|
+
const entries: Entry[] = [
|
|
712
|
+
{ spec: s, linkerPath: [{ spec: linkerSpec("Path A") }] },
|
|
713
|
+
{ spec: s, linkerPath: [{ spec: linkerSpec("Path B") }] },
|
|
714
|
+
];
|
|
715
|
+
expect(deriveDistinctLabels(entries)).toEqual(["Counts via Path A", "Counts via Path B"]);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
test("multi-step paths joined with ' > '", () => {
|
|
719
|
+
const s = labeledSpec("Counts");
|
|
720
|
+
const entries: Entry[] = [
|
|
721
|
+
{ spec: s, linkerPath: [{ spec: linkerSpec("Hub") }, { spec: linkerSpec("Tail X") }] },
|
|
722
|
+
{ spec: s, linkerPath: [{ spec: linkerSpec("Hub") }, { spec: linkerSpec("Tail Y") }] },
|
|
723
|
+
];
|
|
724
|
+
expect(deriveDistinctLabels(entries)).toEqual([
|
|
725
|
+
"Counts via Hub > Tail X",
|
|
726
|
+
"Counts via Hub > Tail Y",
|
|
727
|
+
]);
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
test("hit qualifications used when nothing else differs", () => {
|
|
731
|
+
const s = labeledSpec("Expression");
|
|
732
|
+
const entries: Entry[] = [
|
|
733
|
+
{ spec: s, qualifications: { forQueries: {}, forHit: [qual("gene", { gene: "BRCA1" })] } },
|
|
734
|
+
{ spec: s, qualifications: { forQueries: {}, forHit: [qual("gene", { gene: "TP53" })] } },
|
|
735
|
+
];
|
|
736
|
+
expect(deriveDistinctLabels(entries)).toEqual([
|
|
737
|
+
"Expression [gene=BRCA1]",
|
|
738
|
+
"Expression [gene=TP53]",
|
|
739
|
+
]);
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
test("per-anchor qualifications named by anchor key", () => {
|
|
743
|
+
const s = labeledSpec("Counts");
|
|
744
|
+
const entries: Entry[] = [
|
|
745
|
+
{
|
|
746
|
+
spec: s,
|
|
747
|
+
qualifications: { forQueries: { [A]: [qual("sample", { batch: "X" })] }, forHit: [] },
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
spec: s,
|
|
751
|
+
qualifications: { forQueries: { [A]: [qual("sample", { batch: "Y" })] }, forHit: [] },
|
|
752
|
+
},
|
|
753
|
+
];
|
|
754
|
+
expect(deriveDistinctLabels(entries)).toEqual([
|
|
755
|
+
"Counts [anchor-main: sample batch=X]",
|
|
756
|
+
"Counts [anchor-main: sample batch=Y]",
|
|
757
|
+
]);
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
test("linker-step qualifications used to disambiguate identical linker labels", () => {
|
|
761
|
+
const s = labeledSpec("Counts");
|
|
762
|
+
const entries: Entry[] = [
|
|
763
|
+
{
|
|
764
|
+
spec: s,
|
|
765
|
+
linkerPath: [
|
|
766
|
+
{ spec: linkerSpec("Mapper"), qualifications: [qual("sample", { batch: "X" })] },
|
|
767
|
+
],
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
spec: s,
|
|
771
|
+
linkerPath: [
|
|
772
|
+
{ spec: linkerSpec("Mapper"), qualifications: [qual("sample", { batch: "Y" })] },
|
|
773
|
+
],
|
|
774
|
+
},
|
|
775
|
+
];
|
|
776
|
+
expect(deriveDistinctLabels(entries)).toEqual([
|
|
777
|
+
"Counts via Mapper [sample batch=X]",
|
|
778
|
+
"Counts via Mapper [sample batch=Y]",
|
|
779
|
+
]);
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
test("layers compose only as far as needed; no over-decoration", () => {
|
|
783
|
+
const entries: Entry[] = [
|
|
784
|
+
{ spec: labeledSpec("Read counts") },
|
|
785
|
+
{ spec: labeledSpec("Coverage") },
|
|
786
|
+
{
|
|
787
|
+
spec: labeledSpec("Coverage"),
|
|
788
|
+
qualifications: { forQueries: { [A]: [qual("sample", { batch: "X" })] }, forHit: [] },
|
|
789
|
+
},
|
|
790
|
+
];
|
|
791
|
+
expect(deriveDistinctLabels(entries)).toEqual([
|
|
792
|
+
"Read counts",
|
|
793
|
+
"Coverage",
|
|
794
|
+
"Coverage [anchor-main: sample batch=X]",
|
|
795
|
+
]);
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
test("hit and anchor qualifications combined when both needed", () => {
|
|
799
|
+
const s = labeledSpec("Counts");
|
|
800
|
+
const entries: Entry[] = [
|
|
801
|
+
{
|
|
802
|
+
spec: s,
|
|
803
|
+
qualifications: {
|
|
804
|
+
forQueries: { [A]: [qual("sample", { batch: "X" })] },
|
|
805
|
+
forHit: [qual("gene", { gene: "BRCA1" })],
|
|
806
|
+
},
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
spec: s,
|
|
810
|
+
qualifications: {
|
|
811
|
+
forQueries: { [A]: [qual("sample", { batch: "X" })] },
|
|
812
|
+
forHit: [qual("gene", { gene: "TP53" })],
|
|
813
|
+
},
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
spec: s,
|
|
817
|
+
qualifications: {
|
|
818
|
+
forQueries: { [A]: [qual("sample", { batch: "Y" })] },
|
|
819
|
+
forHit: [qual("gene", { gene: "BRCA1" })],
|
|
820
|
+
},
|
|
821
|
+
},
|
|
822
|
+
];
|
|
823
|
+
expect(deriveDistinctLabels(entries)).toEqual([
|
|
824
|
+
"Counts [anchor-main: sample batch=X] [gene=BRCA1]",
|
|
825
|
+
"Counts [anchor-main: sample batch=X] [gene=TP53]",
|
|
826
|
+
"Counts [anchor-main: sample batch=Y] [gene=BRCA1]",
|
|
827
|
+
]);
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
test("only distinctive anchor qualifications appear in the label", () => {
|
|
831
|
+
const s = labeledSpec("Counts");
|
|
832
|
+
const sharedB = [qual("project", { id: "P1" })];
|
|
833
|
+
const entries: Entry[] = [
|
|
834
|
+
{
|
|
835
|
+
spec: s,
|
|
836
|
+
qualifications: {
|
|
837
|
+
forQueries: { [A]: [qual("sample", { batch: "X" })], [B]: sharedB },
|
|
838
|
+
forHit: [],
|
|
839
|
+
},
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
spec: s,
|
|
843
|
+
qualifications: {
|
|
844
|
+
forQueries: { [A]: [qual("sample", { batch: "Y" })], [B]: sharedB },
|
|
845
|
+
forHit: [],
|
|
846
|
+
},
|
|
847
|
+
},
|
|
848
|
+
];
|
|
849
|
+
expect(deriveDistinctLabels(entries)).toEqual([
|
|
850
|
+
"Counts [anchor-main: sample batch=X]",
|
|
851
|
+
"Counts [anchor-main: sample batch=Y]",
|
|
852
|
+
]);
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
test("full decoration when every layer carries information", () => {
|
|
856
|
+
const sA: PColumnSpec = {
|
|
857
|
+
...labeledSpec("Counts"),
|
|
858
|
+
annotations: {
|
|
859
|
+
[Annotation.Label]: "Counts",
|
|
860
|
+
[Annotation.Trace]: JSON.stringify([{ type: "stage", label: "RNAseq" }]),
|
|
861
|
+
},
|
|
862
|
+
} as PColumnSpec;
|
|
863
|
+
const sB: PColumnSpec = {
|
|
864
|
+
...labeledSpec("Counts"),
|
|
865
|
+
annotations: {
|
|
866
|
+
[Annotation.Label]: "Counts",
|
|
867
|
+
[Annotation.Trace]: JSON.stringify([{ type: "stage", label: "ATACseq" }]),
|
|
868
|
+
},
|
|
869
|
+
} as PColumnSpec;
|
|
870
|
+
const entries: Entry[] = [
|
|
871
|
+
{ spec: sA, linkerPath: [{ spec: linkerSpec("Mapper") }] },
|
|
872
|
+
{
|
|
873
|
+
spec: sA,
|
|
874
|
+
linkerPath: [{ spec: linkerSpec("Mapper") }],
|
|
875
|
+
qualifications: { forQueries: { [A]: [qual("sample", { batch: "X" })] }, forHit: [] },
|
|
876
|
+
},
|
|
877
|
+
{ spec: sB, linkerPath: [{ spec: linkerSpec("Mapper") }] },
|
|
878
|
+
];
|
|
879
|
+
expect(deriveDistinctLabels(entries)).toEqual([
|
|
880
|
+
"Counts / RNAseq via Mapper",
|
|
881
|
+
"Counts / RNAseq via Mapper [anchor-main: sample batch=X]",
|
|
882
|
+
"Counts / ATACseq via Mapper",
|
|
883
|
+
]);
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
test("identical variants produce identical labels (cannot disambiguate)", () => {
|
|
887
|
+
const s = labeledSpec("Counts");
|
|
888
|
+
const entries: Entry[] = [{ spec: s }, { spec: s }];
|
|
889
|
+
expect(deriveDistinctLabels(entries)).toEqual(["Counts", "Counts"]);
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
test("axis-only qualification (no contextDomain) renders as axis name", () => {
|
|
893
|
+
const s = labeledSpec("Counts");
|
|
894
|
+
const entries: Entry[] = [
|
|
895
|
+
{ spec: s, qualifications: { forQueries: { [A]: [qual("sample")] }, forHit: [] } },
|
|
896
|
+
{ spec: s, qualifications: { forQueries: { [A]: [qual("gene")] }, forHit: [] } },
|
|
897
|
+
];
|
|
898
|
+
expect(deriveDistinctLabels(entries)).toEqual([
|
|
899
|
+
"Counts [anchor-main: sample]",
|
|
900
|
+
"Counts [anchor-main: gene]",
|
|
901
|
+
]);
|
|
902
|
+
});
|
|
903
|
+
});
|