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

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 (99) hide show
  1. package/README.md +97 -75
  2. package/esm/BaseFeatureWidget/BaseFeatureDetail/Attributes.d.ts +3 -0
  3. package/esm/BaseFeatureWidget/BaseFeatureDetail/Attributes.js +14 -1
  4. package/esm/BaseFeatureWidget/SequenceFeatureDetails/SequenceFeatureDetails.d.ts +2 -0
  5. package/esm/BaseFeatureWidget/SequenceFeatureDetails/SequenceFeatureDetails.js +5 -5
  6. package/esm/BaseFeatureWidget/SequenceFeatureDetails/SequenceFeaturePanel.js +1 -1
  7. package/esm/BaseFeatureWidget/SequenceFeatureDetails/dialogs/FeatureSequenceDialog.d.ts +3 -1
  8. package/esm/BaseFeatureWidget/SequenceFeatureDetails/dialogs/FeatureSequenceDialog.js +2 -2
  9. package/esm/BaseFeatureWidget/SequenceFeatureDetails/featureTypeUtil.d.ts +2 -1
  10. package/esm/BaseFeatureWidget/SequenceFeatureDetails/featureTypeUtil.js +7 -14
  11. package/esm/BaseFeatureWidget/SequenceFeatureDetails/seqtypes/CDNASequence.js +3 -3
  12. package/esm/BaseFeatureWidget/stateModelFactory.d.ts +7 -0
  13. package/esm/BaseFeatureWidget/stateModelFactory.js +11 -0
  14. package/esm/BaseFeatureWidget/types.d.ts +2 -0
  15. package/esm/PluginManager.d.ts +6 -3
  16. package/esm/PluginManager.js +6 -3
  17. package/esm/ReExports/coreModules.generated.js +291 -285
  18. package/esm/ReExports/coreWorkerModules.generated.js +288 -283
  19. package/esm/ReExports/list.js +7 -0
  20. package/esm/TextSearch/TextSearchManager.d.ts +3 -3
  21. package/esm/TextSearch/TextSearchManager.js +6 -13
  22. package/esm/configuration/configurationSchema.d.ts +33 -95
  23. package/esm/configuration/configurationSchema.js +50 -218
  24. package/esm/configuration/configurationSlot.d.ts +18 -64
  25. package/esm/configuration/configurationSlot.js +31 -97
  26. package/esm/configuration/formatDetailsConfigSchema.js +2 -0
  27. package/esm/configuration/getConf.d.ts +7 -36
  28. package/esm/configuration/getConf.js +7 -37
  29. package/esm/configuration/index.d.ts +3 -2
  30. package/esm/configuration/index.js +2 -1
  31. package/esm/configuration/partitionAdvanced.d.ts +12 -0
  32. package/esm/configuration/partitionAdvanced.js +42 -0
  33. package/esm/configuration/plot.d.ts +6 -0
  34. package/esm/configuration/plot.js +34 -1
  35. package/esm/configuration/readConfObject.d.ts +15 -17
  36. package/esm/configuration/readConfObject.js +19 -50
  37. package/esm/configuration/schemaTypes.js +5 -1
  38. package/esm/configuration/snapshotPreprocess.js +1 -3
  39. package/esm/configuration/types.d.ts +60 -159
  40. package/esm/pluggableElementTypes/DisplayType.d.ts +7 -0
  41. package/esm/pluggableElementTypes/DisplayType.js +7 -0
  42. package/esm/pluggableElementTypes/RpcMethodType.d.ts +7 -0
  43. package/esm/pluggableElementTypes/RpcMethodType.js +9 -0
  44. package/esm/pluggableElementTypes/addMenuItems.d.ts +2 -1
  45. package/esm/pluggableElementTypes/addMenuItems.js +2 -1
  46. package/esm/pluggableElementTypes/models/BaseDisplayModel.d.ts +3 -2
  47. package/esm/pluggableElementTypes/models/BaseDisplayModel.js +5 -5
  48. package/esm/pluggableElementTypes/models/BaseTrackModel.d.ts +2 -4
  49. package/esm/pluggableElementTypes/models/BaseTrackModel.js +2 -4
  50. package/esm/pluggableElementTypes/models/baseTrackConfig.js +3 -0
  51. package/esm/rpc/RpcRegistry.d.ts +2 -1
  52. package/esm/rpc/byteBudget.d.ts +7 -0
  53. package/esm/rpc/byteBudget.js +10 -0
  54. package/esm/rpc/coreRpcMethods.d.ts +1 -1
  55. package/esm/rpc/coreRpcMethods.js +1 -1
  56. package/esm/rpc/explainTransferError.js +3 -3
  57. package/esm/rpc/methods/CoreLoadTrackCode.d.ts +17 -0
  58. package/esm/rpc/methods/CoreLoadTrackCode.js +24 -0
  59. package/esm/ui/AppLogo.js +43 -3
  60. package/esm/ui/buildExtraTrackMenuItems.d.ts +2 -2
  61. package/esm/ui/colors.d.ts +41 -30
  62. package/esm/ui/colors.js +81 -44
  63. package/esm/ui/multiTrackMenuItems.d.ts +2 -2
  64. package/esm/util/alignedBytes.d.ts +19 -0
  65. package/esm/util/alignedBytes.js +37 -0
  66. package/esm/util/analytics.d.ts +5 -6
  67. package/esm/util/analytics.js +5 -3
  68. package/esm/util/bezierConnector.js +5 -1
  69. package/esm/util/categoricalField.d.ts +23 -3
  70. package/esm/util/categoricalField.js +36 -3
  71. package/esm/util/cellMatches.js +14 -14
  72. package/esm/util/cellsStep.d.ts +3 -5
  73. package/esm/util/cellsStep.js +31 -29
  74. package/esm/util/color-bits/parse.js +22 -11
  75. package/esm/util/colorRamp.js +6 -0
  76. package/esm/util/colorScale.d.ts +9 -2
  77. package/esm/util/colorScale.js +33 -2
  78. package/esm/util/colorSchemes.d.ts +4 -2
  79. package/esm/util/colorSchemes.js +4 -1
  80. package/esm/util/isoformRank.d.ts +39 -0
  81. package/esm/util/isoformRank.js +118 -0
  82. package/esm/util/markEncodingTypes.d.ts +3 -1
  83. package/esm/util/quantileExtent.d.ts +22 -0
  84. package/esm/util/quantileExtent.js +24 -1
  85. package/esm/util/systemColorScheme.d.ts +4 -0
  86. package/esm/util/systemColorScheme.js +21 -0
  87. package/esm/util/tabix.d.ts +1 -1
  88. package/esm/util/tabix.js +1 -1
  89. package/esm/util/temporaryAssembly.d.ts +2 -2
  90. package/esm/util/temporaryAssembly.js +2 -3
  91. package/esm/util/trackConfigDelta.d.ts +1 -1
  92. package/esm/util/tracks.d.ts +25 -15
  93. package/esm/util/tracks.js +67 -43
  94. package/esm/util/types/index.d.ts +15 -7
  95. package/esm/util/types/index.js +21 -2
  96. package/esm/util/useRowVirtualScroll.d.ts +1 -0
  97. package/package.json +7 -4
  98. package/esm/rpc/methods/CoreLoadAdapterCode.d.ts +0 -13
  99. package/esm/rpc/methods/CoreLoadAdapterCode.js +0 -16
