react-msaview 8.3.0 → 8.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/bundle/index.js +499 -295
  2. package/bundle/index.js.map +4 -4
  3. package/dist/blockFasta.d.ts +6 -0
  4. package/dist/blockFasta.js +12 -0
  5. package/dist/blockFasta.js.map +1 -0
  6. package/dist/colorSchemes.d.ts +7 -0
  7. package/dist/colorSchemes.js +15 -0
  8. package/dist/colorSchemes.js.map +1 -1
  9. package/dist/components/MSAView.js +15 -9
  10. package/dist/components/MSAView.js.map +1 -1
  11. package/dist/components/MSAViewer.d.ts +28 -7
  12. package/dist/components/MSAViewer.js +61 -22
  13. package/dist/components/MSAViewer.js.map +1 -1
  14. package/dist/components/MetadataTable.d.ts +5 -0
  15. package/dist/components/MetadataTable.js +29 -0
  16. package/dist/components/MetadataTable.js.map +1 -0
  17. package/dist/components/Track.js +11 -2
  18. package/dist/components/Track.js.map +1 -1
  19. package/dist/components/dialogs/MetadataDialog.js +5 -6
  20. package/dist/components/dialogs/MetadataDialog.js.map +1 -1
  21. package/dist/components/header/ColorSchemeMenu.d.ts +6 -0
  22. package/dist/components/header/ColorSchemeMenu.js +23 -5
  23. package/dist/components/header/ColorSchemeMenu.js.map +1 -1
  24. package/dist/components/header/GoToBox.d.ts +338 -0
  25. package/dist/components/header/GoToBox.js +31 -0
  26. package/dist/components/header/GoToBox.js.map +1 -0
  27. package/dist/components/header/Header.js +4 -2
  28. package/dist/components/header/Header.js.map +1 -1
  29. package/dist/components/header/SelectionMenu.d.ts +12 -0
  30. package/dist/components/header/SelectionMenu.js +72 -0
  31. package/dist/components/header/SelectionMenu.js.map +1 -0
  32. package/dist/components/header/goTo.d.ts +20 -0
  33. package/dist/components/header/goTo.js +69 -0
  34. package/dist/components/header/goTo.js.map +1 -0
  35. package/dist/components/msa/MSACanvas.js +13 -2
  36. package/dist/components/msa/MSACanvas.js.map +1 -1
  37. package/dist/components/msa/MSACanvasBlock.js +5 -1
  38. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  39. package/dist/components/msa/msaKeys.js +6 -0
  40. package/dist/components/msa/msaKeys.js.map +1 -1
  41. package/dist/components/msa/msaRaster.js +1 -0
  42. package/dist/components/msa/msaRaster.js.map +1 -1
  43. package/dist/components/msa/renderHighlights.d.ts +14 -3
  44. package/dist/components/msa/renderHighlights.js +29 -4
  45. package/dist/components/msa/renderHighlights.js.map +1 -1
  46. package/dist/components/msa/tileColor.js +9 -8
  47. package/dist/components/msa/tileColor.js.map +1 -1
  48. package/dist/components/msa/useMsaBlockMouse.d.ts +1 -1
  49. package/dist/components/msa/useMsaBlockMouse.js +18 -11
  50. package/dist/components/msa/useMsaBlockMouse.js.map +1 -1
  51. package/dist/components/overlayColors.d.ts +2 -0
  52. package/dist/components/overlayColors.js +3 -0
  53. package/dist/components/overlayColors.js.map +1 -1
  54. package/dist/components/selectionDrag.d.ts +17 -0
  55. package/dist/components/selectionDrag.js +30 -0
  56. package/dist/components/selectionDrag.js.map +1 -0
  57. package/dist/components/tracks/useTrackHover.d.ts +3 -1
  58. package/dist/components/tracks/useTrackHover.js +26 -6
  59. package/dist/components/tracks/useTrackHover.js.map +1 -1
  60. package/dist/components/tree/dialogs/TreeNodeInfoDialog.js +9 -8
  61. package/dist/components/tree/dialogs/TreeNodeInfoDialog.js.map +1 -1
  62. package/dist/constants.d.ts +1 -1
  63. package/dist/constants.js +6 -6
  64. package/dist/constants.js.map +1 -1
  65. package/dist/element.d.ts +7 -3
  66. package/dist/element.js +32 -4
  67. package/dist/element.js.map +1 -1
  68. package/dist/featuresLayer.d.ts +8 -0
  69. package/dist/featuresLayer.js +42 -0
  70. package/dist/featuresLayer.js.map +1 -0
  71. package/dist/index.d.ts +1 -1
  72. package/dist/model/DataModel.d.ts +1 -1
  73. package/dist/model/DataModel.js +3 -3
  74. package/dist/model/DataModel.js.map +1 -1
  75. package/dist/model/msaModel.d.ts +14 -1
  76. package/dist/model/msaModel.js +17 -1
  77. package/dist/model/msaModel.js.map +1 -1
  78. package/dist/model.d.ts +242 -187
  79. package/dist/model.js +264 -45
  80. package/dist/model.js.map +1 -1
  81. package/dist/svgTestUtil.d.ts +284 -336
  82. package/dist/types.d.ts +50 -0
  83. package/dist/version.d.ts +1 -1
  84. package/dist/version.js +1 -1
  85. package/package.json +6 -6
  86. package/src/blockFasta.ts +18 -0
  87. package/src/colorSchemes.ts +20 -0
  88. package/src/components/MSAView.tsx +16 -9
  89. package/src/components/MSAViewer.tsx +95 -26
  90. package/src/components/MetadataTable.tsx +55 -0
  91. package/src/components/Track.tsx +14 -1
  92. package/src/components/dialogs/MetadataDialog.tsx +9 -8
  93. package/src/components/header/ColorSchemeMenu.tsx +31 -12
  94. package/src/components/header/GoToBox.tsx +64 -0
  95. package/src/components/header/Header.tsx +6 -2
  96. package/src/components/header/SelectionMenu.tsx +117 -0
  97. package/src/components/header/goTo.ts +75 -0
  98. package/src/components/msa/MSACanvas.tsx +12 -2
  99. package/src/components/msa/MSACanvasBlock.tsx +5 -1
  100. package/src/components/msa/msaKeys.ts +6 -0
  101. package/src/components/msa/msaRaster.ts +1 -0
  102. package/src/components/msa/renderHighlights.ts +46 -4
  103. package/src/components/msa/tileColor.ts +9 -8
  104. package/src/components/msa/useMsaBlockMouse.ts +24 -27
  105. package/src/components/overlayColors.ts +4 -0
  106. package/src/components/selectionDrag.ts +42 -0
  107. package/src/components/tracks/useTrackHover.ts +28 -6
  108. package/src/components/tree/dialogs/TreeNodeInfoDialog.tsx +14 -16
  109. package/src/constants.ts +6 -6
  110. package/src/element.ts +45 -5
  111. package/src/featuresLayer.ts +48 -0
  112. package/src/index.ts +5 -0
  113. package/src/model/DataModel.ts +3 -3
  114. package/src/model/msaModel.ts +19 -1
  115. package/src/model.ts +313 -55
  116. package/src/types.ts +58 -0
  117. package/src/version.ts +1 -1
  118. package/dist/components/header/RowSearch.d.ts +0 -6
  119. package/dist/components/header/RowSearch.js +0 -15
  120. package/dist/components/header/RowSearch.js.map +0 -1
  121. package/dist/components/header/revealRow.d.ts +0 -3
  122. package/dist/components/header/revealRow.js +0 -15
  123. package/dist/components/header/revealRow.js.map +0 -1
  124. package/src/components/header/RowSearch.tsx +0 -34
  125. package/src/components/header/revealRow.ts +0 -17
