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.
- package/bundle/index.js +499 -295
- package/bundle/index.js.map +4 -4
- package/dist/blockFasta.d.ts +6 -0
- package/dist/blockFasta.js +12 -0
- package/dist/blockFasta.js.map +1 -0
- package/dist/colorSchemes.d.ts +7 -0
- package/dist/colorSchemes.js +15 -0
- package/dist/colorSchemes.js.map +1 -1
- package/dist/components/MSAView.js +15 -9
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/MSAViewer.d.ts +28 -7
- package/dist/components/MSAViewer.js +61 -22
- package/dist/components/MSAViewer.js.map +1 -1
- package/dist/components/MetadataTable.d.ts +5 -0
- package/dist/components/MetadataTable.js +29 -0
- package/dist/components/MetadataTable.js.map +1 -0
- package/dist/components/Track.js +11 -2
- package/dist/components/Track.js.map +1 -1
- package/dist/components/dialogs/MetadataDialog.js +5 -6
- package/dist/components/dialogs/MetadataDialog.js.map +1 -1
- package/dist/components/header/ColorSchemeMenu.d.ts +6 -0
- package/dist/components/header/ColorSchemeMenu.js +23 -5
- package/dist/components/header/ColorSchemeMenu.js.map +1 -1
- package/dist/components/header/GoToBox.d.ts +338 -0
- package/dist/components/header/GoToBox.js +31 -0
- package/dist/components/header/GoToBox.js.map +1 -0
- package/dist/components/header/Header.js +4 -2
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/SelectionMenu.d.ts +12 -0
- package/dist/components/header/SelectionMenu.js +72 -0
- package/dist/components/header/SelectionMenu.js.map +1 -0
- package/dist/components/header/goTo.d.ts +20 -0
- package/dist/components/header/goTo.js +69 -0
- package/dist/components/header/goTo.js.map +1 -0
- package/dist/components/msa/MSACanvas.js +13 -2
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +5 -1
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/msaKeys.js +6 -0
- package/dist/components/msa/msaKeys.js.map +1 -1
- package/dist/components/msa/msaRaster.js +1 -0
- package/dist/components/msa/msaRaster.js.map +1 -1
- package/dist/components/msa/renderHighlights.d.ts +14 -3
- package/dist/components/msa/renderHighlights.js +29 -4
- package/dist/components/msa/renderHighlights.js.map +1 -1
- package/dist/components/msa/tileColor.js +9 -8
- package/dist/components/msa/tileColor.js.map +1 -1
- package/dist/components/msa/useMsaBlockMouse.d.ts +1 -1
- package/dist/components/msa/useMsaBlockMouse.js +18 -11
- package/dist/components/msa/useMsaBlockMouse.js.map +1 -1
- package/dist/components/overlayColors.d.ts +2 -0
- package/dist/components/overlayColors.js +3 -0
- package/dist/components/overlayColors.js.map +1 -1
- package/dist/components/selectionDrag.d.ts +17 -0
- package/dist/components/selectionDrag.js +30 -0
- package/dist/components/selectionDrag.js.map +1 -0
- package/dist/components/tracks/useTrackHover.d.ts +3 -1
- package/dist/components/tracks/useTrackHover.js +26 -6
- package/dist/components/tracks/useTrackHover.js.map +1 -1
- package/dist/components/tree/dialogs/TreeNodeInfoDialog.js +9 -8
- package/dist/components/tree/dialogs/TreeNodeInfoDialog.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +6 -6
- package/dist/constants.js.map +1 -1
- package/dist/element.d.ts +7 -3
- package/dist/element.js +32 -4
- package/dist/element.js.map +1 -1
- package/dist/featuresLayer.d.ts +8 -0
- package/dist/featuresLayer.js +42 -0
- package/dist/featuresLayer.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/model/DataModel.d.ts +1 -1
- package/dist/model/DataModel.js +3 -3
- package/dist/model/DataModel.js.map +1 -1
- package/dist/model/msaModel.d.ts +14 -1
- package/dist/model/msaModel.js +17 -1
- package/dist/model/msaModel.js.map +1 -1
- package/dist/model.d.ts +242 -187
- package/dist/model.js +264 -45
- package/dist/model.js.map +1 -1
- package/dist/svgTestUtil.d.ts +284 -336
- package/dist/types.d.ts +50 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -6
- package/src/blockFasta.ts +18 -0
- package/src/colorSchemes.ts +20 -0
- package/src/components/MSAView.tsx +16 -9
- package/src/components/MSAViewer.tsx +95 -26
- package/src/components/MetadataTable.tsx +55 -0
- package/src/components/Track.tsx +14 -1
- package/src/components/dialogs/MetadataDialog.tsx +9 -8
- package/src/components/header/ColorSchemeMenu.tsx +31 -12
- package/src/components/header/GoToBox.tsx +64 -0
- package/src/components/header/Header.tsx +6 -2
- package/src/components/header/SelectionMenu.tsx +117 -0
- package/src/components/header/goTo.ts +75 -0
- package/src/components/msa/MSACanvas.tsx +12 -2
- package/src/components/msa/MSACanvasBlock.tsx +5 -1
- package/src/components/msa/msaKeys.ts +6 -0
- package/src/components/msa/msaRaster.ts +1 -0
- package/src/components/msa/renderHighlights.ts +46 -4
- package/src/components/msa/tileColor.ts +9 -8
- package/src/components/msa/useMsaBlockMouse.ts +24 -27
- package/src/components/overlayColors.ts +4 -0
- package/src/components/selectionDrag.ts +42 -0
- package/src/components/tracks/useTrackHover.ts +28 -6
- package/src/components/tree/dialogs/TreeNodeInfoDialog.tsx +14 -16
- package/src/constants.ts +6 -6
- package/src/element.ts +45 -5
- package/src/featuresLayer.ts +48 -0
- package/src/index.ts +5 -0
- package/src/model/DataModel.ts +3 -3
- package/src/model/msaModel.ts +19 -1
- package/src/model.ts +313 -55
- package/src/types.ts +58 -0
- package/src/version.ts +1 -1
- package/dist/components/header/RowSearch.d.ts +0 -6
- package/dist/components/header/RowSearch.js +0 -15
- package/dist/components/header/RowSearch.js.map +0 -1
- package/dist/components/header/revealRow.d.ts +0 -3
- package/dist/components/header/revealRow.js +0 -15
- package/dist/components/header/revealRow.js.map +0 -1
- package/src/components/header/RowSearch.tsx +0 -34
- package/src/components/header/revealRow.ts +0 -17
package/src/model.ts
CHANGED
|
@@ -26,9 +26,10 @@ import {
|
|
|
26
26
|
parseNewick,
|
|
27
27
|
} from 'msa-parsers'
|
|
28
28
|
|
|
29
|
+
import { blockFasta } from './blockFasta.ts'
|
|
29
30
|
import { calculateBlocks } from './calculateBlocks.ts'
|
|
30
31
|
import { clustalXColumnColors } from './clustalX.ts'
|
|
31
|
-
import colorSchemes from './colorSchemes.ts'
|
|
32
|
+
import colorSchemes, { letterColorTable } from './colorSchemes.ts'
|
|
32
33
|
import { columnCountsFromRows, letterOfResidueSlot } from './columnCounts.ts'
|
|
33
34
|
import { columnStats } from './columnStats.ts'
|
|
34
35
|
import { packDomainLanes } from './components/msa/packDomainLanes.ts'
|
|
@@ -58,7 +59,7 @@ import {
|
|
|
58
59
|
defaultTreeWidth,
|
|
59
60
|
labelReferenceFontSize,
|
|
60
61
|
maxCellSize,
|
|
61
|
-
maxInlineSnapshotBytes,
|
|
62
|
+
maxInlineSnapshotBytes as defaultMaxInlineSnapshotBytes,
|
|
62
63
|
maxNeighborJoiningRows,
|
|
63
64
|
minColWidth,
|
|
64
65
|
minLetterColWidth,
|
|
@@ -74,6 +75,7 @@ import {
|
|
|
74
75
|
import { createPaletteMap } from './createPaletteMap.ts'
|
|
75
76
|
import { exportFileName } from './exportFileName.ts'
|
|
76
77
|
import { featureField, featureName } from './featureFields.ts'
|
|
78
|
+
import { featuresToAnnotations } from './featuresLayer.ts'
|
|
77
79
|
import { fetchTextWithProgress, isAbortError } from './fetchUtils.ts'
|
|
78
80
|
import { flatToTree } from './flatToTree.ts'
|
|
79
81
|
import {
|
|
@@ -142,9 +144,11 @@ import type {
|
|
|
142
144
|
DomainBand,
|
|
143
145
|
Encoding,
|
|
144
146
|
EncodingChannel,
|
|
147
|
+
Feature,
|
|
145
148
|
Highlight,
|
|
146
149
|
Legend,
|
|
147
150
|
LegendEntry,
|
|
151
|
+
MsaSelection,
|
|
148
152
|
NodeWithIds,
|
|
149
153
|
Region,
|
|
150
154
|
ResidueMapping,
|
|
@@ -153,12 +157,14 @@ import type {
|
|
|
153
157
|
ResolvedEncoding,
|
|
154
158
|
ResolvedFeaturePanel,
|
|
155
159
|
ResolvedHighlight,
|
|
160
|
+
ResolvedSelection,
|
|
156
161
|
TreeOrder,
|
|
157
162
|
TreeRoot,
|
|
158
163
|
ResolvedRowPanel,
|
|
159
164
|
ResolvedStripPanel,
|
|
160
165
|
RowFeaturesSpec,
|
|
161
166
|
RowPanelSpan,
|
|
167
|
+
RowDataInput,
|
|
162
168
|
RowPanelSpec,
|
|
163
169
|
RowResidue,
|
|
164
170
|
StructureResidue,
|
|
@@ -225,6 +231,23 @@ function rowPanelWidth(panel: RowPanelSpec, view: { rowHeight: number }) {
|
|
|
225
231
|
)
|
|
226
232
|
}
|
|
227
233
|
|
|
234
|
+
// one row of the row table as the channels read it. A number or a boolean
|
|
235
|
+
// reads as its string, the way a GFF value does, and null or an object drops
|
|
236
|
+
function stringFields(row: unknown) {
|
|
237
|
+
if (typeof row !== 'object' || row === null) {
|
|
238
|
+
return undefined
|
|
239
|
+
}
|
|
240
|
+
const fields: Record<string, string> = {}
|
|
241
|
+
for (const [field, value] of Object.entries(row)) {
|
|
242
|
+
if (typeof value === 'string') {
|
|
243
|
+
fields[field] = value
|
|
244
|
+
} else if (typeof value === 'number' || typeof value === 'boolean') {
|
|
245
|
+
fields[field] = String(value)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return fields
|
|
249
|
+
}
|
|
250
|
+
|
|
228
251
|
// the color each row takes from a field of the row table under a scale
|
|
229
252
|
function colorByRow(
|
|
230
253
|
rowData: Record<string, Record<string, string> | undefined>,
|
|
@@ -425,18 +448,23 @@ function featurePanelSpans({
|
|
|
425
448
|
)
|
|
426
449
|
}
|
|
427
450
|
|
|
428
|
-
// a scale over the values a field takes across
|
|
451
|
+
// a scale over the values a field takes across every feature loaded, so hiding
|
|
452
|
+
// a type in the filter dialog leaves the rest in their colors, and a legend of
|
|
453
|
+
// the values the features drawn carry
|
|
429
454
|
function resolveFeatureScale(
|
|
430
455
|
field: string,
|
|
431
456
|
scale: ScaleSpec | undefined,
|
|
432
457
|
annotations: Annotation[],
|
|
458
|
+
drawn: Annotation[],
|
|
433
459
|
) {
|
|
460
|
+
const valuesOf = (list: Annotation[]) =>
|
|
461
|
+
list.map(a => featureField(a, field)).filter(notEmpty)
|
|
462
|
+
const resolved = resolveScale(scale, valuesOf(annotations))
|
|
463
|
+
const shown = new Set(valuesOf(drawn))
|
|
434
464
|
return {
|
|
435
465
|
field,
|
|
436
|
-
...
|
|
437
|
-
|
|
438
|
-
annotations.map(a => featureField(a, field)).filter(notEmpty),
|
|
439
|
-
),
|
|
466
|
+
...resolved,
|
|
467
|
+
legend: resolved.legend.filter(entry => shown.has(entry.id)),
|
|
440
468
|
}
|
|
441
469
|
}
|
|
442
470
|
|
|
@@ -457,7 +485,10 @@ function columnTrackSizes(tracks?: readonly ColumnTrackSpec[]) {
|
|
|
457
485
|
return (tracks ?? []).map(columnTrackSize)
|
|
458
486
|
}
|
|
459
487
|
|
|
460
|
-
function smallColumnTracks(
|
|
488
|
+
function smallColumnTracks(
|
|
489
|
+
tracks: ColumnTrackSpec[] | undefined,
|
|
490
|
+
maxInlineSnapshotBytes: number,
|
|
491
|
+
) {
|
|
461
492
|
const sizes = columnTrackSizes(tracks)
|
|
462
493
|
const kept = tracks?.filter((_t, i) => sizes[i]! <= maxInlineSnapshotBytes)
|
|
463
494
|
return kept?.length ? { columnTracks: kept } : {}
|
|
@@ -484,6 +515,7 @@ export const preservedOnReset = new Set([
|
|
|
484
515
|
'scrollZoomAxis',
|
|
485
516
|
'bgColor',
|
|
486
517
|
'colorSchemeName',
|
|
518
|
+
'customColorScheme',
|
|
487
519
|
'showColumnStats',
|
|
488
520
|
'drawLabels',
|
|
489
521
|
'labelsAlignRight',
|
|
@@ -752,6 +784,20 @@ function inRanges(ranges: [number, number][] | undefined, position: number) {
|
|
|
752
784
|
return !!ranges?.some(([start, end]) => position >= start && position <= end)
|
|
753
785
|
}
|
|
754
786
|
|
|
787
|
+
// sorted integers as inclusive [first, last] runs of consecutive values
|
|
788
|
+
function consecutiveRuns(sorted: number[]) {
|
|
789
|
+
const runs: [number, number][] = []
|
|
790
|
+
for (const n of sorted) {
|
|
791
|
+
const last = runs.at(-1)
|
|
792
|
+
if (last && n <= last[1] + 1) {
|
|
793
|
+
last[1] = Math.max(last[1], n)
|
|
794
|
+
} else {
|
|
795
|
+
runs.push([n, n])
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return runs
|
|
799
|
+
}
|
|
800
|
+
|
|
755
801
|
// the value every one of a node's children reports, or undefined where one of
|
|
756
802
|
// them has no value or they disagree
|
|
757
803
|
function sharedValue(values: (string | undefined)[]) {
|
|
@@ -794,8 +840,15 @@ function preorder(tree: NodeWithIds) {
|
|
|
794
840
|
* const root = RootModel.create({})
|
|
795
841
|
* root.view.setData({ msa: '>seq1\nACGT\n>seq2\nACGT' })
|
|
796
842
|
* ```
|
|
843
|
+
*
|
|
844
|
+
* `maxInlineSnapshotBytes` is the largest pasted or local document, and the
|
|
845
|
+
* largest data track, the snapshot keeps; a larger one stays in the live model
|
|
846
|
+
* and `unshareableData` names it. The standalone app, whose links carry the
|
|
847
|
+
* snapshot in the URL fragment, raises it.
|
|
797
848
|
*/
|
|
798
|
-
function stateModelFactory(
|
|
849
|
+
function stateModelFactory({
|
|
850
|
+
maxInlineSnapshotBytes = defaultMaxInlineSnapshotBytes,
|
|
851
|
+
}: { maxInlineSnapshotBytes?: number } = {}) {
|
|
799
852
|
return types
|
|
800
853
|
.compose(
|
|
801
854
|
DialogQueueSessionMixin(),
|
|
@@ -991,7 +1044,7 @@ function stateModelFactory() {
|
|
|
991
1044
|
* data from the loaded tree/msa/treeMetadata, generally loaded by
|
|
992
1045
|
* autorun
|
|
993
1046
|
*/
|
|
994
|
-
data: types.optional(DataModelF(), {}),
|
|
1047
|
+
data: types.optional(DataModelF(maxInlineSnapshotBytes), {}),
|
|
995
1048
|
|
|
996
1049
|
/**
|
|
997
1050
|
* #property
|
|
@@ -1022,6 +1075,21 @@ function stateModelFactory() {
|
|
|
1022
1075
|
* Persists in the snapshot and the URL.
|
|
1023
1076
|
*/
|
|
1024
1077
|
highlights: stripDefault(types.array(types.frozen<Highlight>()), []),
|
|
1078
|
+
/**
|
|
1079
|
+
* #property
|
|
1080
|
+
* features on the rows as data, `{row, start, end, name, ...}` in
|
|
1081
|
+
* residues of each row, drawn with the GFF's annotations. Persists in
|
|
1082
|
+
* the snapshot and the URL.
|
|
1083
|
+
*/
|
|
1084
|
+
features: stripDefault(types.array(types.frozen<Feature>()), []),
|
|
1085
|
+
/**
|
|
1086
|
+
* #property
|
|
1087
|
+
* the block the reader selected: `{start, end}` columns of the file,
|
|
1088
|
+
* 1-based inclusive like a column highlight, and `rows` by name, every
|
|
1089
|
+
* row where absent. Undefined until something is selected, so a view
|
|
1090
|
+
* without one adds nothing to the shared URL.
|
|
1091
|
+
*/
|
|
1092
|
+
selection: types.frozen<MsaSelection | undefined>(),
|
|
1025
1093
|
/**
|
|
1026
1094
|
* #property
|
|
1027
1095
|
* where the view opens, in `highlights` coordinates: `{row, start,
|
|
@@ -1322,6 +1390,33 @@ function stateModelFactory() {
|
|
|
1322
1390
|
setHighlights(highlights: Highlight[]) {
|
|
1323
1391
|
self.highlights.replace(highlights)
|
|
1324
1392
|
},
|
|
1393
|
+
/**
|
|
1394
|
+
* #action
|
|
1395
|
+
*/
|
|
1396
|
+
setFeatures(features: Feature[]) {
|
|
1397
|
+
self.features.replace(features)
|
|
1398
|
+
},
|
|
1399
|
+
/**
|
|
1400
|
+
* #action
|
|
1401
|
+
* select a block, in `selection` coordinates. `start` and `end` may come
|
|
1402
|
+
* in either order.
|
|
1403
|
+
*/
|
|
1404
|
+
setSelection(selection?: MsaSelection) {
|
|
1405
|
+
const next = selection && {
|
|
1406
|
+
start: Math.min(selection.start, selection.end),
|
|
1407
|
+
end: Math.max(selection.start, selection.end),
|
|
1408
|
+
...(selection.rows ? { rows: selection.rows } : {}),
|
|
1409
|
+
}
|
|
1410
|
+
if (!compareStructural(next, self.selection)) {
|
|
1411
|
+
self.selection = next
|
|
1412
|
+
}
|
|
1413
|
+
},
|
|
1414
|
+
/**
|
|
1415
|
+
* #action
|
|
1416
|
+
*/
|
|
1417
|
+
clearSelection() {
|
|
1418
|
+
this.setSelection(undefined)
|
|
1419
|
+
},
|
|
1325
1420
|
|
|
1326
1421
|
/**
|
|
1327
1422
|
* #action
|
|
@@ -1493,28 +1588,28 @@ function stateModelFactory() {
|
|
|
1493
1588
|
* #action
|
|
1494
1589
|
*/
|
|
1495
1590
|
setMSAFilehandle(msaFilehandle?: FileLocationType) {
|
|
1496
|
-
self.msaFilehandle = msaFilehandle
|
|
1591
|
+
self.msaFilehandle = cast(msaFilehandle)
|
|
1497
1592
|
},
|
|
1498
1593
|
|
|
1499
1594
|
/**
|
|
1500
1595
|
* #action
|
|
1501
1596
|
*/
|
|
1502
1597
|
setTreeFilehandle(treeFilehandle?: FileLocationType) {
|
|
1503
|
-
self.treeFilehandle = treeFilehandle
|
|
1598
|
+
self.treeFilehandle = cast(treeFilehandle)
|
|
1504
1599
|
},
|
|
1505
1600
|
|
|
1506
1601
|
/**
|
|
1507
1602
|
* #action
|
|
1508
1603
|
*/
|
|
1509
1604
|
setTreeMetadataFilehandle(treeMetadataFilehandle?: FileLocationType) {
|
|
1510
|
-
self.treeMetadataFilehandle = treeMetadataFilehandle
|
|
1605
|
+
self.treeMetadataFilehandle = cast(treeMetadataFilehandle)
|
|
1511
1606
|
},
|
|
1512
1607
|
|
|
1513
1608
|
/**
|
|
1514
1609
|
* #action
|
|
1515
1610
|
*/
|
|
1516
1611
|
setGFFFilehandle(gffFilehandle?: FileLocationType) {
|
|
1517
|
-
self.gffFilehandle = gffFilehandle
|
|
1612
|
+
self.gffFilehandle = cast(gffFilehandle)
|
|
1518
1613
|
},
|
|
1519
1614
|
|
|
1520
1615
|
/**
|
|
@@ -1543,9 +1638,9 @@ function stateModelFactory() {
|
|
|
1543
1638
|
* store the GFF text in the snapshot like the alignment and tree. The
|
|
1544
1639
|
* parsed annotations are volatile and a blob filehandle is cleared once
|
|
1545
1640
|
* read, so the text is the only persisted copy. An autorun parses it
|
|
1546
|
-
* into annotations.
|
|
1641
|
+
* into annotations, and clears the ones it drew when the text goes.
|
|
1547
1642
|
*/
|
|
1548
|
-
setGFF(result
|
|
1643
|
+
setGFF(result?: string) {
|
|
1549
1644
|
self.data.setGFF(result)
|
|
1550
1645
|
},
|
|
1551
1646
|
}))
|
|
@@ -1567,11 +1662,21 @@ function stateModelFactory() {
|
|
|
1567
1662
|
get realAllowedGappyness() {
|
|
1568
1663
|
return this.hideGapsEffective ? self.allowedGappyness : 100
|
|
1569
1664
|
},
|
|
1665
|
+
/**
|
|
1666
|
+
* #getter
|
|
1667
|
+
* the annotations the overlay, the legend and the encodings draw: the
|
|
1668
|
+
* GFF's, then the `features` layer's
|
|
1669
|
+
*/
|
|
1670
|
+
get allAnnotations(): Annotation[] {
|
|
1671
|
+
return self.features.length > 0
|
|
1672
|
+
? [...self.annotations, ...featuresToAnnotations(self.features)]
|
|
1673
|
+
: self.annotations
|
|
1674
|
+
},
|
|
1570
1675
|
/**
|
|
1571
1676
|
* #getter
|
|
1572
1677
|
*/
|
|
1573
1678
|
get actuallyShowDomains() {
|
|
1574
|
-
return self.showDomains &&
|
|
1679
|
+
return self.showDomains && this.allAnnotations.length > 0
|
|
1575
1680
|
},
|
|
1576
1681
|
/**
|
|
1577
1682
|
* #getter
|
|
@@ -1605,12 +1710,26 @@ function stateModelFactory() {
|
|
|
1605
1710
|
* #getter
|
|
1606
1711
|
*/
|
|
1607
1712
|
get colorScheme() {
|
|
1713
|
+
const { customColorScheme, colorSchemeName } = self
|
|
1608
1714
|
// colorSchemeName is a free string (menus, snapshots, URL params), so a
|
|
1609
1715
|
// stale name falls back to the default
|
|
1610
|
-
return
|
|
1611
|
-
|
|
1612
|
-
colorSchemes[
|
|
1613
|
-
|
|
1716
|
+
return customColorScheme
|
|
1717
|
+
? letterColorTable(customColorScheme)
|
|
1718
|
+
: (colorSchemes[colorSchemeName] ??
|
|
1719
|
+
colorSchemes[defaultColorSchemeName]!)
|
|
1720
|
+
},
|
|
1721
|
+
|
|
1722
|
+
/**
|
|
1723
|
+
* #getter
|
|
1724
|
+
* the scheme coloring each cell from its column's statistics, or
|
|
1725
|
+
* undefined while the cells take a fixed color per letter, from the
|
|
1726
|
+
* built-in table or from `customColorScheme`
|
|
1727
|
+
*/
|
|
1728
|
+
get dynamicColorSchemeName() {
|
|
1729
|
+
const { customColorScheme, colorSchemeName } = self
|
|
1730
|
+
return !customColorScheme && colorSchemeName.includes('dynamic')
|
|
1731
|
+
? colorSchemeName
|
|
1732
|
+
: undefined
|
|
1614
1733
|
},
|
|
1615
1734
|
|
|
1616
1735
|
/**
|
|
@@ -1633,7 +1752,7 @@ function stateModelFactory() {
|
|
|
1633
1752
|
return !!this.tree.noTree
|
|
1634
1753
|
},
|
|
1635
1754
|
get noDomains() {
|
|
1636
|
-
return self.
|
|
1755
|
+
return self.allAnnotations.length === 0
|
|
1637
1756
|
},
|
|
1638
1757
|
|
|
1639
1758
|
/**
|
|
@@ -1642,9 +1761,9 @@ function stateModelFactory() {
|
|
|
1642
1761
|
* from disk or pasted in becomes inline text, and DataModel drops an
|
|
1643
1762
|
* inline document past `maxInlineSnapshotBytes`.
|
|
1644
1763
|
*
|
|
1645
|
-
* The header lists these, and the standalone app drops
|
|
1764
|
+
* The header lists these, and the standalone app drops `#data=` from
|
|
1646
1765
|
* the address bar while the list is non-empty or the encoded link would
|
|
1647
|
-
* pass
|
|
1766
|
+
* pass its `maxLinkLength`, so a copied link never opens an empty viewer
|
|
1648
1767
|
* unannounced. A document fetched from a URL never appears here, since
|
|
1649
1768
|
* the snapshot keeps its filehandle.
|
|
1650
1769
|
*
|
|
@@ -1687,7 +1806,8 @@ function stateModelFactory() {
|
|
|
1687
1806
|
* `data.treeMetadata`, the name that travels in existing links, so the
|
|
1688
1807
|
* inline size limit and `treeMetadataFilehandle` cover it. labelWidthMap
|
|
1689
1808
|
* reads it on every layout, so a malformed user-supplied file returns {}
|
|
1690
|
-
* instead of throwing out of rendering
|
|
1809
|
+
* instead of throwing out of rendering, and every field is a string by
|
|
1810
|
+
* the time a scale sorts it.
|
|
1691
1811
|
*/
|
|
1692
1812
|
get rowData(): Record<string, Record<string, string> | undefined> {
|
|
1693
1813
|
const text = self.data.treeMetadata
|
|
@@ -1697,7 +1817,12 @@ function stateModelFactory() {
|
|
|
1697
1817
|
try {
|
|
1698
1818
|
const parsed: unknown = JSON.parse(text)
|
|
1699
1819
|
return typeof parsed === 'object' && parsed !== null
|
|
1700
|
-
? (
|
|
1820
|
+
? Object.fromEntries(
|
|
1821
|
+
Object.entries(parsed).map(([name, row]) => [
|
|
1822
|
+
name,
|
|
1823
|
+
stringFields(row),
|
|
1824
|
+
]),
|
|
1825
|
+
)
|
|
1701
1826
|
: {}
|
|
1702
1827
|
} catch (e) {
|
|
1703
1828
|
console.error('failed to parse rowData', e)
|
|
@@ -3101,7 +3226,7 @@ function stateModelFactory() {
|
|
|
3101
3226
|
// first occurrence wins; only name, description and segment start are
|
|
3102
3227
|
// read from it
|
|
3103
3228
|
const types = new Map<string, Annotation>()
|
|
3104
|
-
for (const annot of self.
|
|
3229
|
+
for (const annot of self.allAnnotations) {
|
|
3105
3230
|
if (!types.has(annot.accession)) {
|
|
3106
3231
|
types.set(annot.accession, annot)
|
|
3107
3232
|
}
|
|
@@ -3109,7 +3234,7 @@ function stateModelFactory() {
|
|
|
3109
3234
|
return types
|
|
3110
3235
|
},
|
|
3111
3236
|
get filteredAnnotations() {
|
|
3112
|
-
return self.
|
|
3237
|
+
return self.allAnnotations.filter(
|
|
3113
3238
|
r => !self.turnedOffFeatures.get(r.accession),
|
|
3114
3239
|
)
|
|
3115
3240
|
},
|
|
@@ -3531,7 +3656,7 @@ function stateModelFactory() {
|
|
|
3531
3656
|
* replace the row table, which the model keeps as the JSON string
|
|
3532
3657
|
* `data.treeMetadata` (see docs/layers.md)
|
|
3533
3658
|
*/
|
|
3534
|
-
setRowData(rowData:
|
|
3659
|
+
setRowData(rowData: RowDataInput) {
|
|
3535
3660
|
self.data.setTreeMetadata(JSON.stringify(rowData))
|
|
3536
3661
|
},
|
|
3537
3662
|
/**
|
|
@@ -3923,19 +4048,9 @@ function stateModelFactory() {
|
|
|
3923
4048
|
* on every mouse move.
|
|
3924
4049
|
*/
|
|
3925
4050
|
get highlightedColumnRuns() {
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
(a, b) => a - b,
|
|
3930
|
-
)) {
|
|
3931
|
-
const last = runs.at(-1)
|
|
3932
|
-
if (last && col === last.end + 1) {
|
|
3933
|
-
last.end = col
|
|
3934
|
-
} else {
|
|
3935
|
-
runs.push({ start: col, end: col })
|
|
3936
|
-
}
|
|
3937
|
-
}
|
|
3938
|
-
return runs
|
|
4051
|
+
return consecutiveRuns(
|
|
4052
|
+
[...(self.highlightedColumns ?? [])].sort((a, b) => a - b),
|
|
4053
|
+
).map(([start, end]) => ({ start, end }))
|
|
3939
4054
|
},
|
|
3940
4055
|
|
|
3941
4056
|
/**
|
|
@@ -3991,6 +4106,85 @@ function stateModelFactory() {
|
|
|
3991
4106
|
})
|
|
3992
4107
|
},
|
|
3993
4108
|
|
|
4109
|
+
/**
|
|
4110
|
+
* #getter
|
|
4111
|
+
* `selection` projected onto what is on screen, the way
|
|
4112
|
+
* `resolvedHighlights` projects a column span and a row set. A
|
|
4113
|
+
* selection whose columns are all hidden, or whose rows are all
|
|
4114
|
+
* collapsed away or unknown, resolves to undefined.
|
|
4115
|
+
*/
|
|
4116
|
+
get resolvedSelection(): ResolvedSelection | undefined {
|
|
4117
|
+
const { selection, rowNamesSet, numRows } = self
|
|
4118
|
+
const span =
|
|
4119
|
+
selection &&
|
|
4120
|
+
self.visibleSpan({ start: selection.start, end: selection.end })
|
|
4121
|
+
if (!selection || !span) {
|
|
4122
|
+
return undefined
|
|
4123
|
+
}
|
|
4124
|
+
const rowRuns = selection.rows
|
|
4125
|
+
? consecutiveRuns(
|
|
4126
|
+
selection.rows
|
|
4127
|
+
.map(name => rowNamesSet.get(name))
|
|
4128
|
+
.filter(notEmpty)
|
|
4129
|
+
.sort((a, b) => a - b),
|
|
4130
|
+
)
|
|
4131
|
+
: numRows > 0
|
|
4132
|
+
? [[0, numRows - 1] as [number, number]]
|
|
4133
|
+
: []
|
|
4134
|
+
return rowRuns.length > 0 ? { ...span, rowRuns } : undefined
|
|
4135
|
+
},
|
|
4136
|
+
|
|
4137
|
+
/**
|
|
4138
|
+
* #getter
|
|
4139
|
+
* the columns and rows the selection covers, for the header's readout.
|
|
4140
|
+
* Columns count the file's columns from `start` to `end`, hidden ones
|
|
4141
|
+
* included, as `selectionFasta` copies them.
|
|
4142
|
+
*/
|
|
4143
|
+
get selectionSize() {
|
|
4144
|
+
const resolved = this.resolvedSelection
|
|
4145
|
+
const span = this.selectionFileSpan
|
|
4146
|
+
return resolved && span
|
|
4147
|
+
? {
|
|
4148
|
+
columns: span.end - span.start + 1,
|
|
4149
|
+
rows: sum(resolved.rowRuns.map(([a, b]) => b - a + 1)),
|
|
4150
|
+
}
|
|
4151
|
+
: undefined
|
|
4152
|
+
},
|
|
4153
|
+
|
|
4154
|
+
/**
|
|
4155
|
+
* #getter
|
|
4156
|
+
* `selection`'s columns clamped to the file's
|
|
4157
|
+
*/
|
|
4158
|
+
get selectionFileSpan() {
|
|
4159
|
+
const { selection } = self
|
|
4160
|
+
const width = self.MSA?.getWidth() ?? 0
|
|
4161
|
+
const start = Math.max(1, Math.floor(selection?.start ?? 0))
|
|
4162
|
+
const end = Math.min(width, Math.ceil(selection?.end ?? 0))
|
|
4163
|
+
return selection && start <= end ? { start, end } : undefined
|
|
4164
|
+
},
|
|
4165
|
+
|
|
4166
|
+
/**
|
|
4167
|
+
* #getter
|
|
4168
|
+
* the selected block as FASTA: the selected rows on screen, top to
|
|
4169
|
+
* bottom, each with its letters across the file's columns from `start`
|
|
4170
|
+
* to `end`, gaps included
|
|
4171
|
+
*/
|
|
4172
|
+
get selectionFasta() {
|
|
4173
|
+
const { rowNames, rowMap } = self
|
|
4174
|
+
const resolved = this.resolvedSelection
|
|
4175
|
+
const span = this.selectionFileSpan
|
|
4176
|
+
if (!resolved || !span) {
|
|
4177
|
+
return ''
|
|
4178
|
+
}
|
|
4179
|
+
const rows = resolved.rowRuns
|
|
4180
|
+
.flatMap(([first, last]) => rowNames.slice(first, last + 1))
|
|
4181
|
+
.flatMap(name => {
|
|
4182
|
+
const seq = rowMap.get(name)
|
|
4183
|
+
return seq === undefined ? [] : [[name, seq] as const]
|
|
4184
|
+
})
|
|
4185
|
+
return blockFasta(rows, span.start, span.end)
|
|
4186
|
+
},
|
|
4187
|
+
|
|
3994
4188
|
/**
|
|
3995
4189
|
* #method
|
|
3996
4190
|
* per-column summary statistics: consensus residue and its identity
|
|
@@ -4021,23 +4215,25 @@ function stateModelFactory() {
|
|
|
4021
4215
|
/**
|
|
4022
4216
|
* #getter
|
|
4023
4217
|
* each encoding with its scale resolved against the values its field
|
|
4024
|
-
* takes: a feature channel reads them across
|
|
4218
|
+
* takes: a feature channel reads them across every feature loaded, every
|
|
4025
4219
|
* other channel across the row table. Resolved once per change of that
|
|
4026
4220
|
* table or the encodings, never per row per frame.
|
|
4027
4221
|
*/
|
|
4028
4222
|
get resolvedEncodings(): ResolvedEncoding[] {
|
|
4029
4223
|
const rows = Object.values(self.rowData)
|
|
4030
|
-
const features = self.filteredAnnotations
|
|
4031
4224
|
return self.encodings.map(encoding => ({
|
|
4032
4225
|
...encoding,
|
|
4033
|
-
...
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4226
|
+
...(featureChannels.has(encoding.channel)
|
|
4227
|
+
? resolveFeatureScale(
|
|
4228
|
+
encoding.field,
|
|
4229
|
+
encoding.scale,
|
|
4230
|
+
self.allAnnotations,
|
|
4231
|
+
self.filteredAnnotations,
|
|
4232
|
+
)
|
|
4233
|
+
: resolveScale(
|
|
4234
|
+
encoding.scale,
|
|
4235
|
+
rows.map(row => row?.[encoding.field]).filter(notEmpty),
|
|
4236
|
+
)),
|
|
4041
4237
|
}))
|
|
4042
4238
|
},
|
|
4043
4239
|
|
|
@@ -4055,7 +4251,12 @@ function stateModelFactory() {
|
|
|
4055
4251
|
if (panel.kind === 'features') {
|
|
4056
4252
|
const color = panel.encoding?.color
|
|
4057
4253
|
const encoding = color
|
|
4058
|
-
? resolveFeatureScale(
|
|
4254
|
+
? resolveFeatureScale(
|
|
4255
|
+
color.field,
|
|
4256
|
+
color.scale,
|
|
4257
|
+
self.allAnnotations,
|
|
4258
|
+
annotations,
|
|
4259
|
+
)
|
|
4059
4260
|
: featureFillEncoding
|
|
4060
4261
|
const label = panel.encoding?.label
|
|
4061
4262
|
return {
|
|
@@ -4386,6 +4587,63 @@ function stateModelFactory() {
|
|
|
4386
4587
|
)
|
|
4387
4588
|
})
|
|
4388
4589
|
},
|
|
4590
|
+
/**
|
|
4591
|
+
* #action
|
|
4592
|
+
* zoom and scroll so the selected columns fill the alignment's width,
|
|
4593
|
+
* and, where the selection names its rows, so those rows fill its height
|
|
4594
|
+
*/
|
|
4595
|
+
zoomToSelection() {
|
|
4596
|
+
const { selection, resolvedSelection } = self
|
|
4597
|
+
if (!selection || !resolvedSelection) {
|
|
4598
|
+
return
|
|
4599
|
+
}
|
|
4600
|
+
transaction(() => {
|
|
4601
|
+
this.zoomToRegion({ start: selection.start, end: selection.end })
|
|
4602
|
+
if (selection.rows && self.viewInitialized) {
|
|
4603
|
+
const { rowRuns } = resolvedSelection
|
|
4604
|
+
const first = rowRuns[0]![0]
|
|
4605
|
+
const last = rowRuns.at(-1)![1]
|
|
4606
|
+
self.rowHeight = clamp(
|
|
4607
|
+
self.msaAreaHeight / (last - first + 1),
|
|
4608
|
+
minRowHeight,
|
|
4609
|
+
maxCellSize,
|
|
4610
|
+
)
|
|
4611
|
+
self.scrollY = clamp(-first * self.rowHeight, self.maxScrollY, 0)
|
|
4612
|
+
}
|
|
4613
|
+
})
|
|
4614
|
+
},
|
|
4615
|
+
/**
|
|
4616
|
+
* #action
|
|
4617
|
+
* select the block between two cells given as visible column and row
|
|
4618
|
+
* indices, the coordinates a drag reports, clamped to the alignment. A
|
|
4619
|
+
* cell with no `row` selects the columns across every row, and so does a
|
|
4620
|
+
* block spanning every row on screen.
|
|
4621
|
+
*/
|
|
4622
|
+
selectBlock(
|
|
4623
|
+
anchor: { col: number; row?: number },
|
|
4624
|
+
head: { col: number; row?: number },
|
|
4625
|
+
) {
|
|
4626
|
+
const { numColumns, numRows, rowNames } = self
|
|
4627
|
+
if (numColumns === 0 || numRows === 0) {
|
|
4628
|
+
return
|
|
4629
|
+
}
|
|
4630
|
+
const ordered = (a: number, b: number, last: number) =>
|
|
4631
|
+
[clamp(a, 0, last), clamp(b, 0, last)].sort((x, y) => x - y) as [
|
|
4632
|
+
number,
|
|
4633
|
+
number,
|
|
4634
|
+
]
|
|
4635
|
+
const [startCol, endCol] = ordered(anchor.col, head.col, numColumns - 1)
|
|
4636
|
+
const [firstRow, lastRow] =
|
|
4637
|
+
anchor.row === undefined || head.row === undefined
|
|
4638
|
+
? [0, numRows - 1]
|
|
4639
|
+
: ordered(anchor.row, head.row, numRows - 1)
|
|
4640
|
+
const everyRow = firstRow === 0 && lastRow === numRows - 1
|
|
4641
|
+
self.setSelection({
|
|
4642
|
+
start: self.visibleColToGlobalCol(startCol) + 1,
|
|
4643
|
+
end: self.visibleColToGlobalCol(endCol) + 1,
|
|
4644
|
+
...(everyRow ? {} : { rows: rowNames.slice(firstRow, lastRow + 1) }),
|
|
4645
|
+
})
|
|
4646
|
+
},
|
|
4389
4647
|
/**
|
|
4390
4648
|
* #action
|
|
4391
4649
|
*/
|
|
@@ -4667,7 +4925,7 @@ function stateModelFactory() {
|
|
|
4667
4925
|
addDisposer(
|
|
4668
4926
|
self,
|
|
4669
4927
|
autorun(() => {
|
|
4670
|
-
if (self.
|
|
4928
|
+
if (self.dynamicColorSchemeName) {
|
|
4671
4929
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
4672
4930
|
self.colStats
|
|
4673
4931
|
}
|
|
@@ -4736,7 +4994,7 @@ function stateModelFactory() {
|
|
|
4736
4994
|
// stripDefault handles per-property defaults; this drops inline documents
|
|
4737
4995
|
// whose filehandle can refetch them
|
|
4738
4996
|
...rest,
|
|
4739
|
-
...smallColumnTracks(columnTracks),
|
|
4997
|
+
...smallColumnTracks(columnTracks, maxInlineSnapshotBytes),
|
|
4740
4998
|
data: {
|
|
4741
4999
|
...(rest.treeFilehandle ? {} : { tree: data.tree }),
|
|
4742
5000
|
...(rest.msaFilehandle ? {} : { msa: data.msa }),
|