react-msaview 8.0.0 → 8.1.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/model.d.ts +31 -32
- package/dist/model.js +184 -141
- package/dist/model.js.map +1 -1
- package/dist/svgTestUtil.d.ts +18 -22
- 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/model.ts +232 -161
- package/src/types.ts +2 -0
- package/src/useWheelScroll.ts +0 -4
- package/src/version.ts +1 -1
|
@@ -13,12 +13,12 @@ import { useTreeHover } from './useTreeHover.ts'
|
|
|
13
13
|
|
|
14
14
|
import type { MsaViewModel } from '../../model.ts'
|
|
15
15
|
|
|
16
|
-
const useStyles = makeStyles()(
|
|
16
|
+
const useStyles = makeStyles()(theme => ({
|
|
17
17
|
hover: {
|
|
18
18
|
position: 'absolute',
|
|
19
19
|
pointerEvents: 'none',
|
|
20
20
|
zIndex: 100,
|
|
21
|
-
background:
|
|
21
|
+
background: theme.palette.action.hover,
|
|
22
22
|
},
|
|
23
23
|
}))
|
|
24
24
|
|
|
@@ -402,7 +402,8 @@ function renderTreeLabels({
|
|
|
402
402
|
// label when the "name" is just the auto-generated internal-node id
|
|
403
403
|
const isAnonymousCollapsed = collapsedSet.has(id) && name === id
|
|
404
404
|
if (!isAnonymousCollapsed && inYBlock(y, offsetY, by, pad)) {
|
|
405
|
-
//
|
|
405
|
+
// fontSize/4 below the row center, the baseline the alignment letters
|
|
406
|
+
// share
|
|
406
407
|
const yp = y + fontSize / 4
|
|
407
408
|
let xp = 0
|
|
408
409
|
if (!noTree) {
|
package/src/constants.ts
CHANGED
|
@@ -48,6 +48,11 @@ export const defaultScrollZoomAxis: ScrollZoomAxis = 'both'
|
|
|
48
48
|
export const minLetterRowHeight = 8
|
|
49
49
|
export const minLetterColWidth = 5
|
|
50
50
|
|
|
51
|
+
// A feature span draws its label at two pixels under its height, capped at 11,
|
|
52
|
+
// so a span shorter than this holds no readable text: an underline bar, a
|
|
53
|
+
// sub-row band or a features panel over small rows
|
|
54
|
+
export const minFeatureLabelHeight = 9
|
|
55
|
+
|
|
51
56
|
// Tree labels are measured once at this size and scaled to the current font
|
|
52
57
|
// size, so a vertical zoom never re-measures the tree.
|
|
53
58
|
export const labelReferenceFontSize = 16
|
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
|
|
@@ -1064,10 +1108,12 @@ function stateModelFactory() {
|
|
|
1064
1108
|
/**
|
|
1065
1109
|
* #action
|
|
1066
1110
|
* record a non-fatal load problem: a layer that failed to load, a file
|
|
1067
|
-
* that failed to parse
|
|
1111
|
+
* that failed to parse. A message already on the list is not added again
|
|
1068
1112
|
*/
|
|
1069
1113
|
addWarning(warning: string) {
|
|
1070
|
-
self.warnings
|
|
1114
|
+
if (!self.warnings.includes(warning)) {
|
|
1115
|
+
self.warnings = [...self.warnings, warning]
|
|
1116
|
+
}
|
|
1071
1117
|
},
|
|
1072
1118
|
|
|
1073
1119
|
/**
|
|
@@ -1776,7 +1822,7 @@ function stateModelFactory() {
|
|
|
1776
1822
|
*/
|
|
1777
1823
|
get insertionPositions() {
|
|
1778
1824
|
const { blanks, rows } = this
|
|
1779
|
-
if (blanks.length === 0
|
|
1825
|
+
if (blanks.length === 0) {
|
|
1780
1826
|
return new Map<string, { pos: number; letters: string }[]>()
|
|
1781
1827
|
}
|
|
1782
1828
|
const result = new Map<string, { pos: number; letters: string }[]>()
|
|
@@ -2039,7 +2085,7 @@ function stateModelFactory() {
|
|
|
2039
2085
|
* cladogram mode. The tree's scale bar uses it.
|
|
2040
2086
|
*/
|
|
2041
2087
|
get pxPerBranchLength() {
|
|
2042
|
-
const max =
|
|
2088
|
+
const max = this.rootToTipLength
|
|
2043
2089
|
return this.showBranchLenEffective && max ? self.treeWidth / max : 0
|
|
2044
2090
|
},
|
|
2045
2091
|
|
|
@@ -2094,6 +2140,140 @@ function stateModelFactory() {
|
|
|
2094
2140
|
return self.drawLabels && self.rowHeight >= minLetterRowHeight
|
|
2095
2141
|
},
|
|
2096
2142
|
}))
|
|
2143
|
+
.views(self => {
|
|
2144
|
+
// a spec is a frozen value, so it keys its computed until setColumnTracks
|
|
2145
|
+
// replaces it
|
|
2146
|
+
const columnTrackModels = new WeakMap<
|
|
2147
|
+
ColumnTrackSpec,
|
|
2148
|
+
IComputedValue<BasicTrack>
|
|
2149
|
+
>()
|
|
2150
|
+
return {
|
|
2151
|
+
/**
|
|
2152
|
+
* #getter
|
|
2153
|
+
* a data track's values or string, projected from its row's residues
|
|
2154
|
+
* onto alignment columns when it names a row
|
|
2155
|
+
*/
|
|
2156
|
+
get columnTrackContent() {
|
|
2157
|
+
const { MSA, blanks, hideGapsEffective } = self
|
|
2158
|
+
const width = MSA?.getWidth() ?? 0
|
|
2159
|
+
const project = <T>(track: ColumnTrackSpec, items: T[], fill: T) => {
|
|
2160
|
+
if (!track.row) {
|
|
2161
|
+
return items
|
|
2162
|
+
}
|
|
2163
|
+
const out = Array.from({ length: width }, () => fill)
|
|
2164
|
+
const index = self.seqPosIndex(track.row)
|
|
2165
|
+
items.forEach((item, seqPos) => {
|
|
2166
|
+
const col = index?.[seqPos]
|
|
2167
|
+
if (col !== undefined) {
|
|
2168
|
+
out[col] = item
|
|
2169
|
+
}
|
|
2170
|
+
})
|
|
2171
|
+
return out
|
|
2172
|
+
}
|
|
2173
|
+
const skip = <T>(items: T[]) =>
|
|
2174
|
+
hideGapsEffective ? dropBlanks(blanks, items) : items
|
|
2175
|
+
// an arc endpoint maps row residue -> column -> visible column.
|
|
2176
|
+
// visibleColsBefore, not globalColToVisibleCol, so an endpoint in a
|
|
2177
|
+
// hidden column moves to the neighboring visible one and the arc stays
|
|
2178
|
+
const resolve = (track: ColumnTrackSpec, pos: number) => {
|
|
2179
|
+
const col = track.row
|
|
2180
|
+
? self.seqPosIndex(track.row)?.[pos - 1]
|
|
2181
|
+
: pos - 1
|
|
2182
|
+
if (col === undefined || col < 0 || col >= width) {
|
|
2183
|
+
return undefined
|
|
2184
|
+
}
|
|
2185
|
+
return hideGapsEffective ? visibleColsBefore(blanks, col) : col
|
|
2186
|
+
}
|
|
2187
|
+
return new Map<
|
|
2188
|
+
string,
|
|
2189
|
+
{ values?: number[]; data?: string; arcs?: Arc[] }
|
|
2190
|
+
>(
|
|
2191
|
+
self.columnTracks.map(track => {
|
|
2192
|
+
if (track.kind === 'arc') {
|
|
2193
|
+
const arcs = (track.arcs ?? [])
|
|
2194
|
+
.map(arc => {
|
|
2195
|
+
const start = resolve(track, Math.min(arc.start, arc.end))
|
|
2196
|
+
const end = resolve(track, Math.max(arc.start, arc.end))
|
|
2197
|
+
return start !== undefined &&
|
|
2198
|
+
end !== undefined &&
|
|
2199
|
+
start < end
|
|
2200
|
+
? { start, end, color: arc.color }
|
|
2201
|
+
: undefined
|
|
2202
|
+
})
|
|
2203
|
+
.filter(notEmpty)
|
|
2204
|
+
return [track.id, { arcs }] as const
|
|
2205
|
+
}
|
|
2206
|
+
if (track.kind === 'bar') {
|
|
2207
|
+
const max = track.max ?? 1
|
|
2208
|
+
const values = skip(project(track, track.values ?? [], 0)).map(
|
|
2209
|
+
v => Math.min(1, Math.max(0, v / max)),
|
|
2210
|
+
)
|
|
2211
|
+
return [track.id, { values }] as const
|
|
2212
|
+
}
|
|
2213
|
+
const data = skip(
|
|
2214
|
+
project(track, (track.data ?? '').split(''), ' '),
|
|
2215
|
+
)
|
|
2216
|
+
return [track.id, { data: data.join('') }] as const
|
|
2217
|
+
}),
|
|
2218
|
+
)
|
|
2219
|
+
},
|
|
2220
|
+
/**
|
|
2221
|
+
* #method
|
|
2222
|
+
* the height a track draws at: what the user dragged its divider to,
|
|
2223
|
+
* then the height its snapshot asked for, then its kind's default. Only
|
|
2224
|
+
* a text track falls through to rowHeight
|
|
2225
|
+
*/
|
|
2226
|
+
trackHeight(kind: TrackKind, heightKey: string = kind, given?: number) {
|
|
2227
|
+
return (
|
|
2228
|
+
self.trackHeights.get(heightKey) ??
|
|
2229
|
+
given ??
|
|
2230
|
+
defaultTrackHeights[kind] ??
|
|
2231
|
+
self.rowHeight
|
|
2232
|
+
)
|
|
2233
|
+
},
|
|
2234
|
+
/**
|
|
2235
|
+
* #method
|
|
2236
|
+
* the track a column track spec draws as, computed once per spec. A
|
|
2237
|
+
* text track's height falls through to rowHeight, and its own computed
|
|
2238
|
+
* confines the vertical zoom to it, so a sibling keeps its object and
|
|
2239
|
+
* its canvas skips the redraw
|
|
2240
|
+
*/
|
|
2241
|
+
columnTrackModel(track: ColumnTrackSpec): BasicTrack {
|
|
2242
|
+
let computedModel = columnTrackModels.get(track)
|
|
2243
|
+
if (!computedModel) {
|
|
2244
|
+
computedModel = computed(() => {
|
|
2245
|
+
const heightKey = resizableKinds.has(track.kind)
|
|
2246
|
+
? ownHeightKey(track.id)
|
|
2247
|
+
: undefined
|
|
2248
|
+
const content = this.columnTrackContent.get(track.id)
|
|
2249
|
+
return {
|
|
2250
|
+
model: {
|
|
2251
|
+
id: track.id,
|
|
2252
|
+
name: track.name,
|
|
2253
|
+
kind: track.kind,
|
|
2254
|
+
heightKey,
|
|
2255
|
+
height: this.trackHeight(track.kind, heightKey, track.height),
|
|
2256
|
+
barColor: track.color,
|
|
2257
|
+
arcColor: track.color,
|
|
2258
|
+
customColorScheme: track.colors,
|
|
2259
|
+
data: content?.data,
|
|
2260
|
+
arcs: content?.arcs,
|
|
2261
|
+
},
|
|
2262
|
+
ReactComponent: TrackBlocks,
|
|
2263
|
+
}
|
|
2264
|
+
})
|
|
2265
|
+
columnTrackModels.set(track, computedModel)
|
|
2266
|
+
}
|
|
2267
|
+
return computedModel.get()
|
|
2268
|
+
},
|
|
2269
|
+
/**
|
|
2270
|
+
* #getter
|
|
2271
|
+
*/
|
|
2272
|
+
get columnTrackModels(): BasicTrack[] {
|
|
2273
|
+
return self.columnTracks.map(track => this.columnTrackModel(track))
|
|
2274
|
+
},
|
|
2275
|
+
}
|
|
2276
|
+
})
|
|
2097
2277
|
.views(self => ({
|
|
2098
2278
|
/**
|
|
2099
2279
|
* #getter
|
|
@@ -2199,111 +2379,6 @@ function stateModelFactory() {
|
|
|
2199
2379
|
}))
|
|
2200
2380
|
},
|
|
2201
2381
|
|
|
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
2382
|
/**
|
|
2308
2383
|
* #getter
|
|
2309
2384
|
* the consensus secondary structure as a track, when there is one. A
|
|
@@ -2320,7 +2395,7 @@ function stateModelFactory() {
|
|
|
2320
2395
|
name: 'Base pairs',
|
|
2321
2396
|
kind: 'arc' as const,
|
|
2322
2397
|
heightKey: 'arc',
|
|
2323
|
-
height:
|
|
2398
|
+
height: self.trackHeight('arc'),
|
|
2324
2399
|
arcs,
|
|
2325
2400
|
},
|
|
2326
2401
|
ReactComponent: TrackBlocks,
|
|
@@ -2369,7 +2444,7 @@ function stateModelFactory() {
|
|
|
2369
2444
|
model: {
|
|
2370
2445
|
...model,
|
|
2371
2446
|
heightKey: resizableKinds.has(model.kind) ? model.kind : undefined,
|
|
2372
|
-
height:
|
|
2447
|
+
height: self.trackHeight(model.kind),
|
|
2373
2448
|
},
|
|
2374
2449
|
ReactComponent: TrackBlocks,
|
|
2375
2450
|
}))
|
|
@@ -2379,7 +2454,7 @@ function stateModelFactory() {
|
|
|
2379
2454
|
return [
|
|
2380
2455
|
...this.adapterTrackModels,
|
|
2381
2456
|
...this.basePairTrackModels,
|
|
2382
|
-
...
|
|
2457
|
+
...self.columnTrackModels,
|
|
2383
2458
|
// every computed track reads the alignment's columns, and a tree, a
|
|
2384
2459
|
// GFF and a features panel draw a figure with none
|
|
2385
2460
|
...(self.numColumns > 0 ? this.computedTrackModels : []),
|
|
@@ -3275,13 +3350,6 @@ function stateModelFactory() {
|
|
|
3275
3350
|
)
|
|
3276
3351
|
return { ...segments, ...categorical }
|
|
3277
3352
|
},
|
|
3278
|
-
get strokePalette() {
|
|
3279
|
-
return transform(this.fillPalette, ([key, val]) => [
|
|
3280
|
-
key,
|
|
3281
|
-
outlineColor(val),
|
|
3282
|
-
])
|
|
3283
|
-
},
|
|
3284
|
-
|
|
3285
3353
|
/**
|
|
3286
3354
|
* #getter
|
|
3287
3355
|
* the encoding coloring the overlay's spans, undefined when none does,
|
|
@@ -3600,23 +3668,6 @@ function stateModelFactory() {
|
|
|
3600
3668
|
})
|
|
3601
3669
|
},
|
|
3602
3670
|
|
|
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
3671
|
/**
|
|
3621
3672
|
* #method
|
|
3622
3673
|
*/
|
|
@@ -3683,7 +3734,6 @@ function stateModelFactory() {
|
|
|
3683
3734
|
spans: featurePanelSpans({
|
|
3684
3735
|
panel,
|
|
3685
3736
|
width,
|
|
3686
|
-
rowHeight: self.rowHeight,
|
|
3687
3737
|
colWidth: self.colWidth,
|
|
3688
3738
|
domainBands: this.domainBands,
|
|
3689
3739
|
annotationsByRow: self.annotationsByRow,
|
|
@@ -4002,7 +4052,9 @@ function stateModelFactory() {
|
|
|
4002
4052
|
// alignment all read. The tree arrives with the model for inline data
|
|
4003
4053
|
// and later for a filehandle, so the seeding waits for it and then runs
|
|
4004
4054
|
// once: expanding a seeded clade sticks, and the record collapses it
|
|
4005
|
-
// again only on reload.
|
|
4055
|
+
// again only on reload. `dataInitialized` is true once the MSA alone
|
|
4056
|
+
// has loaded, when the tree is still the flat stub, so a tree
|
|
4057
|
+
// filehandle holds the seeding until its text lands.
|
|
4006
4058
|
let cladesSeeded = false
|
|
4007
4059
|
addDisposer(
|
|
4008
4060
|
self,
|
|
@@ -4010,6 +4062,7 @@ function stateModelFactory() {
|
|
|
4010
4062
|
if (
|
|
4011
4063
|
cladesSeeded ||
|
|
4012
4064
|
!self.dataInitialized ||
|
|
4065
|
+
(self.treeFilehandle && !self.data.tree) ||
|
|
4013
4066
|
self.clades.length === 0
|
|
4014
4067
|
) {
|
|
4015
4068
|
return
|
|
@@ -4193,6 +4246,24 @@ function stateModelFactory() {
|
|
|
4193
4246
|
}),
|
|
4194
4247
|
)
|
|
4195
4248
|
|
|
4249
|
+
// a GFF whose first column names rows of some other alignment parses
|
|
4250
|
+
// without error and draws nothing
|
|
4251
|
+
addDisposer(
|
|
4252
|
+
self,
|
|
4253
|
+
autorun(() => {
|
|
4254
|
+
const { annotations, rowNamesSet } = self
|
|
4255
|
+
if (
|
|
4256
|
+
annotations.length > 0 &&
|
|
4257
|
+
rowNamesSet.size > 0 &&
|
|
4258
|
+
!annotations.some(a => rowNamesSet.has(a.id))
|
|
4259
|
+
) {
|
|
4260
|
+
self.addWarning(
|
|
4261
|
+
`0 of ${annotations.length} annotations name a row in this alignment`,
|
|
4262
|
+
)
|
|
4263
|
+
}
|
|
4264
|
+
}),
|
|
4265
|
+
)
|
|
4266
|
+
|
|
4196
4267
|
// gffFilehandle loads into data.gff, which the autorun above parses
|
|
4197
4268
|
loadOnFilehandleChange({
|
|
4198
4269
|
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.1.0'
|