package/dist/model.js CHANGED
@@ -4,9 +4,10 @@ import { ElementId, FileLocation } from '@jbrowse/core/util/types/mst';
4
4
  import { addDisposer, applySnapshot, cast, getSnapshot, isAlive, types, } from '@jbrowse/mobx-state-tree';
5
5
  import { autorun, compareStructural, computed, reaction, transaction, } from 'mobx';
6
6
  import { generateNodeIds, gffToAnnotations, interProScanToAnnotations, parseEmfTree, parseGFF, parseMSA, parseNewick, } from 'msa-parsers';
7
+ import { blockFasta } from './blockFasta.js';
7
8
  import { calculateBlocks } from './calculateBlocks.js';
8
9
  import { clustalXColumnColors } from './clustalX.js';
9
- import colorSchemes from './colorSchemes.js';
10
+ import colorSchemes, { letterColorTable } from './colorSchemes.js';
10
11
  import { columnCountsFromRows, letterOfResidueSlot } from './columnCounts.js';
11
12
  import { columnStats } from './columnStats.js';
12
13
  import { packDomainLanes } from './components/msa/packDomainLanes.js';
@@ -14,10 +15,11 @@ import { visibleColRange } from './components/msa/visibleColRange.js';
14
15
  import TrackBlocks from './components/tracks/TrackBlocks.js';
