@jbrowse/core 5.0.0-beta.10 → 5.0.0-beta.11

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.
Files changed (67) hide show
  1. package/README.md +113 -5
  2. package/esm/BaseFeatureWidget/BaseFeatureDetail/ArrayValue.js +0 -2
  3. package/esm/BaseFeatureWidget/BaseFeatureDetail/BasicValue.js +22 -6
  4. package/esm/BaseFeatureWidget/BaseFeatureDetail/jexlFilterActions.js +3 -4
  5. package/esm/BaseFeatureWidget/SequenceFeatureDetails/useSequenceData.js +8 -5
  6. package/esm/CorePlugin.js +32 -1
  7. package/esm/ReExports/coreModules.generated.js +248 -246
  8. package/esm/ReExports/coreWorkerModules.generated.js +246 -244
  9. package/esm/ReExports/list.js +3 -2
  10. package/esm/configuration/index.d.ts +2 -0
  11. package/esm/configuration/index.js +1 -0
  12. package/esm/configuration/plot.d.ts +61 -0
  13. package/esm/configuration/plot.js +142 -0
  14. package/esm/pluggableElementTypes/DisplayType.d.ts +7 -0
  15. package/esm/pluggableElementTypes/DisplayType.js +6 -0
  16. package/esm/pluggableElementTypes/WidgetType.d.ts +6 -0
  17. package/esm/pluggableElementTypes/WidgetType.js +6 -0
  18. package/esm/pluggableElementTypes/index.js +1 -8
  19. package/esm/pluggableElementTypes/models/BaseDisplayModel.d.ts +57 -1
  20. package/esm/pluggableElementTypes/models/BaseDisplayModel.js +84 -2
  21. package/esm/pluggableElementTypes/models/BaseViewModel.d.ts +2 -2
  22. package/esm/pluggableElementTypes/models/BaseViewModel.js +1 -1
  23. package/esm/ui/JexlFilterDialog.d.ts +1 -1
  24. package/esm/ui/JexlFilterDialog.js +2 -3
  25. package/esm/util/TimeTraveller.d.ts +3 -0
  26. package/esm/util/TimeTraveller.js +34 -2
  27. package/esm/util/baseDisplayConfig.d.ts +8 -0
  28. package/esm/util/baseDisplayConfig.js +27 -0
  29. package/esm/util/cachedSetup.d.ts +16 -6
  30. package/esm/util/cachedSetup.js +108 -38
  31. package/esm/util/cellMatches.d.ts +23 -0
  32. package/esm/util/cellMatches.js +264 -0
  33. package/esm/util/cellsStep.d.ts +29 -0
  34. package/esm/util/cellsStep.js +437 -0
  35. package/esm/util/copyToClipboard.js +8 -1
  36. package/esm/util/createAbortRotation.js +3 -3
  37. package/esm/util/featureTransforms.d.ts +7 -6
  38. package/esm/util/featureTransforms.js +131 -479
  39. package/esm/util/fetchPhases.d.ts +1 -1
  40. package/esm/util/formatBytes.js +1 -1
  41. package/esm/util/formatGuessers.d.ts +25 -14
  42. package/esm/util/formatGuessers.js +42 -27
  43. package/esm/util/formatRelativeTime.js +1 -1
  44. package/esm/util/getLocationUri.d.ts +2 -1
  45. package/esm/util/getLocationUri.js +8 -2
  46. package/esm/util/groupKeys.d.ts +4 -5
  47. package/esm/util/groupKeys.js +7 -6
  48. package/esm/util/intervals.js +9 -4
  49. package/esm/util/io/index.js +3 -1
  50. package/esm/util/jexlFilters.d.ts +31 -49
  51. package/esm/util/jexlFilters.js +29 -39
  52. package/esm/util/locString.js +5 -2
  53. package/esm/util/markEncoding.js +48 -8
  54. package/esm/util/markEncodingTypes.d.ts +8 -1
  55. package/esm/util/springAnimate.js +1 -1
  56. package/esm/util/stepTables.d.ts +14 -0
  57. package/esm/util/stepTables.js +38 -4
  58. package/esm/util/tabix.d.ts +6 -2
  59. package/esm/util/tabix.js +2 -2
  60. package/esm/util/tracks.d.ts +3 -30
  61. package/esm/util/tracks.js +4 -39
  62. package/esm/util/unknownSnapshotKeys.d.ts +3 -1
  63. package/esm/util/unknownSnapshotKeys.js +5 -11
  64. package/esm/util/usePanZoom.js +10 -3
  65. package/esm/util/useRafCommit.js +1 -0
  66. package/esm/util/withFeatureDetails.js +4 -0
  67. package/package.json +9 -8