package/README.md CHANGED
@@ -235,11 +235,13 @@ a refName.
235
235
  ### categoricalField
236
236
 
237
237
  `labels` names the `domain`'s values in a key, one each in order, where a config
238
- spells them for a reader rather than as the data does.
238
+ spells them for a reader rather than as the data does. `held` deals each key the
239
+ domain does not list a color of its own on first sight (`categoricalScale`); the
240
+ caller keeps it for as long as the colors should hold.
239
241
 
240
242
  ```js
241
243
  // type signature
242
- (field: string, {…}?: { domain?: readonly string[] | undefined; range?: readonly string[] | undefined; labels?: readonly string[] | undefined; }) => CategoricalField
244
+ (field: string, {…}?: {…}) => CategoricalField
243
245
  ```
244
246
 
245
247
  [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/core/src/util/categoricalField.ts)
@@ -250,7 +252,9 @@ One categorical field as every channel reads it — a facet's sections, a color'
250
252
  range entries, a key's rows. `key` files a value, `compare` orders keys (the
251
253
  `domain` first, the rest by `compareGroupKeys`, `''` after them), `label` names
252
254
  a key in a legend, `sectionLabel` on a chip, and `color` paints it. A key's
253
- color depends only on the key and the declaration, so every region agrees on it.
255
+ color depends only on the key and the declaration, so every region agrees on it,
256
+ except that a field dealing its colors (`categoricalField`'s `held`) also steps
257
+ a key off the colors of the keys met before it.
254
258
 
255
259
  [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/core/src/util/categoricalField.ts)
256
260
 
@@ -343,7 +347,9 @@ matplotlib's perceptual ramps, dark at the low end; `juicebox` fades from
343
347
  transparent to red, as Juicebox paints contacts; `fall` runs white through
344
348
  yellow and red to black, as HiGlass does; `reds` and `blues` are ColorBrewer's,
345
349
  from white; `redblue` and `purpleorange` diverge through white, ColorBrewer's
346
- RdBu and PuOr.
350
+ RdBu and PuOr; `redgreyblue` diverges through a grey middle, which stays visible
351
+ on a white page where a white middle would not, as the MAF display's identity
352
+ ramp paints it.
347
353
 
348
354
  ```js
349
355
  // type signature
@@ -357,7 +363,8 @@ readonly[
357
363
  'reds',
358
364
  'blues',
359
365
  'redblue',
360
- 'purpleorange')
366
+ 'purpleorange',
367
+ 'redgreyblue')
361
368
  ]
362
369
  ```
363
370
 
@@ -492,7 +499,9 @@ ramp is `range`'s CSS colours, evenly spaced, where it lists any, else the named
492
499
 
493
500
  Replace the features with runs of constant depth: how many of them overlap each
494
501
  stretch of the region, in a field `as` (`coverage` by default), with the
495
- stretches nothing overlaps left out.
502
+ stretches nothing overlaps left out. A read counts across its whole span,
503
+ deletions and introns included; gap-aware depth stays the alignments display's
504
+ (ADR-118).
496
505
 
497
506
  [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/core/src/util/markEncodingTypes.ts)
498
507
 
@@ -503,11 +512,24 @@ siblings do, juicebox, fall, reds and blues do not.
503
512
 
504
513
  ```js
505
514
  // type signature
506
- (scheme: "blues" | "cividis" | "fall" | "inferno" | "juicebox" | "magma" | "purpleorange" | "redblue" | "reds" | "viridis") => boolean
515
+ (scheme: "blues" | "cividis" | "fall" | "inferno" | "juicebox" | "magma" | "purpleorange" | "redblue" | "redgreyblue" | "reds" | "viridis") => boolean
507
516
  ```
508
517
 
509
518
  [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/core/src/util/colorRamp.ts)
510
519
 
520
+ ### dealKeyColors
521
+
522
+ Deals `keys` their colours in `field.compare` order, so the colours a set of
523
+ keys first seen together takes do not depend on the order they arrive in. A
524
+ no-op for a field that deals nothing.
525
+
526
+ ```js
527
+ // type signature
528
+ (field: CategoricalField, keys: Iterable<string>) => void
529
+ ```
530
+
531
+ [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/core/src/util/categoricalField.ts)
532
+
511
533
  ### DEFAULT_COLOR_SCHEME
512
534
 
513
535
  The ramp a continuous scale samples while it names no `range` and no `scheme`,
@@ -523,6 +545,7 @@ so a declaration spelling it out and one leaving it unset resolve alike.
523
545
  'magma' |
524
546
  'purpleorange' |
525
547
  'redblue' |
548
+ 'redgreyblue' |
526
549
  'reds' |
527
550
  'viridis'
528
551
  ```
@@ -596,6 +619,18 @@ categorical over a `category` column per label.
596
619
 
597
620
  [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/core/src/util/markEncoding.ts)
598
621
 
622
+ ### evaluateForFeature
623
+
624
+ A config value as `feature` reads it: a `jexl:` callback evaluated against the
625
+ feature, anything else as written.
626
+
627
+ ```js
628
+ // type signature
629
+ (value: unknown, feature: Feature, jexl: JexlInstance) => unknown
630
+ ```
631
+
632
+ [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/core/src/configuration/readConfObject.ts)
633
+
599
634
  ### FacetedLayer
600
635
 
601
636
  One layer of a faceted request: its rows in section order, and the stacked row
@@ -660,6 +695,23 @@ the hover.
660
695
 
661
696
  [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/core/src/util/featureTable.ts)
662
697
 
698
+ ### fenceOutliers
699
+
700
+ Where a value axis's open ends stand below a `domainQuantile` of 1: at the
701
+ `extremes`, unless one would stretch the axis past twice the span the
702
+ quantileExtent ends `quantiles` draw, 0 included where the axis reaches it
703
+ (`zero`); that end stops at the fence. So a plot with no spike shows every value
704
+ and cuts none, and a spike leaves the values under it at least half the axis.
705
+ Quantiles that meet span their own size, as a flat domain widens in
706
+ `getNiceDomain`.
707
+
708
+ ```js
709
+ // type signature
710
+ ({ extremes, quantiles, zero, }: { extremes: readonly [number, number]; quantiles: readonly [number, number]; zero: boolean; }) => [number, number]
711
+ ```
712
+
713
+ [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/core/src/util/quantileExtent.ts)
714
+
663
715
  ### fieldReader
664
716
 
665
717
  What a channel reads off a feature: a field by name, a dotted path into a
@@ -756,15 +808,8 @@ mate must drop both rather than treat `<DEL>` as a refName.
756
808
 
757
809
  ### getConf
758
810
 
759
- Reads a configuration value from a state model that has a `.configuration`
760
- member (a track or display state model). For a raw configuration model, use
761
- `readConfObject` instead.
762
-
763
- **This is exactly `readConfObject(model.configuration, path)`** — sugar for the
764
- `.configuration` hop, and nothing more. The two readers carry the same slot-name
765
- check, so reaching for the other one does not get a typo past tsc. It does not
766
- consult the session and has no per-slot behavior; what you read is what the
767
- track stores.
811
+ Reads a configuration value from a track or display state model: exactly
812
+ `readConfObject(model.configuration, path)`, with the same slot-name check.
768
813
 
769
814
  ```js
770
815
  // type signature
@@ -901,6 +946,18 @@ module actually uses.
901
946
 
902
947
  [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/core/src/util/sessionServices.ts)
903
948
 
949
+ ### heldSlotsOf
950
+
951
+ The `HeldSlots` `owner` keeps under `key`, made empty on first ask. Lives as
952
+ long as `owner` and is never persisted, so a reload deals afresh.
953
+
954
+ ```js
955
+ // type signature
956
+ (owner: object, key: string) => HeldSlots
957
+ ```
958
+
959
+ [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/core/src/util/categoricalField.ts)
960
+
904
961
  ### HitIndexed
905
962
 
906
963
  An encoded payload as a display stores it: the hit index the worker built with
@@ -922,27 +979,15 @@ at 0 for a mark with no value.
922
979
 
923
980
  ### hydrateTrackConfig
924
981
 
925
- Hydrate a plain track config into a live config node, dispatching on its `type`
926
- to find the schema. `session.tracks` holds `types.frozen` plain objects until
927
- something references a track (ADR-031). One of those holds only what was
928
- literally authored: a slot at its schema default is absent, `preProcessSnapshot`
929
- has not run, and nothing that walks a live node applies to it.
982
+ Hydrate a plain track config, such as a `session.tracks` entry, into a live
983
+ config node, dispatching on its `type` to find the schema. A plain entry holds
984
+ only what was authored: a slot at its default is absent and `preProcessSnapshot`
985
+ has not run.
930
986
 
931
- Use it where a caller needs the resolved config and may be handed either form.
932
- The About dialog's "Copy config" is reached from two menus, and one of them
933
- passes a `session.tracks` entry.
934
-
935
- Returns **undefined** when the config names a track type no plugin registered,
936
- or when `create` rejects it as invalid. An un-hydrated config has never been
937
- validated, so the dialog opening over it should not throw. Callers fall back to
938
- using the plain object.
939
-
940
- Shares `TrackConfigurationReference`'s per-PluginManager cache, so hydrating the
941
- same entry twice returns the same node, and in an admin session a track opened
942
- later resolves to that same node. A non-admin's open track resolves to the
943
- session's private working copy (ADR-032), and this function returns the pristine
944
- mirror beside it. The two have the same content; `CopyConfigEntryPoints.test.ts`
945
- tests both cases.
987
+ Returns **undefined** when no plugin registered the type or `create` rejects the
988
+ config, which has never been validated; callers fall back to the plain object.
989
+ Hydrating one entry twice returns one node. A shown track resolves to the
990
+ session's working copy instead (ADR-032), which has the same content.
946
991
 
947
992
  ```js
948
993
  // type signature
@@ -1384,6 +1429,11 @@ it itself. Under 0.5 reads as 0.5, where the ends meet rather than cross.
1384
1429
  `scales.y.domainQuantile` and a colour's `domainQuantile` both name it.
1385
1430
  `[Infinity, -Infinity]` where nothing is finite.
1386
1431
 
1432
+ Nearest rank keeps ties: where one value holds the rank, as on a segmented
1433
+ copy-number track, a ramp clipped below 1 paints nearly every value one colour.
1434
+ A value axis fences instead (fenceOutliers); a ramp takes the same fence once a
1435
+ colour default below 1 meets tied data.
1436
+
1387
1437
  ```js
1388
1438
  // type signature
1389
1439
  (values: ArrayLike<number>, count: number, quantile?: number) => [number, number]
@@ -1466,20 +1516,13 @@ widened domain with no table baked again.
1466
1516
 
1467
1517
  ### readConfObject
1468
1518
 
1469
- Given a configuration model (an instance of a ConfigurationSchema), read the
1470
- configuration value at the given path. Use this when you hold the configuration
1471
- model directly, e.g. an entry from `session.tracks`.
1472
-
1473
- Wants a **live config node**, not a snapshot of one, and passing a snapshot is a
1474
- type error. Slots are built with `types.stripDefault`, so a slot sitting at its
1475
- default is absent from a snapshot — "unset" and "at its default" are
1476
- indistinguishable there, and a read off one reports a default as missing.
1519
+ Read the value at a path of a live config node, such as a track's
1520
+ `configuration`, evaluating a `jexl:` callback with `args`.
1477
1521
 
1478
- That is enforced in the types only, deliberately: it can't be a runtime check.
1479
- `generateHierarchy` reads slots straight off the **un-hydrated frozen** entries
1480
- of `jbrowse.tracks` on purpose, because hydrating every track to answer the
1481
- track selector is what `types.frozen` exists to avoid — and those reads are
1482
- indistinguishable at runtime from the broken spelling.
1522
+ A snapshot is refused by the types, since a slot at its default is absent from
1523
+ one. A `session.tracks` entry is one (`TrackConfigEntry`): read its `trackId`
1524
+ and `type` directly, and a slot through a helper that supplies the default, such
1525
+ as `getConfAssemblyNamesOrNone`.
1483
1526
 
1484
1527
  ```js
1485
1528
  // type signature
@@ -1761,32 +1804,11 @@ array naming a sub-schema's member at any depth —
1761
1804
  `setConf(self, ['scales', 'y', 'domainMin'], 5)`. Takes the display or track
1762
1805
  model, or a config node directly.
1763
1806
 
1764
- **The path length says what the write replaces.** A path ending on a slot writes
1765
- that slot and leaves the object around it alone; a path ending on a sub-schema
1766
- replaces the whole object, which is how a channel whose members move together —
1767
- a facet's field and the domain that field's values order — gets written without
1768
- a stale member surviving underneath.
1769
-
1770
- **Prefer this over a bare `self.configuration.setSlot('x', v)`.** The constraint
1771
- here mirrors `getConf`'s, so on a model with a concrete schema an unknown slot
1772
- name is a compile error. `setSlot` itself stays untyped on purpose — the config
1773
- editor's slot facade routes dynamic slot names through it
1774
- (`configurationSchema.ts`) — and guards the name at runtime instead (ADR-052),
1775
- so a misspelled write is diagnosed one way or the other.
1776
-
1777
- **The read is the half with no diagnostic at all.** `getConf` for a name the
1778
- schema doesn't declare returns `undefined` and reports nothing, at any layer, so
1779
- the slot keeps reading as its default forever. Which makes the compile-time
1780
- constraint worth keeping _reachable_: it is only as good as the schema of the
1781
- holder handed in, and a holder widened to `AnyConfigurationModel` switches it
1782
- off entirely — the trap a mixin casting to reach its host walks into. Every such
1783
- cast names a concrete schema instead (`ConfigModelForFields`, or the base schema
1784
- when the slot is the base's), and `HostChecksSlotNames` pins each one.
1785
-
1786
- A wrong _value_ type still throws at runtime (MST type-checks the assignment)
1787
- rather than at compile time. `value` is deliberately `unknown` because
1788
- `undefined` is a legitimate write — it resets a slot to its schema default — and
1789
- the declared slot value type doesn't include it.
1807
+ A path ending on a slot writes that slot; one ending on a sub-schema replaces
1808
+ the whole object, so a channel's members (a facet's field and its domain) move
1809
+ together. On a concrete schema an unknown slot name is a compile error, and
1810
+ `setSlot` refuses one at runtime (ADR-052). A wrong value type throws at
1811
+ runtime; `null` or `undefined` resets the slot to its default (ADR-146).
1790
1812
 
1791
1813
  ```js
1792
1814
  // type signature
@@ -1,4 +1,7 @@
1
1
  import type { Descriptors, FeatureFormatter, FieldActions } from '../types.tsx';
2
+ export declare function withoutLocations(attributes: Record<string, unknown>): Record<string, unknown>;
3
+ /** Whether `Attributes` given these `attributes` and `omit` draws any row. */
4
+ export declare function hasAttributeRows(attributes: Record<string, unknown>, omit?: string[]): boolean;
2
5
  export default function Attributes(props: {
3
6
  attributes: Record<string, unknown>;
4
7
  omit?: string[];
@@ -51,7 +51,7 @@ function isLocation(value) {
51
51
  }
52
52
  // hideUris prunes the data rather than each render branch checking: an array
53
53
  // of objects reaches the data grid and ArrayValue, which print locations too
54
- function withoutLocations(attributes) {
54
+ export function withoutLocations(attributes) {
55
55
  return Object.fromEntries(Object.entries(attributes)
56
56
  .filter(([, value]) => !isLocation(value))
57
57
  .map(([key, value]) => [key, valueWithoutLocations(value)]));
@@ -103,6 +103,19 @@ function measureLabels(attributes, opts) {
103
103
  }
104
104
  return widest;
105
105
  }
106
+ /** Whether `Attributes` given these `attributes` and `omit` draws any row. */
107
+ export function hasAttributeRows(attributes, omit = []) {
108
+ const omits = new Set([...omit, ...globalOmit]);
109
+ return Object.entries(attributes).some(([key, value]) => value == null || omits.has(key)
110
+ ? false
111
+ : Array.isArray(value)
112
+ ? isHomogeneousObjectArray(value) ||
113
+ !isObjectArray(value) ||
114
+ value.some(entry => hasAttributeRows(entry))
115
+ : isObject(value)
116
+ ? isUriLocation(value) || hasAttributeRows(value, omit)
117
+ : true);
118
+ }
106
119
  function widestLabel(attributes, opts) {
107
120
  return Math.ceil(measureLabels(attributes, opts)) + FIELD_NAME_PADDING;
108
121
  }
@@ -1,4 +1,5 @@
1
1
  import type { AbstractSessionModel, SimpleFeatureSerialized } from '../../util/index.ts';
2
+ import type { CanonicalTranscripts } from '../../util/isoformRank.ts';
2
3
  import type { SequenceFeatureDetailsModel, SequenceHoverTarget } from './model.ts';
3
4
  interface SequenceFeatureDetailsProps {
4
5
  model: SequenceFeatureDetailsModel;
@@ -7,6 +8,7 @@ interface SequenceFeatureDetailsProps {
7
8
  feature: SimpleFeatureSerialized;
8
9
  hoverTarget?: SequenceHoverTarget;
9
10
  showOpenInDialog?: boolean;
11
+ canonicalTranscripts?: CanonicalTranscripts;
10
12
  }
11
13
  declare function SequenceFeatureDetails(props: SequenceFeatureDetailsProps): import("react").JSX.Element;
12
14
  export default SequenceFeatureDetails;
@@ -10,15 +10,15 @@ import { useSequenceFetch } from './useSequenceFetch.js';
10
10
  // lazies
11
11
  const SequenceBody = lazy(() => import('./SequenceBody.js'));
12
12
  const SequenceDialog = lazy(() => import('./dialogs/SequenceDialog.js'));
13
- const SequenceFeatureDetailsPanel = observer(function SequenceFeatureDetailsPanel({ model, session, assemblyName, feature, hoverTarget, showOpenInDialog = true, }) {
13
+ const SequenceFeatureDetailsPanel = observer(function SequenceFeatureDetailsPanel({ model, session, assemblyName, feature, hoverTarget, showOpenInDialog = true, canonicalTranscripts, }) {
14
14
  const { upDownBp } = model;
15
15
  const seqPanelRef = useRef(null);
16
16
  // A container feature (e.g. a gene) has no CDS/exon of its own — one of its
17
- // transcript children does. Default to the longest-coding transcript so CDS
18
- // and Protein sequence types work without an extra click into Subfeatures,
19
- // with a selector to switch transcripts when the gene has more than one.
17
+ // transcript children does. Default to the transcript the track draws first
18
+ // so CDS and Protein sequence types work without an extra click into
19
+ // Subfeatures, with a selector to switch when the gene has more than one.
20
20
  const transcripts = getTranscripts(feature);
21
- const [transcriptIndex, setTranscriptIndex] = useState(() => pickDefaultTranscriptIndex(transcripts));
21
+ const [transcriptIndex, setTranscriptIndex] = useState(() => pickDefaultTranscriptIndex(transcripts, canonicalTranscripts));
22
22
  const effectiveFeature = transcripts[transcriptIndex] ?? feature;
23
23
  // mode and revcomp are per-panel state, not on the shared model, so each
24
24
  // subfeature panel (e.g. coding vs noncoding transcripts of one gene) picks
@@ -38,6 +38,6 @@ const SequenceFeaturePanel = observer(function SequenceFeaturePanel({ model, fea
38
38
  feature,
39
39
  viewAssemblyNames: model.view?.assemblyNames,
40
40
  assemblyManager: getSession(model).assemblyManager,
41
- }), feature: feature, hoverTarget: model }) })) : null] }));
41
+ }), feature: feature, hoverTarget: model, canonicalTranscripts: model.canonicalTranscripts }) })) : null] }));
42
42
  });