15
16
  import { cladeGutterWidth } from './components/tree/cladeBrackets.js';
16
17
  import { scaleBarLength } from './components/tree/scaleBar.js';
17
- import { cladeHighlightAlpha, cladeHighlightColor, defaultAllowedGappyness, defaultColWidth, defaultColorSchemeName, defaultCurrentAlignment, defaultDrawMsaLetters, defaultFeaturePanelWidth, defaultHeight, defaultHideGaps, defaultRowHeight, defaultScrollX, defaultScrollY, defaultScrollZoom, defaultScrollZoomAxis, defaultShowDomainLegend, defaultShowDomains, defaultSubFeatureRows, defaultTreeWidth, labelReferenceFontSize, maxCellSize, maxInlineSnapshotBytes, maxNeighborJoiningRows, minColWidth, minLetterColWidth, minLetterRowHeight, minRowHeight, rowPanelHeaderHeight, rowTintAlpha, scrollZoomAxes, segmentFeatureTypes, segmentShades, treeScaleBarHeight, } from './constants.js';
18
+ import { cladeHighlightAlpha, cladeHighlightColor, defaultAllowedGappyness, defaultColWidth, defaultColorSchemeName, defaultCurrentAlignment, defaultDrawMsaLetters, defaultFeaturePanelWidth, defaultHeight, defaultHideGaps, defaultRowHeight, defaultScrollX, defaultScrollY, defaultScrollZoom, defaultScrollZoomAxis, defaultShowDomainLegend, defaultShowDomains, defaultSubFeatureRows, defaultTreeWidth, labelReferenceFontSize, maxCellSize, maxInlineSnapshotBytes as defaultMaxInlineSnapshotBytes, maxNeighborJoiningRows, minColWidth, minLetterColWidth, minLetterRowHeight, minRowHeight, rowPanelHeaderHeight, rowTintAlpha, scrollZoomAxes, segmentFeatureTypes, segmentShades, treeScaleBarHeight, } from './constants.js';
18
19
  import { createPaletteMap } from './createPaletteMap.js';
19
20
  import { exportFileName } from './exportFileName.js';
20
21
  import { featureField, featureName } from './featureFields.js';
22
+ import { featuresToAnnotations } from './featuresLayer.js';
21
23
  import { fetchTextWithProgress, isAbortError } from './fetchUtils.js';
22
24
  import { flatToTree } from './flatToTree.js';
23
25
  import { calcDepthToLeaf, clusterLayout, collapse, find, forEachDescendant, hierarchy, layoutTree, leafIndex, leaves, maxLength, mrca, nodeCoveringRows, setBrLength, sort, sum as hierarchySum, } from './hierarchy.js';
@@ -82,6 +84,23 @@ function rowPanelWidth(panel, view) {
82
84
  return (panel.width ??
83
85
  (panel.kind === 'features' ? defaultFeaturePanelWidth : view.rowHeight));
84
86
  }
87
+ // one row of the row table as the channels read it. A number or a boolean
88
+ // reads as its string, the way a GFF value does, and null or an object drops
89
+ function stringFields(row) {
90
+ if (typeof row !== 'object' || row === null) {
91
+ return undefined;
92
+ }
93
+ const fields = {};
94
+ for (const [field, value] of Object.entries(row)) {
95
+ if (typeof value === 'string') {
96
+ fields[field] = value;
97
+ }
98
+ else if (typeof value === 'number' || typeof value === 'boolean') {
99
+ fields[field] = String(value);
100
+ }
101
+ }
102
+ return fields;
103
+ }
85
104
  // the color each row takes from a field of the row table under a scale
86
105
  function colorByRow(rowData, field, colorOf) {
87
106
  const colors = new Map();
@@ -218,11 +237,17 @@ function featurePanelSpans({ panel, width, colWidth, domainBands, annotationsByR
218
237
  })),
219
238
  ]));
220
239
  }
