react-msaview 8.0.0 → 8.2.0
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 +99 -99
- package/bundle/index.js.map +3 -3
- package/dist/calculateBlocks.js +5 -3
- package/dist/calculateBlocks.js.map +1 -1
- package/dist/components/Loading.js +2 -8
- package/dist/components/Loading.js.map +1 -1
- package/dist/components/SequenceTextArea.js +0 -1
- package/dist/components/SequenceTextArea.js.map +1 -1
- package/dist/components/Track.js +2 -2
- package/dist/components/Track.js.map +1 -1
- package/dist/components/dialogs/AboutDialog.js +6 -6
- package/dist/components/dialogs/AboutDialog.js.map +1 -1
- package/dist/components/dialogs/AnnotationFileDialog.js +5 -2
- package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -1
- package/dist/components/dialogs/ExportSVGDialog.js +1 -1
- package/dist/components/dialogs/FeatureDialog.js +8 -3
- package/dist/components/dialogs/FeatureDialog.js.map +1 -1
- package/dist/components/header/Header.js +1 -1
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/LoadWarnings.js +3 -19
- package/dist/components/header/LoadWarnings.js.map +1 -1
- package/dist/components/header/settingsMenuItems.js +3 -3
- package/dist/components/header/settingsMenuItems.js.map +1 -1
- package/dist/components/import/ImportForm.js +1 -1
- package/dist/components/import/ImportForm.js.map +1 -1
- package/dist/components/msa/MSACanvas.js +1 -3
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/drawFeatureSpans.d.ts +7 -1
- package/dist/components/msa/drawFeatureSpans.js +58 -23
- package/dist/components/msa/drawFeatureSpans.js.map +1 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js +7 -8
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSABlock.js +8 -9
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/tracks/drawTracks.js +8 -5
- package/dist/components/tracks/drawTracks.js.map +1 -1
- package/dist/components/tree/TreeBranchMenu.js +1 -1
- package/dist/components/tree/TreeBranchMenu.js.map +1 -1
- package/dist/components/tree/TreeCanvas.js +1 -3
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +2 -2
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/TreeNodeMenu.js +1 -1
- package/dist/components/tree/TreeNodeMenu.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +2 -1
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +4 -0
- package/dist/constants.js.map +1 -1
- package/dist/expandSpec.d.ts +46 -0
- package/dist/expandSpec.js +127 -0
- package/dist/expandSpec.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/model.d.ts +52 -33
- package/dist/model.js +232 -142
- package/dist/model.js.map +1 -1
- package/dist/svgTestUtil.d.ts +28 -24
- package/dist/types.d.ts +2 -0
- package/dist/useWheelScroll.js +0 -4
- package/dist/useWheelScroll.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/calculateBlocks.ts +5 -3
- package/src/components/Loading.tsx +2 -30
- package/src/components/SequenceTextArea.tsx +0 -1
- package/src/components/Track.tsx +2 -1
- package/src/components/dialogs/AboutDialog.tsx +35 -6
- package/src/components/dialogs/AnnotationFileDialog.tsx +10 -1
- package/src/components/dialogs/ExportSVGDialog.tsx +1 -1
- package/src/components/dialogs/FeatureDialog.tsx +11 -3
- package/src/components/header/Header.tsx +1 -1
- package/src/components/header/LoadWarnings.tsx +8 -21
- package/src/components/header/settingsMenuItems.ts +3 -3
- package/src/components/import/ImportForm.tsx +3 -5
- package/src/components/msa/MSACanvas.tsx +0 -3
- package/src/components/msa/drawFeatureSpans.ts +80 -25
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +7 -9
- package/src/components/msa/renderMSABlock.ts +9 -8
- package/src/components/tracks/drawTracks.ts +8 -16
- package/src/components/tree/TreeBranchMenu.tsx +1 -2
- package/src/components/tree/TreeCanvas.tsx +0 -3
- package/src/components/tree/TreeCanvasBlock.tsx +2 -2
- package/src/components/tree/TreeNodeMenu.tsx +1 -2
- package/src/components/tree/renderTreeCanvas.ts +2 -1
- package/src/constants.ts +5 -0
- package/src/expandSpec.ts +196 -0
- package/src/index.ts +8 -0
- package/src/model.ts +289 -162
- package/src/types.ts +2 -0
- package/src/useWheelScroll.ts +0 -4
- package/src/version.ts +1 -1
package/src/model.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
isAlive,
|
|
10
10
|
types,
|
|
11
11
|
} from '@jbrowse/mobx-state-tree'
|
|
12
|
-
import { autorun, transaction } from 'mobx'
|
|
12
|
+
import { autorun, computed, transaction } from 'mobx'
|
|
13
13
|
import {
|
|
14
14
|
generateNodeIds,
|
|
15
15
|
gffToAnnotations,
|
|
@@ -111,7 +111,6 @@ import {
|
|
|
111
111
|
len,
|
|
112
112
|
outlineColor,
|
|
113
113
|
skipBlanks,
|
|
114
|
-
transform,
|
|
115
114
|
withAlpha,
|
|
116
115
|
} from './util.ts'
|
|
117
116
|
import { saveAs } from './vendor/fileSaver.ts'
|
|
@@ -156,6 +155,7 @@ import type {
|
|
|
156
155
|
} from './types.ts'
|
|
157
156
|
import type { FileLocation as FileLocationType } from '@jbrowse/core/util/types'
|
|
158
157
|
import type { Instance } from '@jbrowse/mobx-state-tree'
|
|
158
|
+
import type { IComputedValue } from 'mobx'
|
|
159
159
|
import type { InterProScanResults } from 'msa-parsers'
|
|
160
160
|
|
|
161
161
|
function parseTreeText(text: string) {
|
|
@@ -257,6 +257,56 @@ function featureLabelMap(annotations: Annotation[], field: string) {
|
|
|
257
257
|
return labels
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
/** what `packDomainLanes` needs to lay a row out, before it has its lane */
|
|
261
|
+
type Unlaned = Omit<RowPanelSpan, 'lane' | 'laneCount'> & {
|
|
262
|
+
startCol: number
|
|
263
|
+
endCol: number
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Lanes for `position: "strandpile"`, gggenomes' `position_strandpile`: the
|
|
268
|
+
* forward features stack above the line and the reverse below it, each strand
|
|
269
|
+
* packing on its own. The deepest row on each side sets the grid every row lays
|
|
270
|
+
* out on, so the line between the strands sits at one height down the panel and
|
|
271
|
+
* a reader can scan it. A feature with no strand piles with the forward ones.
|
|
272
|
+
*/
|
|
273
|
+
function strandpileLanes(
|
|
274
|
+
rows: [string, Unlaned[]][],
|
|
275
|
+
): Map<string, RowPanelSpan[]> {
|
|
276
|
+
const piled = rows.map(([name, bands]) => {
|
|
277
|
+
const reverse = packDomainLanes(
|
|
278
|
+
bands.filter(b => b.annotation.strand === -1),
|
|
279
|
+
)
|
|
280
|
+
const forward = packDomainLanes(
|
|
281
|
+
bands.filter(b => b.annotation.strand !== -1),
|
|
282
|
+
)
|
|
283
|
+
return { name, forward, reverse }
|
|
284
|
+
})
|
|
285
|
+
const up = Math.max(0, ...piled.map(p => p.forward[0]?.laneCount ?? 0))
|
|
286
|
+
const down = Math.max(0, ...piled.map(p => p.reverse[0]?.laneCount ?? 0))
|
|
287
|
+
const laneCount = up + down
|
|
288
|
+
return new Map(
|
|
289
|
+
piled.map(({ name, forward, reverse }) => [
|
|
290
|
+
name,
|
|
291
|
+
[
|
|
292
|
+
// forward level 0 is the lane just above the line, stacking upward
|
|
293
|
+
...forward.map(b => ({ ...b, lane: up - 1 - b.lane, laneCount })),
|
|
294
|
+
...reverse.map(b => ({ ...b, lane: up + b.lane, laneCount })),
|
|
295
|
+
],
|
|
296
|
+
]),
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** one row's spans laid out by the panel's `position` */
|
|
301
|
+
function panelLanes(
|
|
302
|
+
panel: RowFeaturesSpec,
|
|
303
|
+
rows: [string, Unlaned[]][],
|
|
304
|
+
): Map<string, RowPanelSpan[]> {
|
|
305
|
+
return panel.position === 'strandpile'
|
|
306
|
+
? strandpileLanes(rows)
|
|
307
|
+
: new Map(rows.map(([name, bands]) => [name, packDomainLanes(bands)]))
|
|
308
|
+
}
|
|
309
|
+
|
|
260
310
|
/**
|
|
261
311
|
* The spans a `features` panel draws, keyed by row name and measured in the
|
|
262
312
|
* panel's own pixels. `column` takes the bands the overlay draws, at the
|
|
@@ -268,7 +318,6 @@ function featureLabelMap(annotations: Annotation[], field: string) {
|
|
|
268
318
|
function featurePanelSpans({
|
|
269
319
|
panel,
|
|
270
320
|
width,
|
|
271
|
-
rowHeight,
|
|
272
321
|
colWidth,
|
|
273
322
|
domainBands,
|
|
274
323
|
annotationsByRow,
|
|
@@ -276,14 +325,14 @@ function featurePanelSpans({
|
|
|
276
325
|
}: {
|
|
277
326
|
panel: RowFeaturesSpec
|
|
278
327
|
width: number
|
|
279
|
-
rowHeight: number
|
|
280
328
|
colWidth: number
|
|
281
329
|
domainBands: Map<string, DomainBand[]>
|
|
282
330
|
annotationsByRow: Record<string, Annotation[]>
|
|
283
331
|
shifts: Map<string, number> | undefined
|
|
284
332
|
}): Map<string, RowPanelSpan[]> {
|
|
285
333
|
if (panel.x === 'column') {
|
|
286
|
-
return
|
|
334
|
+
return panelLanes(
|
|
335
|
+
panel,
|
|
287
336
|
[...domainBands].map(([name, bands]) => [
|
|
288
337
|
name,
|
|
289
338
|
bands.map(band => ({
|
|
@@ -315,26 +364,25 @@ function featurePanelSpans({
|
|
|
315
364
|
max = Math.max(max, end)
|
|
316
365
|
}
|
|
317
366
|
}
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
const drawable = Math.max(1, width -
|
|
367
|
+
// a gene arrow ends where its feature does, so the extent maps onto the whole
|
|
368
|
+
// panel less the pixel the rightmost stroke needs
|
|
369
|
+
const drawable = Math.max(1, width - 1)
|
|
321
370
|
const scale = max > min ? drawable / (max - min) : 0
|
|
322
371
|
// a lane opens where a span covers more than a tenth of the one before it,
|
|
323
372
|
// which keeps the genes of an operon on one lane: adjacent genes commonly
|
|
324
373
|
// share a few bases, and a stop codon overlapping the next start reads as a
|
|
325
374
|
// second lane over the whole row
|
|
326
|
-
return
|
|
375
|
+
return panelLanes(
|
|
376
|
+
panel,
|
|
327
377
|
shifted.map(([name, features]) => [
|
|
328
378
|
name,
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
})),
|
|
337
|
-
),
|
|
379
|
+
features.map(({ annotation, start, end }) => ({
|
|
380
|
+
annotation,
|
|
381
|
+
xStart: (start - min) * scale,
|
|
382
|
+
xEnd: (end - min) * scale,
|
|
383
|
+
startCol: start,
|
|
384
|
+
endCol: end - (end - start) * laneOverlap,
|
|
385
|
+
})),
|
|
338
386
|
]),
|
|
339
387
|
)
|
|
340
388
|
}
|
|
@@ -798,11 +846,7 @@ function stateModelFactory() {
|
|
|
798
846
|
* data from the loaded tree/msa/treeMetadata, generally loaded by
|
|
799
847
|
* autorun
|
|
800
848
|
*/
|
|
801
|
-
data: types.optional(DataModelF(), {
|
|
802
|
-
tree: '',
|
|
803
|
-
msa: '',
|
|
804
|
-
treeMetadata: '',
|
|
805
|
-
}),
|
|
849
|
+
data: types.optional(DataModelF(), {}),
|
|
806
850
|
|
|
807
851
|
/**
|
|
808
852
|
* #property
|
|
@@ -833,6 +877,14 @@ function stateModelFactory() {
|
|
|
833
877
|
* Persists in the snapshot and the URL.
|
|
834
878
|
*/
|
|
835
879
|
highlights: stripDefault(types.array(types.frozen<Highlight>()), []),
|
|
880
|
+
/**
|
|
881
|
+
* #property
|
|
882
|
+
* where the view opens, in `highlights` coordinates: `{row, start,
|
|
883
|
+
* end}` zooms onto residues of that row, `{start, end}` onto alignment
|
|
884
|
+
* columns. Applied once the alignment and any tree file have loaded,
|
|
885
|
+
* then cleared, so a reloaded session keeps the reader's own scroll.
|
|
886
|
+
*/
|
|
887
|
+
region: types.frozen<Region | undefined>(),
|
|
836
888
|
/**
|
|
837
889
|
* #property
|
|
838
890
|
* clades of the tree with a mark drawn over them. `mrca` names tips
|
|
@@ -1064,10 +1116,12 @@ function stateModelFactory() {
|
|
|
1064
1116
|
/**
|
|
1065
1117
|
* #action
|
|
1066
1118
|
* record a non-fatal load problem: a layer that failed to load, a file
|
|
1067
|
-
* that failed to parse
|
|
1119
|
+
* that failed to parse. A message already on the list is not added again
|
|
1068
1120
|
*/
|
|
1069
1121
|
addWarning(warning: string) {
|
|
1070
|
-
self.warnings
|
|
1122
|
+
if (!self.warnings.includes(warning)) {
|
|
1123
|
+
self.warnings = [...self.warnings, warning]
|
|
1124
|
+
}
|
|
1071
1125
|
},
|
|
1072
1126
|
|
|
1073
1127
|
/**
|
|
@@ -1110,6 +1164,12 @@ function stateModelFactory() {
|
|
|
1110
1164
|
setHighlightedColumns(columns?: number[]) {
|
|
1111
1165
|
self.highlightedColumns = columns
|
|
1112
1166
|
},
|
|
1167
|
+
/**
|
|
1168
|
+
* #action
|
|
1169
|
+
*/
|
|
1170
|
+
setRegion(region?: Region) {
|
|
1171
|
+
self.region = region
|
|
1172
|
+
},
|
|
1113
1173
|
/**
|
|
1114
1174
|
* #action
|
|
1115
1175
|
*/
|
|
@@ -1776,7 +1836,7 @@ function stateModelFactory() {
|
|
|
1776
1836
|
*/
|
|
1777
1837
|
get insertionPositions() {
|
|
1778
1838
|
const { blanks, rows } = this
|
|
1779
|
-
if (blanks.length === 0
|
|
1839
|
+
if (blanks.length === 0) {
|
|
1780
1840
|
return new Map<string, { pos: number; letters: string }[]>()
|
|
1781
1841
|
}
|
|
1782
1842
|
const result = new Map<string, { pos: number; letters: string }[]>()
|
|
@@ -2039,7 +2099,7 @@ function stateModelFactory() {
|
|
|
2039
2099
|
* cladogram mode. The tree's scale bar uses it.
|
|
2040
2100
|
*/
|
|
2041
2101
|
get pxPerBranchLength() {
|
|
2042
|
-
const max =
|
|
2102
|
+
const max = this.rootToTipLength
|
|
2043
2103
|
return this.showBranchLenEffective && max ? self.treeWidth / max : 0
|
|
2044
2104
|
},
|
|
2045
2105
|
|
|
@@ -2094,6 +2154,140 @@ function stateModelFactory() {
|
|
|
2094
2154
|
return self.drawLabels && self.rowHeight >= minLetterRowHeight
|
|
2095
2155
|
},
|
|
2096
2156
|
}))
|
|
2157
|
+
.views(self => {
|
|
2158
|
+
// a spec is a frozen value, so it keys its computed until setColumnTracks
|
|
2159
|
+
// replaces it
|
|
2160
|
+
const columnTrackModels = new WeakMap<
|
|
2161
|
+
ColumnTrackSpec,
|
|
2162
|
+
IComputedValue<BasicTrack>
|
|
2163
|
+
>()
|
|
2164
|
+
return {
|
|
2165
|
+
/**
|
|
2166
|
+
* #getter
|
|
2167
|
+
* a data track's values or string, projected from its row's residues
|
|
2168
|
+
* onto alignment columns when it names a row
|
|
2169
|
+
*/
|
|
2170
|
+
get columnTrackContent() {
|
|
2171
|
+
const { MSA, blanks, hideGapsEffective } = self
|
|
2172
|
+
const width = MSA?.getWidth() ?? 0
|
|
2173
|
+
const project = <T>(track: ColumnTrackSpec, items: T[], fill: T) => {
|
|
2174
|
+
if (!track.row) {
|
|
2175
|
+
return items
|
|
2176
|
+
}
|
|
2177
|
+
const out = Array.from({ length: width }, () => fill)
|
|
2178
|
+
const index = self.seqPosIndex(track.row)
|
|
2179
|
+
items.forEach((item, seqPos) => {
|
|
2180
|
+
const col = index?.[seqPos]
|
|
2181
|
+
if (col !== undefined) {
|
|
2182
|
+
out[col] = item
|
|
2183
|
+
}
|
|
2184
|
+
})
|
|
2185
|
+
return out
|
|
2186
|
+
}
|
|
2187
|
+
const skip = <T>(items: T[]) =>
|
|
2188
|
+
hideGapsEffective ? dropBlanks(blanks, items) : items
|
|
2189
|
+
// an arc endpoint maps row residue -> column -> visible column.
|
|
2190
|
+
// visibleColsBefore, not globalColToVisibleCol, so an endpoint in a
|
|
2191
|
+
// hidden column moves to the neighboring visible one and the arc stays
|
|
2192
|
+
const resolve = (track: ColumnTrackSpec, pos: number) => {
|
|
2193
|
+
const col = track.row
|
|
2194
|
+
? self.seqPosIndex(track.row)?.[pos - 1]
|
|
2195
|
+
: pos - 1
|
|
2196
|
+
if (col === undefined || col < 0 || col >= width) {
|
|
2197
|
+
return undefined
|
|
2198
|
+
}
|
|
2199
|
+
return hideGapsEffective ? visibleColsBefore(blanks, col) : col
|
|
2200
|
+
}
|
|
2201
|
+
return new Map<
|
|
2202
|
+
string,
|
|
2203
|
+
{ values?: number[]; data?: string; arcs?: Arc[] }
|
|
2204
|
+
>(
|
|
2205
|
+
self.columnTracks.map(track => {
|
|
2206
|
+
if (track.kind === 'arc') {
|
|
2207
|
+
const arcs = (track.arcs ?? [])
|
|
2208
|
+
.map(arc => {
|
|
2209
|
+
const start = resolve(track, Math.min(arc.start, arc.end))
|
|
2210
|
+
const end = resolve(track, Math.max(arc.start, arc.end))
|
|
2211
|
+
return start !== undefined &&
|
|
2212
|
+
end !== undefined &&
|
|
2213
|
+
start < end
|
|
2214
|
+
? { start, end, color: arc.color }
|
|
2215
|
+
: undefined
|
|
2216
|
+
})
|
|
2217
|
+
.filter(notEmpty)
|
|
2218
|
+
return [track.id, { arcs }] as const
|
|
2219
|
+
}
|
|
2220
|
+
if (track.kind === 'bar') {
|
|
2221
|
+
const max = track.max ?? 1
|
|
2222
|
+
const values = skip(project(track, track.values ?? [], 0)).map(
|
|
2223
|
+
v => Math.min(1, Math.max(0, v / max)),
|
|
2224
|
+
)
|
|
2225
|
+
return [track.id, { values }] as const
|
|
2226
|
+
}
|
|
2227
|
+
const data = skip(
|
|
2228
|
+
project(track, (track.data ?? '').split(''), ' '),
|
|
2229
|
+
)
|
|
2230
|
+
return [track.id, { data: data.join('') }] as const
|
|
2231
|
+
}),
|
|
2232
|
+
)
|
|
2233
|
+
},
|
|
2234
|
+
/**
|
|
2235
|
+
* #method
|
|
2236
|
+
* the height a track draws at: what the user dragged its divider to,
|
|
2237
|
+
* then the height its snapshot asked for, then its kind's default. Only
|
|
2238
|
+
* a text track falls through to rowHeight
|
|
2239
|
+
*/
|
|
2240
|
+
trackHeight(kind: TrackKind, heightKey: string = kind, given?: number) {
|
|
2241
|
+
return (
|
|
2242
|
+
self.trackHeights.get(heightKey) ??
|
|
2243
|
+
given ??
|
|
2244
|
+
defaultTrackHeights[kind] ??
|
|
2245
|
+
self.rowHeight
|
|
2246
|
+
)
|
|
2247
|
+
},
|
|
2248
|
+
/**
|
|
2249
|
+
* #method
|
|
2250
|
+
* the track a column track spec draws as, computed once per spec. A
|
|
2251
|
+
* text track's height falls through to rowHeight, and its own computed
|
|
2252
|
+
* confines the vertical zoom to it, so a sibling keeps its object and
|
|
2253
|
+
* its canvas skips the redraw
|
|
2254
|
+
*/
|
|
2255
|
+
columnTrackModel(track: ColumnTrackSpec): BasicTrack {
|
|
2256
|
+
let computedModel = columnTrackModels.get(track)
|
|
2257
|
+
if (!computedModel) {
|
|
2258
|
+
computedModel = computed(() => {
|
|
2259
|
+
const heightKey = resizableKinds.has(track.kind)
|
|
2260
|
+
? ownHeightKey(track.id)
|
|
2261
|
+
: undefined
|
|
2262
|
+
const content = this.columnTrackContent.get(track.id)
|
|
2263
|
+
return {
|
|
2264
|
+
model: {
|
|
2265
|
+
id: track.id,
|
|
2266
|
+
name: track.name,
|
|
2267
|
+
kind: track.kind,
|
|
2268
|
+
heightKey,
|
|
2269
|
+
height: this.trackHeight(track.kind, heightKey, track.height),
|
|
2270
|
+
barColor: track.color,
|
|
2271
|
+
arcColor: track.color,
|
|
2272
|
+
customColorScheme: track.colors,
|
|
2273
|
+
data: content?.data,
|
|
2274
|
+
arcs: content?.arcs,
|
|
2275
|
+
},
|
|
2276
|
+
ReactComponent: TrackBlocks,
|
|
2277
|
+
}
|
|
2278
|
+
})
|
|
2279
|
+
columnTrackModels.set(track, computedModel)
|
|
2280
|
+
}
|
|
2281
|
+
return computedModel.get()
|
|
2282
|
+
},
|
|
2283
|
+
/**
|
|
2284
|
+
* #getter
|
|
2285
|
+
*/
|
|
2286
|
+
get columnTrackModels(): BasicTrack[] {
|
|
2287
|
+
return self.columnTracks.map(track => this.columnTrackModel(track))
|
|
2288
|
+
},
|
|
2289
|
+
}
|
|
2290
|
+
})
|
|
2097
2291
|
.views(self => ({
|
|
2098
2292
|
/**
|
|
2099
2293
|
* #getter
|
|
@@ -2199,111 +2393,6 @@ function stateModelFactory() {
|
|
|
2199
2393
|
}))
|
|
2200
2394
|
},
|
|
2201
2395
|
|
|
2202
|
-
/**
|
|
2203
|
-
* #getter
|
|
2204
|
-
* a data track's values or string, projected from its row's residues
|
|
2205
|
-
* onto alignment columns when it names a row
|
|
2206
|
-
*/
|
|
2207
|
-
get columnTrackContent() {
|
|
2208
|
-
const { MSA, blanks, hideGapsEffective } = self
|
|
2209
|
-
const width = MSA?.getWidth() ?? 0
|
|
2210
|
-
const project = <T>(track: ColumnTrackSpec, items: T[], fill: T) => {
|
|
2211
|
-
if (!track.row) {
|
|
2212
|
-
return items
|
|
2213
|
-
}
|
|
2214
|
-
const out = Array.from({ length: width }, () => fill)
|
|
2215
|
-
const index = self.seqPosIndex(track.row)
|
|
2216
|
-
items.forEach((item, seqPos) => {
|
|
2217
|
-
const col = index?.[seqPos]
|
|
2218
|
-
if (col !== undefined) {
|
|
2219
|
-
out[col] = item
|
|
2220
|
-
}
|
|
2221
|
-
})
|
|
2222
|
-
return out
|
|
2223
|
-
}
|
|
2224
|
-
const skip = <T>(items: T[]) =>
|
|
2225
|
-
hideGapsEffective ? dropBlanks(blanks, items) : items
|
|
2226
|
-
// an arc endpoint maps row residue -> column -> visible column.
|
|
2227
|
-
// visibleColsBefore, not globalColToVisibleCol, so an endpoint in a
|
|
2228
|
-
// hidden column moves to the neighboring visible one and the arc stays
|
|
2229
|
-
const resolve = (track: ColumnTrackSpec, pos: number) => {
|
|
2230
|
-
const col = track.row
|
|
2231
|
-
? self.seqPosIndex(track.row)?.[pos - 1]
|
|
2232
|
-
: pos - 1
|
|
2233
|
-
if (col === undefined || col < 0 || col >= width) {
|
|
2234
|
-
return undefined
|
|
2235
|
-
}
|
|
2236
|
-
return hideGapsEffective ? visibleColsBefore(blanks, col) : col
|
|
2237
|
-
}
|
|
2238
|
-
return new Map<
|
|
2239
|
-
string,
|
|
2240
|
-
{ values?: number[]; data?: string; arcs?: Arc[] }
|
|
2241
|
-
>(
|
|
2242
|
-
self.columnTracks.map(track => {
|
|
2243
|
-
if (track.kind === 'arc') {
|
|
2244
|
-
const arcs = (track.arcs ?? [])
|
|
2245
|
-
.map(arc => {
|
|
2246
|
-
const start = resolve(track, Math.min(arc.start, arc.end))
|
|
2247
|
-
const end = resolve(track, Math.max(arc.start, arc.end))
|
|
2248
|
-
return start !== undefined && end !== undefined && start < end
|
|
2249
|
-
? { start, end, color: arc.color }
|
|
2250
|
-
: undefined
|
|
2251
|
-
})
|
|
2252
|
-
.filter(notEmpty)
|
|
2253
|
-
return [track.id, { arcs }] as const
|
|
2254
|
-
}
|
|
2255
|
-
if (track.kind === 'bar') {
|
|
2256
|
-
const max = track.max ?? 1
|
|
2257
|
-
const values = skip(project(track, track.values ?? [], 0)).map(
|
|
2258
|
-
v => Math.min(1, Math.max(0, v / max)),
|
|
2259
|
-
)
|
|
2260
|
-
return [track.id, { values }] as const
|
|
2261
|
-
}
|
|
2262
|
-
const data = skip(project(track, (track.data ?? '').split(''), ' '))
|
|
2263
|
-
return [track.id, { data: data.join('') }] as const
|
|
2264
|
-
}),
|
|
2265
|
-
)
|
|
2266
|
-
},
|
|
2267
|
-
/**
|
|
2268
|
-
* #method
|
|
2269
|
-
* the height a track draws at: what the user dragged its divider to,
|
|
2270
|
-
* then the height its snapshot asked for, then its kind's default. Only
|
|
2271
|
-
* a text track falls through to rowHeight, and `??` short-circuits
|
|
2272
|
-
* before reading it, so vertical zoom does not rebuild the other tracks
|
|
2273
|
-
*/
|
|
2274
|
-
trackHeight(kind: TrackKind, heightKey = kind as string, given?: number) {
|
|
2275
|
-
return (
|
|
2276
|
-
self.trackHeights.get(heightKey) ??
|
|
2277
|
-
given ??
|
|
2278
|
-
defaultTrackHeights[kind] ??
|
|
2279
|
-
self.rowHeight
|
|
2280
|
-
)
|
|
2281
|
-
},
|
|
2282
|
-
/**
|
|
2283
|
-
* #getter
|
|
2284
|
-
*/
|
|
2285
|
-
get columnTrackModels(): BasicTrack[] {
|
|
2286
|
-
return self.columnTracks.map(track => {
|
|
2287
|
-
const heightKey = resizableKinds.has(track.kind)
|
|
2288
|
-
? ownHeightKey(track.id)
|
|
2289
|
-
: undefined
|
|
2290
|
-
return {
|
|
2291
|
-
model: {
|
|
2292
|
-
id: track.id,
|
|
2293
|
-
name: track.name,
|
|
2294
|
-
kind: track.kind,
|
|
2295
|
-
heightKey,
|
|
2296
|
-
height: this.trackHeight(track.kind, heightKey, track.height),
|
|
2297
|
-
barColor: track.color,
|
|
2298
|
-
arcColor: track.color,
|
|
2299
|
-
customColorScheme: track.colors,
|
|
2300
|
-
data: this.columnTrackContent.get(track.id)?.data,
|
|
2301
|
-
arcs: this.columnTrackContent.get(track.id)?.arcs,
|
|
2302
|
-
},
|
|
2303
|
-
ReactComponent: TrackBlocks,
|
|
2304
|
-
}
|
|
2305
|
-
})
|
|
2306
|
-
},
|
|
2307
2396
|
/**
|
|
2308
2397
|
* #getter
|
|
2309
2398
|
* the consensus secondary structure as a track, when there is one. A
|
|
@@ -2320,7 +2409,7 @@ function stateModelFactory() {
|
|
|
2320
2409
|
name: 'Base pairs',
|
|
2321
2410
|
kind: 'arc' as const,
|
|
2322
2411
|
heightKey: 'arc',
|
|
2323
|
-
height:
|
|
2412
|
+
height: self.trackHeight('arc'),
|
|
2324
2413
|
arcs,
|
|
2325
2414
|
},
|
|
2326
2415
|
ReactComponent: TrackBlocks,
|
|
@@ -2369,7 +2458,7 @@ function stateModelFactory() {
|
|
|
2369
2458
|
model: {
|
|
2370
2459
|
...model,
|
|
2371
2460
|
heightKey: resizableKinds.has(model.kind) ? model.kind : undefined,
|
|
2372
|
-
height:
|
|
2461
|
+
height: self.trackHeight(model.kind),
|
|
2373
2462
|
},
|
|
2374
2463
|
ReactComponent: TrackBlocks,
|
|
2375
2464
|
}))
|
|
@@ -2379,7 +2468,7 @@ function stateModelFactory() {
|
|
|
2379
2468
|
return [
|
|
2380
2469
|
...this.adapterTrackModels,
|
|
2381
2470
|
...this.basePairTrackModels,
|
|
2382
|
-
...
|
|
2471
|
+
...self.columnTrackModels,
|
|
2383
2472
|
// every computed track reads the alignment's columns, and a tree, a
|
|
2384
2473
|
// GFF and a features panel draw a figure with none
|
|
2385
2474
|
...(self.numColumns > 0 ? this.computedTrackModels : []),
|
|
@@ -3275,13 +3364,6 @@ function stateModelFactory() {
|
|
|
3275
3364
|
)
|
|
3276
3365
|
return { ...segments, ...categorical }
|
|
3277
3366
|
},
|
|
3278
|
-
get strokePalette() {
|
|
3279
|
-
return transform(this.fillPalette, ([key, val]) => [
|
|
3280
|
-
key,
|
|
3281
|
-
outlineColor(val),
|
|
3282
|
-
])
|
|
3283
|
-
},
|
|
3284
|
-
|
|
3285
3367
|
/**
|
|
3286
3368
|
* #getter
|
|
3287
3369
|
* the encoding coloring the overlay's spans, undefined when none does,
|
|
@@ -3553,6 +3635,26 @@ function stateModelFactory() {
|
|
|
3553
3635
|
return runs
|
|
3554
3636
|
},
|
|
3555
3637
|
|
|
3638
|
+
/**
|
|
3639
|
+
* #method
|
|
3640
|
+
* a highlight label with `{residue}` and `{position}` filled in from the
|
|
3641
|
+
* row's letter at `start`, which is how the `175` shorthand draws "R175"
|
|
3642
|
+
*/
|
|
3643
|
+
fillHighlightLabel(label: string, row?: string, start?: number) {
|
|
3644
|
+
if (!label.includes('{')) {
|
|
3645
|
+
return label
|
|
3646
|
+
}
|
|
3647
|
+
const col =
|
|
3648
|
+
row && start !== undefined
|
|
3649
|
+
? self.seqPosIndex(row)?.[start - 1]
|
|
3650
|
+
: undefined
|
|
3651
|
+
const residue =
|
|
3652
|
+
row && col !== undefined ? (self.MSA?.getRow(row)[col] ?? '') : ''
|
|
3653
|
+
return label
|
|
3654
|
+
.replaceAll('{residue}', residue)
|
|
3655
|
+
.replaceAll('{position}', start === undefined ? '' : String(start))
|
|
3656
|
+
},
|
|
3657
|
+
|
|
3556
3658
|
/**
|
|
3557
3659
|
* #getter
|
|
3558
3660
|
* `highlights` projected onto what is on screen: residue spans go
|
|
@@ -3568,7 +3670,10 @@ function stateModelFactory() {
|
|
|
3568
3670
|
...Object.values(transientHighlights).flat(),
|
|
3569
3671
|
]
|
|
3570
3672
|
return all.flatMap(({ row, rows, start, end, label, color }) => {
|
|
3571
|
-
const base = {
|
|
3673
|
+
const base = {
|
|
3674
|
+
label: label && this.fillHighlightLabel(label, row, start),
|
|
3675
|
+
color,
|
|
3676
|
+
}
|
|
3572
3677
|
if (rows) {
|
|
3573
3678
|
const rowIndices = rows
|
|
3574
3679
|
.map(name => rowNamesSet.get(name))
|
|
@@ -3600,23 +3705,6 @@ function stateModelFactory() {
|
|
|
3600
3705
|
})
|
|
3601
3706
|
},
|
|
3602
3707
|
|
|
3603
|
-
/**
|
|
3604
|
-
* #getter
|
|
3605
|
-
* `columnStatsAt` for the hovered column, undefined when nothing is
|
|
3606
|
-
* hovered
|
|
3607
|
-
*/
|
|
3608
|
-
get mouseOverColumnStats(): ColumnStats | undefined {
|
|
3609
|
-
const { mouseCol, colStats, conservation, propertyConservation } = self
|
|
3610
|
-
return mouseCol === undefined
|
|
3611
|
-
? undefined
|
|
3612
|
-
: columnStats({
|
|
3613
|
-
col: mouseCol,
|
|
3614
|
-
colStats,
|
|
3615
|
-
conservation,
|
|
3616
|
-
propertyConservation,
|
|
3617
|
-
})
|
|
3618
|
-
},
|
|
3619
|
-
|
|
3620
3708
|
/**
|
|
3621
3709
|
* #method
|
|
3622
3710
|
*/
|
|
@@ -3683,7 +3771,6 @@ function stateModelFactory() {
|
|
|
3683
3771
|
spans: featurePanelSpans({
|
|
3684
3772
|
panel,
|
|
3685
3773
|
width,
|
|
3686
|
-
rowHeight: self.rowHeight,
|
|
3687
3774
|
colWidth: self.colWidth,
|
|
3688
3775
|
domainBands: this.domainBands,
|
|
3689
3776
|
annotationsByRow: self.annotationsByRow,
|
|
@@ -4002,7 +4089,9 @@ function stateModelFactory() {
|
|
|
4002
4089
|
// alignment all read. The tree arrives with the model for inline data
|
|
4003
4090
|
// and later for a filehandle, so the seeding waits for it and then runs
|
|
4004
4091
|
// once: expanding a seeded clade sticks, and the record collapses it
|
|
4005
|
-
// again only on reload.
|
|
4092
|
+
// again only on reload. `dataInitialized` is true once the MSA alone
|
|
4093
|
+
// has loaded, when the tree is still the flat stub, so a tree
|
|
4094
|
+
// filehandle holds the seeding until its text lands.
|
|
4006
4095
|
let cladesSeeded = false
|
|
4007
4096
|
addDisposer(
|
|
4008
4097
|
self,
|
|
@@ -4010,6 +4099,7 @@ function stateModelFactory() {
|
|
|
4010
4099
|
if (
|
|
4011
4100
|
cladesSeeded ||
|
|
4012
4101
|
!self.dataInitialized ||
|
|
4102
|
+
(self.treeFilehandle && !self.data.tree) ||
|
|
4013
4103
|
self.clades.length === 0
|
|
4014
4104
|
) {
|
|
4015
4105
|
return
|
|
@@ -4028,6 +4118,25 @@ function stateModelFactory() {
|
|
|
4028
4118
|
}),
|
|
4029
4119
|
)
|
|
4030
4120
|
|
|
4121
|
+
// zoomToRegion needs a width and resolves residues through the visible
|
|
4122
|
+
// columns, so this waits for both, and for a tree file whose collapsed
|
|
4123
|
+
// clades would hide columns and move the target after the zoom
|
|
4124
|
+
addDisposer(
|
|
4125
|
+
self,
|
|
4126
|
+
autorun(() => {
|
|
4127
|
+
const { region } = self
|
|
4128
|
+
if (
|
|
4129
|
+
region &&
|
|
4130
|
+
self.viewInitialized &&
|
|
4131
|
+
self.numColumns > 0 &&
|
|
4132
|
+
!(self.treeFilehandle && !self.data.tree)
|
|
4133
|
+
) {
|
|
4134
|
+
this.zoomToRegion(region)
|
|
4135
|
+
self.setRegion(undefined)
|
|
4136
|
+
}
|
|
4137
|
+
}),
|
|
4138
|
+
)
|
|
4139
|
+
|
|
4031
4140
|
// the matchMedia query is pinned to the current device pixel ratio, so
|
|
4032
4141
|
// each change re-registers against the new one
|
|
4033
4142
|
if (
|
|
@@ -4193,6 +4302,24 @@ function stateModelFactory() {
|
|
|
4193
4302
|
}),
|
|
4194
4303
|
)
|
|
4195
4304
|
|
|
4305
|
+
// a GFF whose first column names rows of some other alignment parses
|
|
4306
|
+
// without error and draws nothing
|
|
4307
|
+
addDisposer(
|
|
4308
|
+
self,
|
|
4309
|
+
autorun(() => {
|
|
4310
|
+
const { annotations, rowNamesSet } = self
|
|
4311
|
+
if (
|
|
4312
|
+
annotations.length > 0 &&
|
|
4313
|
+
rowNamesSet.size > 0 &&
|
|
4314
|
+
!annotations.some(a => rowNamesSet.has(a.id))
|
|
4315
|
+
) {
|
|
4316
|
+
self.addWarning(
|
|
4317
|
+
`0 of ${annotations.length} annotations name a row in this alignment`,
|
|
4318
|
+
)
|
|
4319
|
+
}
|
|
4320
|
+
}),
|
|
4321
|
+
)
|
|
4322
|
+
|
|
4196
4323
|
// gffFilehandle loads into data.gff, which the autorun above parses
|
|
4197
4324
|
loadOnFilehandleChange({
|
|
4198
4325
|
what: 'annotations',
|
package/src/types.ts
CHANGED
|
@@ -131,6 +131,8 @@ export interface RowFeaturesSpec {
|
|
|
131
131
|
header?: string
|
|
132
132
|
encoding?: FeatureEncodingSpec
|
|
133
133
|
transform?: AlignTransform[]
|
|
134
|
+
/** how overlapping features share the row's height; see docs/layers.md */
|
|
135
|
+
position?: 'identity' | 'strandpile'
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
/**
|
package/src/useWheelScroll.ts
CHANGED
|
@@ -147,10 +147,6 @@ export function useWheelScroll({
|
|
|
147
147
|
}, [mouseDragging, onScrollX, onScrollY])
|
|
148
148
|
|
|
149
149
|
function onMouseDown(event: React.MouseEvent) {
|
|
150
|
-
const target = event.target as HTMLElement
|
|
151
|
-
if (target.draggable || target.dataset.resizer) {
|
|
152
|
-
return
|
|
153
|
-
}
|
|
154
150
|
if (event.button === 0) {
|
|
155
151
|
prevX.current = event.clientX
|
|
156
152
|
prevY.current = event.clientY
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '8.
|
|
1
|
+
export const version = '8.2.0'
|