43
43
  export default SequenceFeaturePanel;
@@ -1,11 +1,13 @@
1
1
  import type { AbstractSessionModel, SimpleFeatureSerialized } from '../../../util/index.ts';
2
+ import type { CanonicalTranscripts } from '../../../util/isoformRank.ts';
2
3
  import type { SequenceHoverTarget } from '../model.ts';
3
- declare const FeatureSequenceDialog: ({ feature, error, session, assemblyName, hoverTarget, handleClose, }: {
4
+ declare const FeatureSequenceDialog: ({ feature, error, session, assemblyName, hoverTarget, canonicalTranscripts, handleClose, }: {
4
5
  feature: SimpleFeatureSerialized | undefined;
5
6
  error?: unknown;
6
7
  session: AbstractSessionModel;
7
8
  assemblyName: string | undefined;
8
9
  hoverTarget?: SequenceHoverTarget;
10
+ canonicalTranscripts?: CanonicalTranscripts;
9
11
  handleClose: () => void;
10
12
  }) => import("react").JSX.Element;
11
13
  export default FeatureSequenceDialog;
@@ -17,13 +17,13 @@ const useStyles = makeStyles()({
17
17
  // The settings model lives for as long as the dialog does: it holds no
18
18
  // snapshot and reads no tree, so nothing has to carry one around on the chance
19
19
  // a sequence panel is opened.
20
- const FeatureSequenceDialog = observer(function FeatureSequenceDialog({ feature, error, session, assemblyName, hoverTarget, handleClose, }) {
20
+ const FeatureSequenceDialog = observer(function FeatureSequenceDialog({ feature, error, session, assemblyName, hoverTarget, canonicalTranscripts, handleClose, }) {
21
21
  const { classes } = useStyles();
22
22
  const [model] = useState(() => SequenceFeatureDetailsF().create());
23
23
  return (_jsx(InfoDialog, { maxWidth: "xl", open: true, title: feature
24
24
  ? `Feature sequence - ${feature.name || feature.id || feature.type}`
25
25
  : 'Feature sequence', onClose: () => {
26
26
  handleClose();
27
- }, children: _jsx("div", { className: classes.content, children: error ? (_jsx(ErrorBanner, { error: error })) : feature ? (_jsx(SequenceFeatureDetails, { model: model, session: session, assemblyName: assemblyName, feature: feature, hoverTarget: hoverTarget, showOpenInDialog: false })) : (_jsx(LoadingEllipses, { message: "Loading feature" })) }) }));
27
+ }, children: _jsx("div", { className: classes.content, children: error ? (_jsx(ErrorBanner, { error: error })) : feature ? (_jsx(SequenceFeatureDetails, { model: model, session: session, assemblyName: assemblyName, feature: feature, hoverTarget: hoverTarget, showOpenInDialog: false, canonicalTranscripts: canonicalTranscripts })) : (_jsx(LoadingEllipses, { message: "Loading feature" })) }) }));
28
28
  });
29
29
  export default FeatureSequenceDialog;
@@ -1,10 +1,11 @@
1
1
  import type { SimpleFeatureSerialized, SimpleFeatureSerializedNoId } from '../../util/index.ts';
2
+ import type { CanonicalTranscripts } from '../../util/isoformRank.ts';
2
3
  import type { SequenceDisplayMode, ShowCoordinatesMode } from './model.ts';
3
4
  export declare function featureHasCDS(feature: SimpleFeatureSerializedNoId): boolean;
4
5
  export declare function featureHasExon(feature: SimpleFeatureSerializedNoId): boolean;
5
6
  export declare function featureHasExonOrCDS(feature: SimpleFeatureSerializedNoId): boolean;
6
7
  export declare function getTranscripts(feature: SimpleFeatureSerialized): SimpleFeatureSerialized[];
7
- export declare function pickDefaultTranscriptIndex(transcripts: SimpleFeatureSerialized[]): number;
8
+ export declare function pickDefaultTranscriptIndex(transcripts: SimpleFeatureSerialized[], canonical?: CanonicalTranscripts): number;
8
9
  export declare function getDefaultMode(feature: SimpleFeatureSerializedNoId): SequenceDisplayMode;
9
10
  export declare function modeHasUpDownstream(mode: SequenceDisplayMode): boolean;
10
11
  export declare function modeSupportsRevcomp(mode: SequenceDisplayMode): boolean;
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_CANONICAL_TRANSCRIPTS, isoformScorer, rankIsoforms, serializedIsoformReader, } from '../../util/isoformRank.js';
1
2
  // these predicates only ever read type/subfeatures, so they accept a bare
2
3
  // subfeature (no guaranteed uniqueId) as readily as a full feature
3
4
  export function featureHasCDS(feature) {
@@ -30,20 +31,12 @@ export function getTranscripts(feature) {
30
31
  uniqueId: `${feature.uniqueId}_${idx}`,
31
32
  })) ?? []);