221
- // a scale over the values a field takes across the features drawn
222
- function resolveFeatureScale(field, scale, annotations) {
240
+ // a scale over the values a field takes across every feature loaded, so hiding
241
+ // a type in the filter dialog leaves the rest in their colors, and a legend of
242
+ // the values the features drawn carry
243
+ function resolveFeatureScale(field, scale, annotations, drawn) {
244
+ const valuesOf = (list) => list.map(a => featureField(a, field)).filter(notEmpty);
245
+ const resolved = resolveScale(scale, valuesOf(annotations));
246
+ const shown = new Set(valuesOf(drawn));
223
247
  return {
224
248
  field,
225
- ...resolveScale(scale, annotations.map(a => featureField(a, field)).filter(notEmpty)),
249
+ ...resolved,
250
+ legend: resolved.legend.filter(entry => shown.has(entry.id)),
226
251
  };
227
252
  }
228
253
  const columnTrackBytes = new WeakMap();
@@ -239,7 +264,7 @@ function columnTrackSize(track) {
239
264
  function columnTrackSizes(tracks) {
240
265
  return (tracks ?? []).map(columnTrackSize);
241
266
  }
242
- function smallColumnTracks(tracks) {
267
+ function smallColumnTracks(tracks, maxInlineSnapshotBytes) {
243
268
  const sizes = columnTrackSizes(tracks);
244
269
  const kept = tracks?.filter((_t, i) => sizes[i] <= maxInlineSnapshotBytes);
245
270
  return kept?.length ? { columnTracks: kept } : {};
@@ -265,6 +290,7 @@ export const preservedOnReset = new Set([
265
290
  'scrollZoomAxis',
266
291
  'bgColor',
267
292
  'colorSchemeName',
293
+ 'customColorScheme',
268
294
  'showColumnStats',
269
295
  'drawLabels',
270
296
  'labelsAlignRight',
@@ -474,6 +500,20 @@ function overflows(content, viewport) {
474
500
  function inRanges(ranges, position) {
475
501
  return !!ranges?.some(([start, end]) => position >= start && position <= end);
476
502
  }
503
+ // sorted integers as inclusive [first, last] runs of consecutive values
504
+ function consecutiveRuns(sorted) {
505
+ const runs = [];
506
+ for (const n of sorted) {
507
+ const last = runs.at(-1);
508
+ if (last && n <= last[1] + 1) {
509
+ last[1] = Math.max(last[1], n);
510
+ }
511
+ else {
512
+ runs.push([n, n]);
513
+ }
514
+ }
515
+ return runs;
516
+ }
477
517
  // the value every one of a node's children reports, or undefined where one of
478
518
  // them has no value or they disagree
479
519
  function sharedValue(values) {
@@ -514,8 +554,13 @@ function preorder(tree) {
514
554
  * const root = RootModel.create({})
515
555
  * root.view.setData({ msa: '>seq1\nACGT\n>seq2\nACGT' })
516
556
  * ```
557
+ *
558
+ * `maxInlineSnapshotBytes` is the largest pasted or local document, and the
559
+ * largest data track, the snapshot keeps; a larger one stays in the live model
560
+ * and `unshareableData` names it. The standalone app, whose links carry the
561
+ * snapshot in the URL fragment, raises it.
517
562
  */
518
- function stateModelFactory() {
563
+ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBytes, } = {}) {
519
564
  return types
520
565
  .compose(DialogQueueSessionMixin(), TreeModelF(), MSAModelF(), types.model('MsaView', {
521
566
  /**
@@ -676,7 +721,7 @@ function stateModelFactory() {
676
721
  * data from the loaded tree/msa/treeMetadata, generally loaded by
677
722
  * autorun
678
723
  */
679
- data: types.optional(DataModelF(), {}),
724
+ data: types.optional(DataModelF(maxInlineSnapshotBytes), {}),
680
725
  /**
681
726
  * #property
682
727
  * the user's explicit hide choices per annotation accession, keyed by
@@ -706,6 +751,21 @@ function stateModelFactory() {
706
751
  * Persists in the snapshot and the URL.
707
752
  */
708
753
  highlights: stripDefault(types.array(types.frozen()), []),
754
+ /**
755
+ * #property
756
+ * features on the rows as data, `{row, start, end, name, ...}` in
757
+ * residues of each row, drawn with the GFF's annotations. Persists in
758
+ * the snapshot and the URL.
759
+ */
760
+ features: stripDefault(types.array(types.frozen()), []),
761
+ /**
762
+ * #property
763
+ * the block the reader selected: `{start, end}` columns of the file,
764
+ * 1-based inclusive like a column highlight, and `rows` by name, every
765
+ * row where absent. Undefined until something is selected, so a view
766
+ * without one adds nothing to the shared URL.
767
+ */
768
+ selection: types.frozen(),
709
769
  /**
710
770
  * #property
711
771
  * where the view opens, in `highlights` coordinates: `{row, start,
@@ -980,6 +1040,33 @@ function stateModelFactory() {
980
1040
  setHighlights(highlights) {
981
1041
  self.highlights.replace(highlights);
982
1042
  },
1043
+ /**
1044
+ * #action
1045
+ */
1046
+ setFeatures(features) {
1047
+ self.features.replace(features);
1048
+ },
1049
+ /**
1050
+ * #action
1051
+ * select a block, in `selection` coordinates. `start` and `end` may come
1052
+ * in either order.
1053
+ */
1054
+ setSelection(selection) {
1055
+ const next = selection && {
1056
+ start: Math.min(selection.start, selection.end),
1057
+ end: Math.max(selection.start, selection.end),
1058
+ ...(selection.rows ? { rows: selection.rows } : {}),
1059
+ };
1060
+ if (!compareStructural(next, self.selection)) {
1061
+ self.selection = next;
1062
+ }
1063
+ },
1064
+ /**
1065
+ * #action
1066
+ */
1067
+ clearSelection() {
1068
+ this.setSelection(undefined);
1069
+ },
983
1070
  /**
984
1071
  * #action
985
1072
  * replace the clades the viewer marks (see docs/layers.md)
@@ -1137,25 +1224,25 @@ function stateModelFactory() {
1137
1224
  * #action
1138
1225
  */
1139
1226
  setMSAFilehandle(msaFilehandle) {
1140
- self.msaFilehandle = msaFilehandle;
1227
+ self.msaFilehandle = cast(msaFilehandle);
1141
1228
  },
1142
1229
  /**
1143
1230
  * #action
1144
1231
  */
1145
1232
  setTreeFilehandle(treeFilehandle) {
1146
- self.treeFilehandle = treeFilehandle;
1233
+ self.treeFilehandle = cast(treeFilehandle);
1147
1234
  },
1148
1235
  /**
1149
1236
  * #action
1150
1237
  */
1151
1238
  setTreeMetadataFilehandle(treeMetadataFilehandle) {
1152
- self.treeMetadataFilehandle = treeMetadataFilehandle;
1239
+ self.treeMetadataFilehandle = cast(treeMetadataFilehandle);
1153
1240
  },
1154
1241
  /**
1155
1242
  * #action
1156
1243
  */
1157
1244
  setGFFFilehandle(gffFilehandle) {
1158
- self.gffFilehandle = gffFilehandle;
1245
+ self.gffFilehandle = cast(gffFilehandle);
1159
1246
  },
1160
1247
  /**
1161
1248
  * #action
@@ -1180,7 +1267,7 @@ function stateModelFactory() {
1180
1267
  * store the GFF text in the snapshot like the alignment and tree. The
1181
1268
  * parsed annotations are volatile and a blob filehandle is cleared once
1182
1269
  * read, so the text is the only persisted copy. An autorun parses it
1183
- * into annotations.
1270
+ * into annotations, and clears the ones it drew when the text goes.
1184
1271
  */
1185
1272
  setGFF(result) {
1186
1273
  self.data.setGFF(result);
@@ -1201,11 +1288,21 @@ function stateModelFactory() {
1201
1288
  get realAllowedGappyness() {
1202
1289
  return this.hideGapsEffective ? self.allowedGappyness : 100;
1203
1290
  },
1291
+ /**
1292
+ * #getter
1293
+ * the annotations the overlay, the legend and the encodings draw: the
1294
+ * GFF's, then the `features` layer's
1295
+ */
1296
+ get allAnnotations() {
1297
+ return self.features.length > 0
1298
+ ? [...self.annotations, ...featuresToAnnotations(self.features)]
1299
+ : self.annotations;
1300
+ },
1204
1301
  /**
1205
1302
  * #getter
1206
1303
  */
1207
1304
  get actuallyShowDomains() {
1208
- return self.showDomains && self.annotations.length > 0;
1305
+ return self.showDomains && this.allAnnotations.length > 0;
1209
1306
  },
1210
1307
  /**
1211
1308
  * #getter
@@ -1239,10 +1336,25 @@ function stateModelFactory() {
1239
1336
  * #getter
1240
1337
  */
1241
1338
  get colorScheme() {
1339
+ const { customColorScheme, colorSchemeName } = self;
1242
1340
  // colorSchemeName is a free string (menus, snapshots, URL params), so a
1243
1341
  // stale name falls back to the default
1244
- return (colorSchemes[self.colorSchemeName] ??
1245
- colorSchemes[defaultColorSchemeName]);
1342
+ return customColorScheme
1343
+ ? letterColorTable(customColorScheme)
1344
+ : (colorSchemes[colorSchemeName] ??
1345
+ colorSchemes[defaultColorSchemeName]);
1346
+ },
1347
+ /**
1348
+ * #getter
1349
+ * the scheme coloring each cell from its column's statistics, or
1350
+ * undefined while the cells take a fixed color per letter, from the
1351
+ * built-in table or from `customColorScheme`
1352
+ */
1353
+ get dynamicColorSchemeName() {
1354
+ const { customColorScheme, colorSchemeName } = self;
1355
+ return !customColorScheme && colorSchemeName.includes('dynamic')
1356
+ ? colorSchemeName
1357
+ : undefined;
1246
1358
  },
1247
1359
  /**
1248
1360
  * #getter
@@ -1263,7 +1375,7 @@ function stateModelFactory() {
1263
1375
  return !!this.tree.noTree;
1264
1376
  },
1265
1377
  get noDomains() {
1266
- return self.annotations.length === 0;
1378
+ return self.allAnnotations.length === 0;
1267
1379
  },
1268
1380
  /**
1269
1381
  * #getter
@@ -1271,9 +1383,9 @@ function stateModelFactory() {
1271
1383
  * from disk or pasted in becomes inline text, and DataModel drops an
1272
1384
  * inline document past `maxInlineSnapshotBytes`.
1273
1385
  *
1274
- * The header lists these, and the standalone app drops `?data=` from
1386
+ * The header lists these, and the standalone app drops `#data=` from
1275
1387
  * the address bar while the list is non-empty or the encoded link would
1276
- * pass 8,000 characters, so a copied link never opens an empty viewer
1388
+ * pass its `maxLinkLength`, so a copied link never opens an empty viewer
1277
1389
  * unannounced. A document fetched from a URL never appears here, since
1278
1390
  * the snapshot keeps its filehandle.
1279
1391
  *
@@ -1310,7 +1422,8 @@ function stateModelFactory() {
1310
1422
  * `data.treeMetadata`, the name that travels in existing links, so the
1311
1423
  * inline size limit and `treeMetadataFilehandle` cover it. labelWidthMap
1312
1424
  * reads it on every layout, so a malformed user-supplied file returns {}
1313
- * instead of throwing out of rendering.
1425
+ * instead of throwing out of rendering, and every field is a string by
1426
+ * the time a scale sorts it.
1314
1427
  */
1315
1428
  get rowData() {
1316
1429
  const text = self.data.treeMetadata;
@@ -1320,7 +1433,10 @@ function stateModelFactory() {
1320
1433
  try {
1321
1434
  const parsed = JSON.parse(text);
1322
1435
  return typeof parsed === 'object' && parsed !== null
1323
- ? parsed
1436
+ ? Object.fromEntries(Object.entries(parsed).map(([name, row]) => [
1437
+ name,
1438
+ stringFields(row),
1439
+ ]))
1324
1440
  : {};
1325
1441
  }
1326
1442
  catch (e) {
@@ -2586,7 +2702,7 @@ function stateModelFactory() {
2586
2702
  // first occurrence wins; only name, description and segment start are
2587
2703
  // read from it
2588
2704
  const types = new Map();
2589
- for (const annot of self.annotations) {
2705
+ for (const annot of self.allAnnotations) {
2590
2706
  if (!types.has(annot.accession)) {
2591
2707
  types.set(annot.accession, annot);
2592
2708
  }
@@ -2594,7 +2710,7 @@ function stateModelFactory() {
2594
2710
  return types;
2595
2711
  },
2596
2712
  get filteredAnnotations() {
2597
- return self.annotations.filter(r => !self.turnedOffFeatures.get(r.accession));
2713
+ return self.allAnnotations.filter(r => !self.turnedOffFeatures.get(r.accession));
2598
2714
  },
2599
2715
  get annotationsByRow() {
2600
2716
  return groupBy(this.filteredAnnotations, r => r.id);
@@ -3306,18 +3422,7 @@ function stateModelFactory() {
3306
3422
  * on every mouse move.
3307
3423
  */
3308
3424
  get highlightedColumnRuns() {
3309
- const { highlightedColumns } = self;
3310
- const runs = [];
3311
- for (const col of [...(highlightedColumns ?? [])].sort((a, b) => a - b)) {
3312
- const last = runs.at(-1);
3313
- if (last && col === last.end + 1) {
3314
- last.end = col;
3315
- }
3316
- else {
3317
- runs.push({ start: col, end: col });
3318
- }
3319
- }
3320
- return runs;
3425
+ return consecutiveRuns([...(self.highlightedColumns ?? [])].sort((a, b) => a - b)).map(([start, end]) => ({ start, end }));
3321
3426
  },
3322
3427
  /**
3323
3428
  * #method
@@ -3368,6 +3473,78 @@ function stateModelFactory() {
3368
3473
  return span ? [{ ...base, ...span, rowIndices: [] }] : [];
3369
3474
  });
3370
3475
  },
3476
+ /**
3477
+ * #getter
3478
+ * `selection` projected onto what is on screen, the way
3479
+ * `resolvedHighlights` projects a column span and a row set. A
3480
+ * selection whose columns are all hidden, or whose rows are all
3481
+ * collapsed away or unknown, resolves to undefined.
3482
+ */
3483
+ get resolvedSelection() {
3484
+ const { selection, rowNamesSet, numRows } = self;
3485
+ const span = selection &&
3486
+ self.visibleSpan({ start: selection.start, end: selection.end });
3487
+ if (!selection || !span) {
3488
+ return undefined;
3489
+ }
3490
+ const rowRuns = selection.rows
3491
+ ? consecutiveRuns(selection.rows
3492
+ .map(name => rowNamesSet.get(name))
3493
+ .filter(notEmpty)
3494
+ .sort((a, b) => a - b))
3495
+ : numRows > 0
3496
+ ? [[0, numRows - 1]]
3497
+ : [];
3498
+ return rowRuns.length > 0 ? { ...span, rowRuns } : undefined;
3499
+ },
3500
+ /**
3501
+ * #getter
3502
+ * the columns and rows the selection covers, for the header's readout.
3503
+ * Columns count the file's columns from `start` to `end`, hidden ones
3504
+ * included, as `selectionFasta` copies them.
3505
+ */
3506
+ get selectionSize() {
3507
+ const resolved = this.resolvedSelection;
3508
+ const span = this.selectionFileSpan;
3509
+ return resolved && span
3510
+ ? {
3511
+ columns: span.end - span.start + 1,
3512
+ rows: sum(resolved.rowRuns.map(([a, b]) => b - a + 1)),
3513
+ }
3514
+ : undefined;
3515
+ },
3516
+ /**
3517
+ * #getter
3518
+ * `selection`'s columns clamped to the file's
3519
+ */
3520
+ get selectionFileSpan() {
3521
+ const { selection } = self;
3522
+ const width = self.MSA?.getWidth() ?? 0;
3523
+ const start = Math.max(1, Math.floor(selection?.start ?? 0));
3524
+ const end = Math.min(width, Math.ceil(selection?.end ?? 0));
3525
+ return selection && start <= end ? { start, end } : undefined;
3526
+ },
3527
+ /**
3528
+ * #getter
3529
+ * the selected block as FASTA: the selected rows on screen, top to
3530
+ * bottom, each with its letters across the file's columns from `start`
3531
+ * to `end`, gaps included
3532
+ */
3533
+ get selectionFasta() {
3534
+ const { rowNames, rowMap } = self;
3535
+ const resolved = this.resolvedSelection;
3536
+ const span = this.selectionFileSpan;
3537
+ if (!resolved || !span) {
3538
+ return '';
3539
+ }
3540
+ const rows = resolved.rowRuns
3541
+ .flatMap(([first, last]) => rowNames.slice(first, last + 1))
3542
+ .flatMap(name => {
3543
+ const seq = rowMap.get(name);
3544
+ return seq === undefined ? [] : [[name, seq]];
3545
+ });
3546
+ return blockFasta(rows, span.start, span.end);
3547
+ },
3371
3548
  /**
3372
3549
  * #method
3373
3550
  * per-column summary statistics: consensus residue and its identity
@@ -3396,20 +3573,17 @@ function stateModelFactory() {
3396
3573
  /**
3397
3574
  * #getter
3398
3575
  * each encoding with its scale resolved against the values its field
3399
- * takes: a feature channel reads them across the features drawn, every
3576
+ * takes: a feature channel reads them across every feature loaded, every
3400
3577
  * other channel across the row table. Resolved once per change of that
3401
3578
  * table or the encodings, never per row per frame.
3402
3579
  */
3403
3580
  get resolvedEncodings() {
3404
3581
  const rows = Object.values(self.rowData);
3405
- const features = self.filteredAnnotations;
3406
3582
  return self.encodings.map(encoding => ({
3407
3583
  ...encoding,
3408
- ...resolveScale(encoding.scale, featureChannels.has(encoding.channel)
3409
- ? features
3410
- .map(a => featureField(a, encoding.field))
3411
- .filter(notEmpty)
3412
- : rows.map(row => row?.[encoding.field]).filter(notEmpty)),
3584
+ ...(featureChannels.has(encoding.channel)
3585
+ ? resolveFeatureScale(encoding.field, encoding.scale, self.allAnnotations, self.filteredAnnotations)
3586
+ : resolveScale(encoding.scale, rows.map(row => row?.[encoding.field]).filter(notEmpty))),
3413
3587
  }));
3414
3588
  },
3415
3589
  /**
@@ -3426,7 +3600,7 @@ function stateModelFactory() {
3426
3600
  if (panel.kind === 'features') {
3427
3601
  const color = panel.encoding?.color;
3428
3602
  const encoding = color
3429
- ? resolveFeatureScale(color.field, color.scale, annotations)
3603
+ ? resolveFeatureScale(color.field, color.scale, self.allAnnotations, annotations)
3430
3604
  : featureFillEncoding;
3431
3605
  const label = panel.encoding?.label;
3432
3606
  return {
@@ -3701,6 +3875,51 @@ function stateModelFactory() {
3701
3875
  self.scrollX = clamp(-span.startCol * self.colWidth, self.maxScrollX, 0);
3702
3876
  });
3703
3877
  },
3878
+ /**
3879
+ * #action
3880
+ * zoom and scroll so the selected columns fill the alignment's width,
3881
+ * and, where the selection names its rows, so those rows fill its height
3882
+ */
3883
+ zoomToSelection() {
3884
+ const { selection, resolvedSelection } = self;
3885
+ if (!selection || !resolvedSelection) {
3886
+ return;
3887
+ }
3888
+ transaction(() => {
3889
+ this.zoomToRegion({ start: selection.start, end: selection.end });
3890
+ if (selection.rows && self.viewInitialized) {
3891
+ const { rowRuns } = resolvedSelection;
3892
+ const first = rowRuns[0][0];
3893
+ const last = rowRuns.at(-1)[1];
3894
+ self.rowHeight = clamp(self.msaAreaHeight / (last - first + 1), minRowHeight, maxCellSize);
3895
+ self.scrollY = clamp(-first * self.rowHeight, self.maxScrollY, 0);
3896
+ }
3897
+ });
3898
+ },
3899
+ /**
3900
+ * #action
3901
+ * select the block between two cells given as visible column and row
3902
+ * indices, the coordinates a drag reports, clamped to the alignment. A
3903
+ * cell with no `row` selects the columns across every row, and so does a
3904
+ * block spanning every row on screen.
3905
+ */
3906
+ selectBlock(anchor, head) {
3907
+ const { numColumns, numRows, rowNames } = self;
3908
+ if (numColumns === 0 || numRows === 0) {
3909
+ return;
3910
+ }
3911
+ const ordered = (a, b, last) => [clamp(a, 0, last), clamp(b, 0, last)].sort((x, y) => x - y);
3912
+ const [startCol, endCol] = ordered(anchor.col, head.col, numColumns - 1);
3913
+ const [firstRow, lastRow] = anchor.row === undefined || head.row === undefined
3914
+ ? [0, numRows - 1]
3915
+ : ordered(anchor.row, head.row, numRows - 1);
3916
+ const everyRow = firstRow === 0 && lastRow === numRows - 1;
3917
+ self.setSelection({
3918
+ start: self.visibleColToGlobalCol(startCol) + 1,
3919
+ end: self.visibleColToGlobalCol(endCol) + 1,
3920
+ ...(everyRow ? {} : { rows: rowNames.slice(firstRow, lastRow + 1) }),
3921
+ });
3922
+ },
3704
3923
  /**
3705
3924
  * #action
3706
3925
  */
@@ -3931,7 +4150,7 @@ function stateModelFactory() {
3931
4150
  // xref solution https://github.com/mobxjs/mobx/issues/266#issuecomment-222007278
3932
4151
  // xref problem https://github.com/GMOD/react-msaview/issues/75
3933
4152
  addDisposer(self, autorun(() => {
3934
- if (self.colorSchemeName.includes('dynamic')) {
4153
+ if (self.dynamicColorSchemeName) {
3935
4154
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions
3936
4155
  self.colStats;
3937
4156
  }
@@ -3981,7 +4200,7 @@ function stateModelFactory() {
3981
4200
  // stripDefault handles per-property defaults; this drops inline documents
3982
4201
  // whose filehandle can refetch them
3983
4202
  ...rest,
3984
- ...smallColumnTracks(columnTracks),
4203
+ ...smallColumnTracks(columnTracks, maxInlineSnapshotBytes),
3985
4204
  data: {
3986
4205
  ...(rest.treeFilehandle ? {} : { tree: data.tree }),
3987
4206
  ...(rest.msaFilehandle ? {} : { msa: data.msa }),