@@ -1,42 +1,23 @@
1
1
  import { binnedAggregate, fusesBinAggregate } from './binnedAggregate.js';
2
2
  import { categoricalField } from './categoricalField.js';
3
+ import { binnedCellMatches, fusesAfterCells, fusesCellMatches, } from './cellMatches.js';
4
+ import { cells } from './cellsStep.js';
3
5
  import { DerivedTable, NO_COLUMN, WithTable, asTable, numberReaderOf, readerOf, selectRows, throughIndex, valueAt, withColumns, } from './featureTable.js';
4
6
  import { fieldReader, isPlainFieldRef } from './fieldReader.js';
5
- import { stringToJexlExpression } from './jexlStrings.js';
7
+ import { isJexl, stringToJexlExpression } from './jexlStrings.js';
6
8
  import SimpleFeature, { buildJexlContext } from './simpleFeature.js';
7
- import { BIN_OVERLAP_FIELD, DEFAULT_BIN_AS, MadeTable, OpSums, binSize, groupKey, intervalsOf, isWeighted, madeGroups, stepNumberReader, stepReader, } from './stepTables.js';
9
+ import { BIN_OVERLAP_FIELD, DEFAULT_BIN_AS, FannedTable, MadeTable, OpSums, binSize, boundsThrough, groupKey, intervalsOf, isWeighted, madeGroups, stepNumberReader, stepReader, } from './stepTables.js';
8
10
  import { junctionEnds, svClassOfAlt, svClassOfToken } from './svAlt.js';
9
11
  export { BIN_OVERLAP_FIELD, DEFAULT_BIN_AS } from './stepTables.js';
10
12
  export const DEFAULT_BIN_FIELD = 'start';
11
13
  export const DEFAULT_FLATTEN_FIELD = 'subfeatures';
12
- export const DEFAULT_CELLS_FIELD = 'seq';
14
+ export { DEFAULT_CELLS_FIELD } from './cellsStep.js';
13
15
  export const DEFAULT_COVERAGE_AS = 'coverage';
14
16
  export const DEFAULT_PILEUP_AS = 'row';
15
17
  export const DEFAULT_PILEUP_FIELDS = ['start', 'end'];
16
18
  function oneSection(n) {
17
19
  return Uint32Array.of(0, n);
18
20
  }