32
33
  }
33
- // index of the transcript the gene glyph itself would collapse to in
34
- // 'longestCoding' mode (RenderFeatureDataRPC/glyphs/subfeatures.ts): prefer a
35
- // coding transcript, then the longest span — so the sequence panel's default
36
- // matches what the track already drew. A feature that is itself a transcript
37
- // (clicked directly, not via its container gene) has no nested transcripts to
38
- // autopick from.
39
- export function pickDefaultTranscriptIndex(transcripts) {
40
- if (transcripts.length === 0) {
41
- return 0;
42
- }
43
- const coding = transcripts.filter(t => featureHasCDS(t));
44
- const candidates = coding.length > 0 ? coding : transcripts;
45
- const longest = candidates.reduce((a, b) => b.end - b.start > a.end - a.start ? b : a);
46
- return transcripts.indexOf(longest);
34
+ // The isoform the canvas gene glyph draws first, so the panel opens on the
35
+ // transcript the track showed. A feature that is itself a transcript has no
36
+ // nested transcripts to pick from.
37
+ export function pickDefaultTranscriptIndex(transcripts, canonical = DEFAULT_CANONICAL_TRANSCRIPTS) {
38
+ const [best] = rankIsoforms(transcripts, isoformScorer(serializedIsoformReader, canonical));
39
+ return best ? transcripts.indexOf(best) : 0;
47
40
  }
48
41
  export function getDefaultMode(feature) {
49
42
  return featureHasCDS(feature)
@@ -51,9 +51,9 @@ const CDNASequence = observer(function CDNASequence({ cds, exons, sequence, upst
51
51
  }
52
52
  // The stretches of the feature its own exons do not reach. Real annotations
53
53
  // produce them: CAT/liftoff emits a transcript spanning the source alignment
54
- // with exons only where the CDS is supported, so KCNT2 in
55
- // `hprc_cfhr_HG00099.1.genes.gff3.gz` runs 10,207 bp past its first exon and
56
- // 28 of that corpus's 438 transcripts do the same.
54
+ // with exons only where the CDS is supported, so KCNT2 in HG00099.1's HPRC CAT
55
+ // annotation runs 10,207 bp past its first exon, and 28 of the 438 transcripts
56
+ // around CFH do the same.
57
57
  //
58
58
  // Uncolored, like the inter-exon gaps they are — the annotation does not call
59
59
  // them exonic, so stretching the first and last exon over them (which is what
@@ -1,6 +1,7 @@
1
1
  import type PluginManager from '../PluginManager.ts';
2
2
  import type { AnyConfigurationModel } from '../configuration/index.ts';
3
3
  import type { ParentFeatureSummary, SimpleFeatureSerialized } from '../util/index.ts';
4
+ import type { CanonicalTranscripts } from '../util/isoformRank.ts';
4
5
  import type { SequenceHoverPosition } from './SequenceFeatureDetails/model.ts';
5
6
  import type { FormatDetailsTiers } from './formatDetails.ts';
6
7
  import type { Descriptors, MaybeSerializedFeat } from './types.tsx';
@@ -88,6 +89,12 @@ export declare function stateModelFactory(pluginManager: PluginManager): import(
88
89
  feature?: SimpleFeatureSerialized;
89
90
  error?: Error;
90
91
  };
92
+ /**
93
+ * #getter
94
+ * the isoform tags the open track's display ranks by, so the sequence
95
+ * panel opens on the transcript the track draws first
96
+ */
97
+ readonly canonicalTranscripts: CanonicalTranscripts;
91
98
  /**
92
99
  * #getter
93
100
  * levels of subfeature card the panel renders; unset means no limit
@@ -1,6 +1,7 @@
1
1
  import { getEnv, isStateTreeNode, types } from '@jbrowse/mobx-state-tree';
2
2
  import { hydrateTrackConfig } from '../configuration/index.js';
3
3
  import { getSession } from '../util/index.js';
4
+ import { DEFAULT_CANONICAL_TRANSCRIPTS, isCanonicalTranscriptsHost, } from '../util/isoformRank.js';
4
5
  import { ElementId } from '../util/types/mst.js';
5
6
  import { SequenceFeatureDetailsF } from './SequenceFeatureDetails/model.js';
6
7
  import { applyFormatDetails, formatDetailsNumber } from './formatDetails.js';
@@ -116,6 +117,16 @@ export function stateModelFactory(pluginManager) {
116
117
  return { error };
117
118
  }
118
119
  },
120
+ /**
121
+ * #getter
122
+ * the isoform tags the open track's display ranks by, so the sequence
123
+ * panel opens on the transcript the track draws first
124
+ */
125
+ get canonicalTranscripts() {
126
+ const track = self.track;
127
+ return (track?.displays.find(isCanonicalTranscriptsHost)
128
+ ?.canonicalTranscripts ?? DEFAULT_CANONICAL_TRANSCRIPTS);
129
+ },
119
130
  /**
120
131
  * #getter
121
132
  * levels of subfeature card the panel renders; unset means no limit
@@ -1,4 +1,5 @@
1
1
  import type { MenuItem } from '../ui/MenuTypes.ts';
2
+ import type { CanonicalTranscripts } from '../util/isoformRank.ts';
2
3
  import type { SimpleFeatureSerialized } from '../util/simpleFeature.ts';
3
4
  import type { SequenceFeatureDetailsModel, SequenceHoverTarget } from './SequenceFeatureDetails/model.ts';
4
5
  import type { IStateTreeNode } from '@jbrowse/mobx-state-tree';
@@ -9,6 +10,7 @@ export interface FeatureDetailsModel extends IStateTreeNode, SequenceHoverTarget
9
10
  view: {
10
11
  assemblyNames: string[];
11
12
  } | undefined;
13
+ canonicalTranscripts?: CanonicalTranscripts;
12
14
  }
13
15
  export interface Descriptors {
14
16
  [key: string]: React.ReactNode | Descriptors;
@@ -375,9 +375,12 @@ export default class PluginManager {
375
375
  * frozen object, so a hit can only ever come from this same instance and this
376
376
  * same track type. See ADR-031.
377
377
  *
378
- * The node is never mutated: admin edits replace the frozen entry (new
379
- * identity drops the WeakMap entry), and a non-admin's edits go to a private
380
- * session working copy, not here (ADR-032). Both levels are `WeakMap`s so
378
+ * The node is never mutated: every edit goes to a private session working
379
+ * copy, not here, and is stored as a delta whose merged config is a new
380
+ * object (ADR-032). In a session composing `SessionTracksManagerSessionMixin`,
381
+ * which every product does, `track.configuration` resolves to that working
382
+ * copy, so this memo serves `hydrateTrackConfig` and sessions without the
383
+ * mixin. Both levels are `WeakMap`s so
381
384
  * entries collect normally — no manual invalidation needed, which is why this
382
385
  * is a memo and not a cache with a lifecycle to expose.
383
386
  */
@@ -155,9 +155,12 @@ export default class PluginManager {
155
155
  * frozen object, so a hit can only ever come from this same instance and this
156
156
  * same track type. See ADR-031.
157
157
  *
158
- * The node is never mutated: admin edits replace the frozen entry (new
159
- * identity drops the WeakMap entry), and a non-admin's edits go to a private
160
- * session working copy, not here (ADR-032). Both levels are `WeakMap`s so
158
+ * The node is never mutated: every edit goes to a private session working
159
+ * copy, not here, and is stored as a delta whose merged config is a new
160
+ * object (ADR-032). In a session composing `SessionTracksManagerSessionMixin`,
161
+ * which every product does, `track.configuration` resolves to that working
162
+ * copy, so this memo serves `hydrateTrackConfig` and sessions without the
163
+ * mixin. Both levels are `WeakMap`s so
161
164
  * entries collect normally — no manual invalidation needed, which is why this
162
165
  * is a memo and not a cache with a lifecycle to expose.
163
166
  */