19
- // Section starts through a step whose rows came from its input's in order:
20
- // each section starts at the first row whose parent row is in it.
21
- function boundsThrough(bounds, parentRow) {
22
- const out = new Uint32Array(bounds.length);
23
- let lo = 0;
24
- for (let s = 0; s < bounds.length; s++) {
25
- const edge = bounds[s];
26
- let hi = parentRow.length;
27
- while (lo < hi) {
28
- const mid = (lo + hi) >>> 1;
29
- if (parentRow[mid] < edge) {
30
- lo = mid + 1;
31
- }
32
- else {
33
- hi = mid;
34
- }
35
- }
36
- out[s] = lo;
37
- }
38
- return out;
39
- }
40
21
  function expression(expr, jexl) {
41
22
  if (!jexl) {
42
23
  throw new Error(`a jexl transform needs a jexl instance (${expr})`);
@@ -94,19 +75,6 @@ function fannedEntries(items) {
94
75
  ? Object.entries(items)
95
76
  : [];
96
77
  }
97
- /**
98
- * The entries a `flatten` fanned out, one row each over the container row it
99
- * came from. A container kept with nothing to fan out is a row of its own
100
- * that reads everything off itself.
101
- */
102
- class FannedTable extends DerivedTable {
103
- get madeFrom() {
104
- return true;
105
- }
106
- row(i) {
107
- return this.isKept(i) ? this.parent.row(this.parentOf(i)) : super.row(i);
108
- }
109
- }
110
78
  /**
111
79
  * The entries a `flatten` fanned out of a list or record a row holds. An
112
80
  * entry answers a field it carries — a feature its own, a record its own key,
@@ -421,425 +389,6 @@ function flattenValues({ table, bounds }, step) {
421
389
  bounds: boundsThrough(bounds, rows),
422
390
  };
423
391
  }
424
- const DASH = 45;
425
- const SPACE = 32;
426
- const LOWER_BIT = 0x20;
427
- const CELL_STATES = ['match', 'mismatch', 'gap', 'insertion'];
428
- const MATCH = 0;
429
- const MISMATCH = 1;
430
- const GAP = 2;
431
- const INSERTION = 3;
432
- const NO_STATE = 255;
433
- function isGapByte(b) {
434
- return b === DASH || b === SPACE;
435
- }
436
- // The row a cell step reads against: past any table that only reordered or
437
- // annotated the rows, the one they were fanned out of.
438
- function containerOf(table) {
439
- let at = table;
440
- let toBase;
441
- while (at instanceof DerivedTable && !at.madeFrom) {
442
- toBase = at.parentRow ? compose(toBase, at.parentRow) : toBase;
443
- at = at.parent;
444
- }
445
- if (!(at instanceof DerivedTable)) {
446
- return undefined;
447
- }
448
- return {
449
- made: at,
450
- toMade: toBase,
451
- table: at.parent,
452
- row: at.parentRow ? compose(toBase, at.parentRow) : toBase,
453
- };
454
- }
455
- function compose(outer, inner) {
456
- if (!outer) {
457
- return inner;
458
- }
459
- const out = new Uint32Array(outer.length);
460
- for (let i = 0; i < outer.length; i++) {
461
- out[i] = inner[outer[i]];
462
- }
463
- return out;
464
- }
465
- // Below this length a character loop copies a string faster than
466
- // `encodeInto`, as the MAF packer measured.
467
- const ASCII_COPY_MAX_LENGTH = 64;
468
- const encoder = new TextEncoder();
469
- // A column's text as bytes, one per character: an adapter's own, or the
470
- // strings a feature holds copied into one buffer.
471
- function textOf(column, n) {
472
- if (column.kind === 'text') {
473
- return column;
474
- }
475
- const read = readerOf(column);
476
- const texts = new Array(n);
477
- let total = 0;
478
- for (let i = 0; i < n; i++) {
479
- const v = read(i);
480
- if (typeof v === 'string') {
481
- texts[i] = v;
482
- total += v.length;
483
- }
484
- }
485
- const bytes = new Uint8Array(total);
486
- const offset = new Uint32Array(n);
487
- const length = new Uint32Array(n);
488
- let pos = 0;
489
- for (let i = 0; i < n; i++) {
490
- const text = texts[i];
491
- if (text === undefined) {
492
- continue;
493
- }
494
- offset[i] = pos;
495
- if (text.length <= ASCII_COPY_MAX_LENGTH) {
496
- for (let c = 0; c < text.length; c++) {
497
- bytes[pos + c] = text.charCodeAt(c);
498
- }
499
- length[i] = text.length;
500
- }
501
- else {
502
- length[i] = encoder.encodeInto(text, bytes.subarray(pos, pos + text.length)).written;
503
- }
504
- pos += text.length;
505
- }
506
- return { kind: 'text', bytes, offset, length, at: undefined };
507
- }
508
- /**
509
- * The texts a `cells` walk reads, by the row of the table it walked: each
510
- * row's own, and the reference it is read against, which a row fanned out of
511
- * a container reads off the container.
512
- */
513
- class WalkedTexts {
514
- row;
515
- reference;
516
- refRow;
517
- constructor(row, ref, refRow) {
518
- this.row = row;
519
- this.reference = ref;
520
- this.refRow = refRow;
521
- }
522
- rowOf(r) {
523
- const { at } = this.row;
524
- return at ? at[r] : r;
525
- }
526
- refOf(r) {
527
- const p = this.refRow ? this.refRow[r] : r;
528
- const { at } = this.reference;
529
- return at ? at[p] : p;
530
- }
531
- }
532
- function walkedTexts(table, field) {
533
- const row = textOf(table.column(field), table.length);
534
- const container = containerOf(table);
535
- const made = container?.made;
536
- if (container && !(made instanceof FannedTable && made.hasKept)) {
537
- const { table: base } = container;
538
- return new WalkedTexts(row, textOf(base.column(field), base.length), container.row);
539
- }
540
- // A container a flatten kept with nothing to fan out is the row itself, so
541
- // its reference is its own container's, as its `parent()` says.
542
- const byParent = (r) => table.row(r).parent?.()?.get(field);
543
- const readContainer = container
544
- ? readerOf(container.table.column(field))
545
- : undefined;
546
- const read = readContainer && made instanceof FannedTable
547
- ? (r) => {
548
- const { toMade, row: refRow } = container;
549
- return made.isKept(toMade ? toMade[r] : r)
550
- ? byParent(r)
551
- : readContainer(refRow ? refRow[r] : r);
552
- }
553
- : byParent;
554
- return new WalkedTexts(row, textOf({ kind: 'value', read }, table.length));
555
- }
556
- /**
557
- * The runs a `cells` step answers, one per stretch of one state against the
558
- * reference, typed: each run's span, its state and the text column it starts
559
- * at, with `base`, `match` and `length` read back out of the texts when asked
560
- * for.
561
- */
562
- class CellTable extends DerivedTable {
563
- start;
564
- end;
565
- state;
566
- textAt;
567
- texts;
568
- firstOfRow;
569
- derived = new Map();
570
- constructor(parent, parentRow, lanes, texts) {
571
- super(parent, parentRow);
572
- this.start = lanes.start;
573
- this.end = lanes.end;
574
- this.state = lanes.state;
575
- this.textAt = lanes.textAt;
576
- this.texts = texts;
577
- }
578
- get madeFrom() {
579
- return true;
580
- }
581
- laneOf(name, build) {
582
- let column = this.derived.get(name);
583
- if (!column) {
584
- column = build();
585
- this.derived.set(name, column);
586
- }
587
- return column;
588
- }
589
- own(name) {
590
- switch (name) {
591
- case 'start': {
592
- return { kind: 'number', values: this.start, at: undefined };
593
- }
594
- case 'end': {
595
- return { kind: 'number', values: this.end, at: undefined };
596
- }
597
- case 'state': {
598
- return {
599
- kind: 'category',
600
- codes: this.state,
601
- labels: CELL_STATES,
602
- at: undefined,
603
- };
604
- }
605
- case 'match': {
606
- return this.laneOf(name, () => {
607
- const values = new Float32Array(this.length);
608
- for (let i = 0; i < this.length; i++) {
609
- const s = this.state[i];
610
- values[i] = s === MATCH ? 1 : s === MISMATCH ? 0 : Number.NaN;
611
- }
612
- return { kind: 'number', values, at: undefined, nanIsAbsent: true };
613
- });
614
- }
615
- case 'length': {
616
- return this.laneOf(name, () => {
617
- const values = new Float32Array(this.length).fill(Number.NaN);
618
- for (let i = 0; i < this.length; i++) {
619
- if (this.state[i] === INSERTION) {
620
- values[i] = this.inserted(i).length;
621
- }
622
- }
623
- return { kind: 'number', values, at: undefined, nanIsAbsent: true };
624
- });
625
- }
626
- case 'base': {
627
- return this.laneOf(name, () => {
628
- const values = new Array(this.length);
629
- for (let i = 0; i < this.length; i++) {
630
- const s = this.state[i];
631
- values[i] =
632
- s === MISMATCH
633
- ? this.mismatched(i)
634
- : s === INSERTION
635
- ? this.inserted(i)
636
- : undefined;
637
- }
638
- return { kind: 'value', read: i => values[i] };
639
- });
640
- }
641
- default: {
642
- return undefined;
643
- }
644
- }
645
- }
646
- mismatched(i) {
647
- const { bytes, offset } = this.texts.row;
648
- const k = this.texts.rowOf(this.parentOf(i));
649
- return String.fromCharCode(bytes[offset[k] + this.textAt[i]]);
650
- }
651
- // The bases a row holds where the reference has none, from the column the
652
- // insertion starts at to the reference's next base.
653
- inserted(i) {
654
- const { row, reference: ref } = this.texts;
655
- const r = this.parentOf(i);
656
- const k = this.texts.rowOf(r);
657
- const f = this.texts.refOf(r);
658
- const rowAt = row.offset[k];
659
- const refAt = ref.offset[f];
660
- const end = Math.min(row.length[k], ref.length[f]);
661
- let out = '';
662
- for (let col = this.textAt[i]; col < end && ref.bytes[refAt + col] === DASH; col++) {
663
- const b = row.bytes[rowAt + col];
664
- if (!isGapByte(b)) {
665
- out += String.fromCharCode(b);
666
- }
667
- }
668
- return out;
669
- }
670
- id(i) {
671
- const p = this.parentOf(i);
672
- this.firstOfRow ??= firstRowIndex(this.parentRow, this.parent.length);
673
- return `${this.parent.row(p).id()}#${i - this.firstOfRow[p]}`;
674
- }
675
- json(i) {
676
- const out = {
677
- ...this.parent.row(this.parentOf(i)).toJSON(),
678
- start: this.start[i],
679
- end: this.end[i],
680
- state: CELL_STATES[this.state[i]],
681
- };
682
- for (const name of ['base', 'match', 'length']) {
683
- const v = valueAt(this.column(name), i);
684
- if (v !== undefined) {
685
- out[name] = v;
686
- }
687
- }
688
- return out;
689
- }
690
- }
691
- // The lanes a `cells` walk writes, doubled as a row would overrun them.
692
- class RunLanes {
693
- start;
694
- end;
695
- state;
696
- textAt;
697
- parentRow;
698
- length = 0;
699
- constructor(capacity) {
700
- this.start = new Uint32Array(capacity);
701
- this.end = new Uint32Array(capacity);
702
- this.state = new Uint8Array(capacity);
703
- this.textAt = new Uint32Array(capacity);
704
- this.parentRow = new Uint32Array(capacity);
705
- }
706
- reserve(more) {
707
- const need = this.length + more;
708
- if (need <= this.start.length) {
709
- return;
710
- }
711
- const size = Math.max(this.start.length * 2, need);
712
- const widen = (a) => {
713
- const next = new a.constructor(size);
714
- next.set(a);
715
- return next;
716
- };
717
- this.start = widen(this.start);
718
- this.end = widen(this.end);
719
- this.state = widen(this.state);
720
- this.textAt = widen(this.textAt);
721
- this.parentRow = widen(this.parentRow);
722
- }
723
- // One row's runs against its reference: a run per column is the most it
724
- // can answer, so the lanes grow once and the walk writes through locals.
725
- walk(r, startPos, rowBytes, rowAt, rowLen, refBytes, refAt, refLen) {
726
- this.reserve(refLen + 1);
727
- const { start, end, state, textAt, parentRow } = this;
728
- let n = this.length;
729
- let pos = startPos;
730
- // The columns carrying the row's own sequence: a gap run reaching either
731
- // end of the row measures where the block was cut, not the alignment, so
732
- // it is no cell either.
733
- let first = 0;
734
- while (first < rowLen && isGapByte(rowBytes[rowAt + first])) {
735
- first++;
736
- }
737
- let last = rowLen - 1;
738
- while (last > first && isGapByte(rowBytes[rowAt + last])) {
739
- last--;
740
- }
741
- let runStart = -1;
742
- let runState = NO_STATE;
743
- let runBase = -1;
744
- let runCol = 0;
745
- let insertAt = -1;
746
- for (let col = 0; col < refLen; col++) {
747
- const refByte = refBytes[refAt + col];
748
- const rowByte = col < rowLen ? rowBytes[rowAt + col] : SPACE;
749
- if (refByte === DASH) {
750
- if (insertAt < 0 && col < rowLen && !isGapByte(rowByte)) {
751
- insertAt = col;
752
- }
753
- continue;
754
- }
755
- if (insertAt >= 0) {
756
- start[n] = pos;
757
- end[n] = pos;
758
- state[n] = INSERTION;
759
- textAt[n] = insertAt;
760
- parentRow[n] = r;
761
- n++;
762
- insertAt = -1;
763
- }
764
- const drawn = col >= first && col <= last && col < rowLen;
765
- const st = !drawn
766
- ? NO_STATE
767
- : isGapByte(rowByte)
768
- ? GAP
769
- : (refByte | LOWER_BIT) === (rowByte | LOWER_BIT)
770
- ? MATCH
771
- : MISMATCH;
772
- const base = st === MISMATCH ? rowByte : -1;
773
- if (st !== runState || base !== runBase) {
774
- if (runStart >= 0) {
775
- start[n] = runStart;
776
- end[n] = pos;
777
- state[n] = runState;
778
- textAt[n] = runCol;
779
- parentRow[n] = r;
780
- n++;
781
- }
782
- runStart = st === NO_STATE ? -1 : pos;
783
- runState = st;
784
- runBase = base;
785
- runCol = col;
786
- }
787
- pos++;
788
- }
789
- if (runStart >= 0) {
790
- start[n] = runStart;
791
- end[n] = pos;
792
- state[n] = runState;
793
- textAt[n] = runCol;
794
- parentRow[n] = r;
795
- n++;
796
- }
797
- this.length = n;
798
- }
799
- }
800
- // Where each parent row's children start, for children written parent by
801
- // parent in order.
802
- function firstRowIndex(parentRow, parents) {
803
- const first = new Uint32Array(parents);
804
- for (let i = parentRow.length - 1; i >= 0; i--) {
805
- first[parentRow[i]] = i;
806
- }
807
- return first;
808
- }
809
- function cells({ table, bounds }, step) {
810
- const { field = DEFAULT_CELLS_FIELD } = step;
811
- const texts = walkedTexts(table, field);
812
- const readStart = numberReaderOf(table.column('start'));
813
- const { bytes: rowBytes, offset: rowOffset, length: rowLength } = texts.row;
814
- const { bytes: refBytes, offset: refOffset, length: refLength, } = texts.reference;
815
- // A run per reference base is the most a row can answer and far past what
816
- // one does, so the lanes start at an eighth of it.
817
- let bound = 0;
818
- for (let r = 0; r < table.length; r++) {
819
- bound += refLength[texts.refOf(r)] + 1;
820
- }
821
- const lanes = new RunLanes(Math.max(1024, bound >>> 3));
822
- for (let r = 0; r < table.length; r++) {
823
- const f = texts.refOf(r);
824
- const refLen = refLength[f];
825
- const pos = readStart(r);
826
- if (refLen > 0 && pos >= 0) {
827
- const k = texts.rowOf(r);
828
- lanes.walk(r, pos, rowBytes, rowOffset[k], rowLength[k], refBytes, refOffset[f], refLen);
829
- }
830
- }
831
- const n = lanes.length;
832
- const rows = lanes.parentRow.subarray(0, n);
833
- return {
834
- table: new CellTable(table, rows, {
835
- start: lanes.start.subarray(0, n),
836
- end: lanes.end.subarray(0, n),
837
- state: lanes.state.subarray(0, n),
838
- textAt: lanes.textAt.subarray(0, n),
839
- }, texts),
840
- bounds: boundsThrough(bounds, rows),
841
- };
842
- }
843
392
  function statedMate(f) {
844
393
  const mate = f.get('mate');
845
394
  return mate?.refName !== undefined && mate.start !== undefined
@@ -867,23 +416,34 @@ function mateFields(f, alt, mate, ownDirection, alleleIndex = 0) {
867
416
  ...(svType ? { svType } : {}),
868
417
  };
869
418
  }
419
+ // A paired read's mate, where the file places one: the sequence and position
420
+ // its next segment aligns to, one base of it.
421
+ function pairedMate(f) {
422
+ const refName = f.get('next_ref');
423
+ const pos = f.get('next_pos');
424
+ return typeof refName === 'string' && typeof pos === 'number' && pos >= 0
425
+ ? { refName, start: pos, end: pos + 1, mateDirection: 0 }
426
+ : undefined;
427
+ }
870
428
  /**
871
429
  * #api
872
430
  * How a record states its other end, or undefined where it states none: the
873
- * `mate` a paired adapter fills (BEDPE, STAR-Fusion), or an `ALT` the breakend
874
- * and symbolic-SV readers resolve. The `mate` step admits exactly the features
875
- * this names one for, so a caller deciding whether links are the picture a
876
- * track wants asks here rather than re-reading the fields.
431
+ * `mate` a paired adapter fills (BEDPE, STAR-Fusion), an `ALT` the breakend
432
+ * and symbolic-SV readers resolve, or a read's `next_ref` and `next_pos`. The
433
+ * `mate` step admits exactly the features this names one for, so a caller
434
+ * deciding whether links are the picture a track wants asks here rather than
435
+ * re-reading the fields.
877
436
  */
878
437
  export function matedBy(f) {
879
438
  if (statedMate(f)) {
880
439
  return 'mate';
881
440
  }
882
441
  const alts = f.get('ALT');
883
- return Array.isArray(alts) &&
884
- alts.some((alt, i) => junctionEnds(f, alt, i))
885
- ? 'alt'
886
- : undefined;
442
+ if (Array.isArray(alts) &&
443
+ alts.some((alt, i) => junctionEnds(f, alt, i))) {
444
+ return 'alt';
445
+ }
446
+ return pairedMate(f) ? 'pair' : undefined;
887
447
  }
888
448
  /**
889
449
  * The ends a `mate` step admitted: each a row of the record it came from, with
@@ -923,9 +483,7 @@ function mates({ table, bounds }) {
923
483
  for (let s = 0; s + 1 < bounds.length; s++) {
924
484
  out[s] = parentRow.length;
925
485
  const seen = new Set();
926
- const admit = (i, f, mate, written, id) => {
927
- const here = `${f.get('refName')}:${f.get('start')}-${f.get('end')}`;
928
- const there = `${mate.refName}:${mate.start}-${mate.end}`;
486
+ const admit = (i, here, there, written, id) => {
929
487
  const key = here < there ? `${here}|${there}` : `${there}|${here}`;
930
488
  if (!seen.has(key)) {
931
489
  seen.add(key);
@@ -934,17 +492,26 @@ function mates({ table, bounds }) {
934
492
  ids.push(id);
935
493
  }
936
494
  };
495
+ const span = (f, mate) => [
496
+ `${f.get('refName')}:${f.get('start')}-${f.get('end')}`,
497
+ `${mate.refName}:${mate.start}-${mate.end}`,
498
+ ];
937
499
  for (let i = bounds[s]; i < bounds[s + 1]; i++) {
938
500
  const f = table.row(i);
939
501
  const stated = statedMate(f);
940
502
  if (stated) {
941
503
  const ends = junctionEnds(f);
942
504
  const mate = { ...stated, mateDirection: ends.mate.keeps };
943
- admit(i, f, mate, mateFields(f, undefined, mate, ends.own.keeps));
505
+ admit(i, ...span(f, mate), mateFields(f, undefined, mate, ends.own.keeps));
944
506
  continue;
945
507
  }
946
508
  const alts = f.get('ALT');
947
509
  if (!Array.isArray(alts)) {
510
+ // each read of a pair names the other's start, so the pair is one key
511
+ const pair = pairedMate(f);
512
+ if (pair) {
513
+ admit(i, `${f.get('refName')}:${f.get('start')}`, `${pair.refName}:${pair.start}`, { mate: pair, mateDirection: 0 });
514
+ }
948
515
  continue;
949
516
  }
950
517
  for (const [a, alt] of alts.entries()) {
@@ -958,7 +525,7 @@ function mates({ table, bounds }) {
958
525
  end: ends.mate.pos + 1,
959
526
  mateDirection: ends.mate.keeps,
960
527
  };
961
- admit(i, f, mate, mateFields(f, alt, mate, ends.own.keeps, a), alts.length > 1 ? `${f.id()}#${a}` : undefined);
528
+ admit(i, ...span(f, mate), mateFields(f, alt, mate, ends.own.keeps, a), alts.length > 1 ? `${f.id()}#${a}` : undefined);
962
529
  }
963
530
  }
964
531
  }
@@ -1350,12 +917,24 @@ function runSteps(staged, steps, jexl) {
1350
917
  for (let k = 0; k < list.length; k++) {
1351
918
  const step = list[k];
1352
919
  const next = list[k + 1];
1353
- const fused = step.type === 'bin' &&
920
+ const after = list[k + 2];
921
+ const matches = step.type === 'cells' &&
922
+ next?.type === 'bin' &&
923
+ after?.type === 'aggregate' &&
924
+ fusesCellMatches(step, next, after)
925
+ ? binnedCellMatches(current, step, next, after)
926
+ : undefined;
927
+ const fused = !matches &&
928
+ step.type === 'bin' &&
1354
929
  next?.type === 'aggregate' &&
1355
930
  fusesBinAggregate(step, next)
1356
931
  ? binnedAggregate(current, step, next)
1357
932
  : undefined;
1358
- if (fused) {
933
+ if (matches) {
934
+ current = matches;
935
+ k += 2;
936
+ }
937
+ else if (fused) {
1359
938
  current = fused;
1360
939
  k++;
1361
940
  }
@@ -1365,10 +944,83 @@ function runSteps(staged, steps, jexl) {
1365
944
  }
1366
945
  return current;
1367
946
  }
947
+ /**
948
+ * Each layer's steps over `steps` run once for all of them. A trailing
949
+ * `cells` runs inside each layer whose own steps start with the bin and
950
+ * aggregate it fuses with, so `runSteps` meets the three together, and once
951
+ * for the other layers.
952
+ */
953
+ function runLayers(staged, steps, layers, jexl, keep = s => s) {
954
+ const list = steps ?? [];
955
+ const last = list.at(-1);
956
+ const trailing = last?.type === 'cells' && layers.some(own => fusesAfterCells(last, own))
957
+ ? last
958
+ : undefined;
959
+ if (!trailing) {
960
+ const shared = keep(runSteps(staged, list, jexl), list);
961
+ return layers.map(own => keep(runSteps(shared, own, jexl), own));
962
+ }
963
+ const ahead = runSteps(staged, list.slice(0, -1), jexl);
964
+ let shared;
965
+ return layers.map(own => fusesAfterCells(trailing, own)
966
+ ? keep(runSteps(ahead, [trailing, ...own], jexl), [...list, ...own])
967
+ : keep(runSteps((shared ??= keep(runStep(ahead, trailing, jexl), list)), own, jexl), own));
968
+ }
1368
969
  function rowOf(value) {
1369
970
  const row = Number(value);
1370
971
  return row > 0 ? Math.floor(row) : 0;
1371
972
  }
973
+ /**
974
+ * A section's rows behind a step that made them from nothing, a coverage's
975
+ * runs or an aggregate's groups, still answer the field their section was
976
+ * split on, as a ggplot2 stat keeps its facet variable: one column of each
977
+ * section's key, built the first time a channel reads it, and a parent that
978
+ * carries the field itself is read instead.
979
+ */
980
+ class SectionTable extends DerivedTable {
981
+ field;
982
+ labels;
983
+ bounds;
984
+ constructor(parent, field, labels, bounds) {
985
+ super(parent, undefined);
986
+ this.field = field;
987
+ this.labels = labels;
988
+ this.bounds = bounds;
989
+ }
990
+ own(field) {
991
+ if (field !== this.field || this.parent.column(field).kind !== 'none') {
992
+ return undefined;
993
+ }
994
+ const { labels } = this;
995
+ const codes = new Uint32Array(this.length);
996
+ for (let s = 0; s < labels.length; s++) {
997
+ codes.fill(s, this.bounds[s], this.bounds[s + 1]);
998
+ }
999
+ // the section of the rows with nothing in the field answers nothing
1000
+ return labels.includes('')
1001
+ ? { kind: 'value', read: i => labels[codes[i]] || undefined }
1002
+ : { kind: 'category', codes, labels, at: undefined };
1003
+ }
1004
+ json(i) {
1005
+ const out = super.json(i);
1006
+ const label = valueAt(this.column(this.field), i);
1007
+ return label === undefined || this.field in out
1008
+ ? out
1009
+ : { ...out, [this.field]: label };
1010
+ }
1011
+ }
1012
+ function makesRows(steps) {
1013
+ return steps?.some(s => s.type === 'aggregate' || s.type === 'coverage');
1014
+ }
1015
+ // The staged rows still naming their section's value where a step made them
1016
+ function keepingSection(staged, steps, field, labels) {
1017
+ return !isJexl(field) && makesRows(steps)
1018
+ ? {
1019
+ ...staged,
1020
+ table: new SectionTable(staged.table, field, labels, staged.bounds),
1021
+ }
1022
+ : staged;
1023
+ }
1372
1024
  /**
1373
1025
  * #api
1374
1026
  * A faceted request's layers: the rows split on the facet's field, the
@@ -1444,9 +1096,10 @@ export function facetLayers(input, facet, layers, jexl) {
1444
1096
  for (let i = 0; i < n; i++) {
1445
1097
  order[next[sectionOf[keyOfRow[i]]]++] = i;
1446
1098
  }
1447
- const sectioned = runSteps({ table: selectRows(table, order), bounds }, sectionSteps, jexl);
1448
- const placed = layers.map(({ transform, row }) => {
1449
- const out = runSteps(sectioned, transform, jexl);
1099
+ const labels = ordered.map(k => keys[k]);
1100
+ const layered = runLayers({ table: selectRows(table, order), bounds }, sectionSteps, layers.map(({ transform }) => transform), jexl, (staged, steps) => keepingSection(staged, steps, field, labels));
1101
+ const placed = layers.map(({ row }, l) => {
1102
+ const out = layered[l];
1450
1103
  const readRow = row === undefined
1451
1104
  ? undefined
1452
1105
  : isPlainFieldRef(row)
@@ -1556,12 +1209,11 @@ function partitionPoint(steps, field) {
1556
1209
  export function layerTables(input, request, jexl) {
1557
1210
  const { transform = [], facet, layers } = request;
1558
1211
  if (!facet) {
1559
- const shared = runTransforms(input, transform, jexl);
1212
+ const table = asTable(input);
1213
+ const layered = runLayers({ table, bounds: oneSection(table.length) }, transform, layers.map(layer => layer.transform), jexl);
1560
1214
  return {
1561
- layers: layers.map(layer => ({
1562
- table: layer.transform?.length
1563
- ? runTransforms(shared, layer.transform, jexl)
1564
- : shared,
1215
+ layers: layers.map((layer, l) => ({
1216
+ table: layered[l].table,
1565
1217
  row: layer.row,
1566
1218
  })),
1567
1219
  sections: undefined,