react-msaview 9.2.0 → 9.4.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 +74 -74
- package/bundle/index.js.map +4 -4
- package/dist/components/DragHandle.js +2 -1
- package/dist/components/DragHandle.js.map +1 -1
- package/dist/components/MSAViewer.d.ts +29 -19
- package/dist/components/MSAViewer.js +2 -1
- package/dist/components/MSAViewer.js.map +1 -1
- package/dist/components/Track.js +2 -5
- package/dist/components/Track.js.map +1 -1
- package/dist/components/VerticalScrollbar.js +1 -1
- package/dist/components/VerticalScrollbar.js.map +1 -1
- package/dist/components/header/Header.js +2 -1
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/minimap/Minimap.js +1 -1
- package/dist/components/minimap/Minimap.js.map +1 -1
- package/dist/components/msa/MSACanvas.js +16 -27
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/rowpanels/RowPanels.js +8 -6
- package/dist/components/rowpanels/RowPanels.js.map +1 -1
- package/dist/components/settings/SettingsPanel.js +2 -1
- package/dist/components/settings/SettingsPanel.js.map +1 -1
- package/dist/components/tree/TreeCanvas.js +8 -6
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +2 -0
- package/dist/constants.js.map +1 -1
- package/dist/element.d.ts +11 -7
- package/dist/element.js +46 -8
- package/dist/element.js.map +1 -1
- package/dist/featurePanels.d.ts +0 -7
- package/dist/featurePanels.js +1 -16
- package/dist/featurePanels.js.map +1 -1
- package/dist/ggplotPalettes.js +12 -1
- package/dist/ggplotPalettes.js.map +1 -1
- package/dist/groupKeys.d.ts +6 -0
- package/dist/groupKeys.js +52 -0
- package/dist/groupKeys.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/model.d.ts +38 -18
- package/dist/model.js +107 -56
- package/dist/model.js.map +1 -1
- package/dist/scales.d.ts +114 -13
- package/dist/scales.js +154 -19
- package/dist/scales.js.map +1 -1
- package/dist/svgTestUtil.d.ts +20 -12
- package/dist/types.d.ts +29 -27
- package/dist/useDragScroll.d.ts +4 -3
- package/dist/useDragScroll.js +9 -6
- package/dist/useDragScroll.js.map +1 -1
- package/dist/useWheelScroll.d.ts +26 -8
- package/dist/useWheelScroll.js +108 -71
- package/dist/useWheelScroll.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/viewerSnapshot.js +2 -1
- package/dist/viewerSnapshot.js.map +1 -1
- package/package.json +3 -3
- package/src/components/DragHandle.tsx +2 -1
- package/src/components/MSAViewer.tsx +11 -8
- package/src/components/Track.tsx +2 -8
- package/src/components/VerticalScrollbar.tsx +1 -1
- package/src/components/header/Header.tsx +2 -1
- package/src/components/minimap/Minimap.tsx +1 -1
- package/src/components/msa/MSACanvas.tsx +14 -35
- package/src/components/rowpanels/RowPanels.tsx +8 -10
- package/src/components/settings/SettingsPanel.tsx +2 -1
- package/src/components/tree/TreeCanvas.tsx +8 -10
- package/src/constants.ts +2 -0
- package/src/element.ts +53 -8
- package/src/featurePanels.ts +1 -25
- package/src/ggplotPalettes.ts +13 -1
- package/src/groupKeys.ts +54 -0
- package/src/index.ts +13 -2
- package/src/model.ts +159 -74
- package/src/scales.ts +268 -25
- package/src/types.ts +28 -25
- package/src/useDragScroll.ts +11 -8
- package/src/useWheelScroll.ts +124 -84
- package/src/version.ts +1 -1
- package/src/viewerSnapshot.ts +3 -1
package/src/constants.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { TreeOrder } from './types.ts'
|
|
|
4
4
|
export const defaultRowHeight = 16
|
|
5
5
|
export const defaultColWidth = 12
|
|
6
6
|
export const defaultHeight = 550
|
|
7
|
+
// the dividers' thickness, between the tree and the alignment and under the view
|
|
8
|
+
export const resizeHandleWidth = 5
|
|
7
9
|
export const defaultScrollX = 0
|
|
8
10
|
export const defaultScrollY = 0
|
|
9
11
|
export const defaultCurrentAlignment = 0
|
package/src/element.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { when } from 'mobx'
|
|
2
2
|
|
|
3
|
+
import { defaultHeight, resizeHandleWidth } from './constants.ts'
|
|
3
4
|
import { mount } from './mount.tsx'
|
|
4
5
|
|
|
5
6
|
import type { MSAViewerProps } from './components/MSAViewer.tsx'
|
|
@@ -46,8 +47,13 @@ const ownAttributes = [
|
|
|
46
47
|
'theme',
|
|
47
48
|
'reference-row',
|
|
48
49
|
'tree-area-width',
|
|
50
|
+
'loading',
|
|
49
51
|
] as const
|
|
50
52
|
|
|
53
|
+
// how far outside the viewport a lazy element starts mounting, so the viewer
|
|
54
|
+
// is drawing by the time the reader scrolls to it
|
|
55
|
+
const lazyMargin = '200px'
|
|
56
|
+
|
|
51
57
|
// the attributes a <nightingale-manager> writes on the elements it holds
|
|
52
58
|
const nightingaleAttributes = ['display-start', 'display-end', 'highlight']
|
|
53
59
|
|
|
@@ -87,13 +93,17 @@ function residueRange(model: MsaViewModel, row: string, viewport: Viewport) {
|
|
|
87
93
|
/**
|
|
88
94
|
* Registers a custom element that renders MSAViewer, for pages with no React.
|
|
89
95
|
* Attributes: msa-url, tree-url, gff-url, color-scheme, height, hide-header,
|
|
90
|
-
* theme, tree-area-width
|
|
91
|
-
* features, colorScheme, highlights, columnTracks, residueMappings
|
|
92
|
-
* selection. The
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
96
|
+
* theme, tree-area-width, reference-row and loading. Properties: msa, tree and
|
|
97
|
+
* gff text, features, colorScheme, highlights, columnTracks, residueMappings
|
|
98
|
+
* and selection. The colorScheme property takes a `{domain, range}` the
|
|
99
|
+
* color-scheme attribute cannot hold, and wins over the attribute while set. Events:
|
|
100
|
+
* cell-hover, cell-click, viewport-change and selection-change, each with the
|
|
101
|
+
* MSAViewer callback's value as detail.
|
|
102
|
+
*
|
|
103
|
+
* `loading="lazy"` mounts the viewer once the element comes within 200 pixels
|
|
104
|
+
* of the viewport, as `<img loading="lazy">` loads its image. Until then the
|
|
105
|
+
* element shows its own children, such as an `<img>` of the first frame, and
|
|
106
|
+
* reserves the viewer's height.
|
|
97
107
|
*
|
|
98
108
|
* Inside a <nightingale-manager>, the element registers with it and speaks its
|
|
99
109
|
* protocol: it follows the display-start, display-end and highlight attributes
|
|
@@ -110,6 +120,7 @@ export function defineMsaElement(tagName = 'jbrowse-msa') {
|
|
|
110
120
|
static observedAttributes = [...ownAttributes, ...nightingaleAttributes]
|
|
111
121
|
|
|
112
122
|
#viewer?: MountedViewer
|
|
123
|
+
#nearby?: IntersectionObserver
|
|
113
124
|
#model?: MsaViewModel
|
|
114
125
|
#manager?: NightingaleManager
|
|
115
126
|
#cancelRange?: () => void
|
|
@@ -289,7 +300,22 @@ export function defineMsaElement(tagName = 'jbrowse-msa') {
|
|
|
289
300
|
if (!this.style.display) {
|
|
290
301
|
this.style.display = 'block'
|
|
291
302
|
}
|
|
292
|
-
|
|
303
|
+
if (this.#lazy) {
|
|
304
|
+
const height =
|
|
305
|
+
numberAttribute(this.getAttribute('height')) ?? defaultHeight
|
|
306
|
+
this.style.minHeight = `${height + resizeHandleWidth}px`
|
|
307
|
+
this.#nearby = new IntersectionObserver(
|
|
308
|
+
entries => {
|
|
309
|
+
if (entries.some(entry => entry.isIntersecting)) {
|
|
310
|
+
this.#mount()
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
{ rootMargin: lazyMargin },
|
|
314
|
+
)
|
|
315
|
+
this.#nearby.observe(this)
|
|
316
|
+
} else {
|
|
317
|
+
this.#mount()
|
|
318
|
+
}
|
|
293
319
|
const manager = this.closest<NightingaleManager>('nightingale-manager')
|
|
294
320
|
if (manager) {
|
|
295
321
|
void customElements.whenDefined('nightingale-manager').then(() => {
|
|
@@ -301,7 +327,22 @@ export function defineMsaElement(tagName = 'jbrowse-msa') {
|
|
|
301
327
|
}
|
|
302
328
|
}
|
|
303
329
|
|
|
330
|
+
get #lazy() {
|
|
331
|
+
return (
|
|
332
|
+
this.getAttribute('loading') === 'lazy' &&
|
|
333
|
+
typeof IntersectionObserver !== 'undefined'
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
#mount() {
|
|
338
|
+
this.#nearby?.disconnect()
|
|
339
|
+
this.#nearby = undefined
|
|
340
|
+
this.#viewer ??= mount(this, this.#props())
|
|
341
|
+
}
|
|
342
|
+
|
|
304
343
|
disconnectedCallback() {
|
|
344
|
+
this.#nearby?.disconnect()
|
|
345
|
+
this.#nearby = undefined
|
|
305
346
|
this.#manager?.unregister(this)
|
|
306
347
|
this.#manager = undefined
|
|
307
348
|
this.#cancelRange?.()
|
|
@@ -323,6 +364,10 @@ export function defineMsaElement(tagName = 'jbrowse-msa') {
|
|
|
323
364
|
}
|
|
324
365
|
} else if (name === 'highlight') {
|
|
325
366
|
this.#followHighlight()
|
|
367
|
+
} else if (name === 'loading') {
|
|
368
|
+
if (this.#nearby && !this.#lazy) {
|
|
369
|
+
this.#mount()
|
|
370
|
+
}
|
|
326
371
|
} else {
|
|
327
372
|
this.#viewer?.update(this.#props())
|
|
328
373
|
}
|
package/src/featurePanels.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import { notEmpty } from '@jbrowse/core/util'
|
|
2
|
-
|
|
3
1
|
import { packDomainLanes } from './components/msa/packDomainLanes.ts'
|
|
4
2
|
import { defaultFeaturePanelWidth } from './constants.ts'
|
|
5
3
|
import { featureField } from './featureFields.ts'
|
|
6
|
-
import { resolveScale } from './scales.ts'
|
|
7
4
|
import { outlineColor } from './util.ts'
|
|
8
5
|
|
|
9
|
-
import type { ScaleSpec } from './scales.ts'
|
|
10
6
|
import type {
|
|
11
7
|
Annotation,
|
|
12
8
|
DomainBand,
|
|
@@ -16,7 +12,7 @@ import type {
|
|
|
16
12
|
} from './types.ts'
|
|
17
13
|
|
|
18
14
|
// the fill of a span under a scale that gives its value no color, such as a
|
|
19
|
-
// gene a `
|
|
15
|
+
// gene a `domain` of gene names leaves out
|
|
20
16
|
const UNSCALED_FEATURE = '#d9d9d9'
|
|
21
17
|
|
|
22
18
|
// the fraction of a span a following one may cover before it takes a lane of
|
|
@@ -247,23 +243,3 @@ export function featurePanelSpans({
|
|
|
247
243
|
]),
|
|
248
244
|
)
|
|
249
245
|
}
|
|
250
|
-
|
|
251
|
-
// a scale over the values a field takes across every feature loaded, so hiding
|
|
252
|
-
// a type in the filter dialog leaves the rest in their colors, and a legend of
|
|
253
|
-
// the values the features drawn carry
|
|
254
|
-
export function resolveFeatureScale(
|
|
255
|
-
field: string,
|
|
256
|
-
scale: ScaleSpec | undefined,
|
|
257
|
-
annotations: Annotation[],
|
|
258
|
-
drawn: Annotation[],
|
|
259
|
-
) {
|
|
260
|
-
const valuesOf = (list: Annotation[]) =>
|
|
261
|
-
list.map(a => featureField(a, field)).filter(notEmpty)
|
|
262
|
-
const resolved = resolveScale(scale, valuesOf(annotations))
|
|
263
|
-
const shown = new Set(valuesOf(drawn))
|
|
264
|
-
return {
|
|
265
|
-
field,
|
|
266
|
-
...resolved,
|
|
267
|
-
legend: resolved.legend.filter(entry => shown.has(entry.id)),
|
|
268
|
-
}
|
|
269
|
-
}
|
package/src/ggplotPalettes.ts
CHANGED
|
@@ -72,10 +72,22 @@ export const namedPalettes: Record<string, readonly string[]> = {
|
|
|
72
72
|
],
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// the spellings jbrowse-components' `paletteFromSpec` takes for two of them
|
|
76
|
+
const aliases: Record<string, string> = {
|
|
77
|
+
okabeIto: 'okabeito',
|
|
78
|
+
tableau10: 'tableau',
|
|
79
|
+
}
|
|
80
|
+
|
|
75
81
|
/**
|
|
76
82
|
* The colors a palette name asks for. `ggplot`, an unknown name, and no name at
|
|
77
83
|
* all give undefined, which `createPaletteMap` answers with the hue ramp.
|
|
78
84
|
*/
|
|
79
85
|
export function paletteNamed(name?: string) {
|
|
80
|
-
return name === undefined
|
|
86
|
+
return name === undefined
|
|
87
|
+
? undefined
|
|
88
|
+
: Object.hasOwn(namedPalettes, name)
|
|
89
|
+
? namedPalettes[name]
|
|
90
|
+
: Object.hasOwn(aliases, name)
|
|
91
|
+
? namedPalettes[aliases[name]!]
|
|
92
|
+
: undefined
|
|
81
93
|
}
|
package/src/groupKeys.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// copied from jbrowse-components packages/core/src/util/groupKeys.ts, which
|
|
2
|
+
// @jbrowse/core 5.0.0-beta.9 ships but the hosts the plugin runs on do not
|
|
3
|
+
|
|
4
|
+
const NUMERIC = /^-?\d+(\.\d+)?$/
|
|
5
|
+
const DIGIT_RUNS = /\d+|\D+/g
|
|
6
|
+
|
|
7
|
+
function compareRuns(a: string, b: string) {
|
|
8
|
+
const runsA = a.match(DIGIT_RUNS) ?? []
|
|
9
|
+
const runsB = b.match(DIGIT_RUNS) ?? []
|
|
10
|
+
const shared = Math.min(runsA.length, runsB.length)
|
|
11
|
+
for (let i = 0; i < shared; i++) {
|
|
12
|
+
const x = runsA[i]!
|
|
13
|
+
const y = runsB[i]!
|
|
14
|
+
if (x === y) {
|
|
15
|
+
continue
|
|
16
|
+
}
|
|
17
|
+
if (/^\d/.test(x) && /^\d/.test(y)) {
|
|
18
|
+
const nx = Number(x)
|
|
19
|
+
const ny = Number(y)
|
|
20
|
+
if (nx !== ny) {
|
|
21
|
+
return nx < ny ? -1 : 1
|
|
22
|
+
}
|
|
23
|
+
return x.length < y.length ? -1 : 1
|
|
24
|
+
}
|
|
25
|
+
return x < y ? -1 : 1
|
|
26
|
+
}
|
|
27
|
+
return runsA.length - runsB.length
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Numbers first by value, then every other value run by run with digit runs
|
|
32
|
+
* by magnitude, so `2` before `10`, `chr2` before `chr10` and `2.3.2.1a`
|
|
33
|
+
* before `2.3.4.4b`. The empty value sorts last.
|
|
34
|
+
*/
|
|
35
|
+
export function compareGroupKeys(a: string, b: string) {
|
|
36
|
+
if (a === b) {
|
|
37
|
+
return 0
|
|
38
|
+
}
|
|
39
|
+
if (a === '' || b === '') {
|
|
40
|
+
return a === '' ? 1 : -1
|
|
41
|
+
}
|
|
42
|
+
const numA = NUMERIC.test(a)
|
|
43
|
+
const numB = NUMERIC.test(b)
|
|
44
|
+
if (numA !== numB) {
|
|
45
|
+
return numA ? -1 : 1
|
|
46
|
+
}
|
|
47
|
+
if (numA) {
|
|
48
|
+
const diff = Number(a) - Number(b)
|
|
49
|
+
if (diff !== 0) {
|
|
50
|
+
return diff < 0 ? -1 : 1
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return compareRuns(a, b)
|
|
54
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -51,8 +51,19 @@ export type { InterProScanResults } from 'msa-parsers'
|
|
|
51
51
|
export type { ColumnCounts } from './columnCounts.ts'
|
|
52
52
|
export type { ColumnStats } from './columnStats.ts'
|
|
53
53
|
export type { ZoomSnapshot } from './zoomSnapshot.ts'
|
|
54
|
-
// the
|
|
55
|
-
|
|
54
|
+
// the color object a channel reads its field through, and what resolving one
|
|
55
|
+
// gives
|
|
56
|
+
export type {
|
|
57
|
+
ColorChannel,
|
|
58
|
+
ColorSpec,
|
|
59
|
+
LegacyColumnTrack,
|
|
60
|
+
LegacyEncoding,
|
|
61
|
+
LegacyRowPanel,
|
|
62
|
+
LegacyScale,
|
|
63
|
+
ResolvedScale,
|
|
64
|
+
ScaleTable,
|
|
65
|
+
SharedScale,
|
|
66
|
+
} from './scales.ts'
|
|
56
67
|
export type {
|
|
57
68
|
Accession,
|
|
58
69
|
Annotation,
|
package/src/model.ts
CHANGED
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
minRowHeight,
|
|
61
61
|
minTreeWidth,
|
|
62
62
|
pxPerTreeDepth,
|
|
63
|
+
resizeHandleWidth,
|
|
63
64
|
rowPanelHeaderHeight,
|
|
64
65
|
rowTintAlpha,
|
|
65
66
|
scrollZoomAxes,
|
|
@@ -70,13 +71,12 @@ import {
|
|
|
70
71
|
} from './constants.ts'
|
|
71
72
|
import { createPaletteMap } from './createPaletteMap.ts'
|
|
72
73
|
import { exportFileName } from './exportFileName.ts'
|
|
73
|
-
import { featureName } from './featureFields.ts'
|
|
74
|
+
import { featureField, featureName } from './featureFields.ts'
|
|
74
75
|
import {
|
|
75
76
|
colorByRow,
|
|
76
77
|
featureColorMap,
|
|
77
78
|
featureLabelMap,
|
|
78
79
|
featurePanelSpans,
|
|
79
|
-
resolveFeatureScale,
|
|
80
80
|
rowPanelWidth,
|
|
81
81
|
stringFields,
|
|
82
82
|
} from './featurePanels.ts'
|
|
@@ -102,7 +102,14 @@ import {
|
|
|
102
102
|
visibleColToGlobalCol,
|
|
103
103
|
visibleColsBefore,
|
|
104
104
|
} from './rowCoordinateCalculations.ts'
|
|
105
|
-
import {
|
|
105
|
+
import {
|
|
106
|
+
colorTable,
|
|
107
|
+
declarationKey,
|
|
108
|
+
readColumnTrack,
|
|
109
|
+
readEncoding,
|
|
110
|
+
readRowPanel,
|
|
111
|
+
resolveShared,
|
|
112
|
+
} from './scales.ts'
|
|
106
113
|
import { buildSeqPosIndex } from './seqPosToGlobalCol.ts'
|
|
107
114
|
import { maxBitsFor } from './sequenceLogo.ts'
|
|
108
115
|
import {
|
|
@@ -136,6 +143,15 @@ import type { ColumnStats } from './columnStats.ts'
|
|
|
136
143
|
import type { ScrollZoomAxis } from './constants.ts'
|
|
137
144
|
import type { HierarchyNode } from './hierarchy.ts'
|
|
138
145
|
import type { ExportSvgOptions } from './renderToSvg.tsx'
|
|
146
|
+
import type {
|
|
147
|
+
ColorChannel,
|
|
148
|
+
LegacyColumnTrack,
|
|
149
|
+
LegacyEncoding,
|
|
150
|
+
LegacyRowPanel,
|
|
151
|
+
ScaleTable,
|
|
152
|
+
ScaleUse,
|
|
153
|
+
SharedScale,
|
|
154
|
+
} from './scales.ts'
|
|
139
155
|
import type {
|
|
140
156
|
Annotation,
|
|
141
157
|
Arc,
|
|
@@ -600,7 +616,11 @@ function stateModelFactory({
|
|
|
600
616
|
* per-column string drawn as a text track. See docs/layers.md
|
|
601
617
|
*/
|
|
602
618
|
columnTracks: stripDefault(
|
|
603
|
-
types.array(
|
|
619
|
+
types.array(
|
|
620
|
+
types.snapshotProcessor(types.frozen<ColumnTrackSpec>(), {
|
|
621
|
+
preProcessor: readColumnTrack,
|
|
622
|
+
}),
|
|
623
|
+
),
|
|
604
624
|
[],
|
|
605
625
|
),
|
|
606
626
|
|
|
@@ -688,20 +708,35 @@ function stateModelFactory({
|
|
|
688
708
|
clades: stripDefault(types.array(types.frozen<Clade>()), []),
|
|
689
709
|
/**
|
|
690
710
|
* #property
|
|
691
|
-
* what the viewer's own marks read from `rowData
|
|
692
|
-
* `{channel, field,
|
|
693
|
-
* `
|
|
711
|
+
* what the viewer's own marks read from `rowData` or the feature
|
|
712
|
+
* table: `{channel, field, range?, domain?, title?}` per channel. The
|
|
713
|
+
* earlier `scale: {palette}` and `scale: {map}` read as `range` and
|
|
714
|
+
* `domain`. See docs/layers.md
|
|
694
715
|
*/
|
|
695
|
-
encodings: stripDefault(
|
|
716
|
+
encodings: stripDefault(
|
|
717
|
+
types.array(
|
|
718
|
+
types.snapshotProcessor(types.frozen<Encoding>(), {
|
|
719
|
+
preProcessor: readEncoding,
|
|
720
|
+
}),
|
|
721
|
+
),
|
|
722
|
+
[],
|
|
723
|
+
),
|
|
696
724
|
/**
|
|
697
725
|
* #property
|
|
698
726
|
* panels drawn between the tree and the alignment:
|
|
699
|
-
* `{kind: "strip", field,
|
|
700
|
-
* per row from a `rowData` field, and
|
|
727
|
+
* `{kind: "strip", field, range?, domain?, title?, width?, header?}`
|
|
728
|
+
* colors one cell per row from a `rowData` field, and
|
|
701
729
|
* `{kind: "features", x, encoding?, transform?, width?, header?}`
|
|
702
730
|
* draws the spans the GFF carries. See docs/layers.md
|
|
703
731
|
*/
|
|
704
|
-
rowPanels: stripDefault(
|
|
732
|
+
rowPanels: stripDefault(
|
|
733
|
+
types.array(
|
|
734
|
+
types.snapshotProcessor(types.frozen<RowPanelSpec>(), {
|
|
735
|
+
preProcessor: readRowPanel,
|
|
736
|
+
}),
|
|
737
|
+
),
|
|
738
|
+
[],
|
|
739
|
+
),
|
|
705
740
|
}),
|
|
706
741
|
)
|
|
707
742
|
.volatile(() => ({
|
|
@@ -765,9 +800,10 @@ function stateModelFactory({
|
|
|
765
800
|
volatileWidth: undefined as number | undefined,
|
|
766
801
|
/**
|
|
767
802
|
* #volatile
|
|
768
|
-
*
|
|
803
|
+
* thickness of the dividers, between the tree and the alignment and under
|
|
804
|
+
* the view, px
|
|
769
805
|
*/
|
|
770
|
-
resizeHandleWidth
|
|
806
|
+
resizeHandleWidth,
|
|
771
807
|
|
|
772
808
|
/**
|
|
773
809
|
* #volatile
|
|
@@ -2310,7 +2346,7 @@ function stateModelFactory({
|
|
|
2310
2346
|
return [track.id, { arcs }] as const
|
|
2311
2347
|
}
|
|
2312
2348
|
if (track.kind === 'bar') {
|
|
2313
|
-
const max = track.
|
|
2349
|
+
const max = track.scales?.y?.domainMax ?? 1
|
|
2314
2350
|
const values = skip(project(track, track.values ?? [], 0)).map(
|
|
2315
2351
|
v => Math.min(1, Math.max(0, v / max)),
|
|
2316
2352
|
)
|
|
@@ -2361,7 +2397,12 @@ function stateModelFactory({
|
|
|
2361
2397
|
height: this.trackHeight(track.kind, heightKey, track.height),
|
|
2362
2398
|
barColor: track.color,
|
|
2363
2399
|
arcColor: track.color,
|
|
2364
|
-
customColorScheme: track.
|
|
2400
|
+
customColorScheme: track.encoding?.color
|
|
2401
|
+
? colorTable(track.encoding.color, [
|
|
2402
|
+
...(track.encoding.color.domain ?? []),
|
|
2403
|
+
...new Set(track.data),
|
|
2404
|
+
])
|
|
2405
|
+
: undefined,
|
|
2365
2406
|
data: content?.data,
|
|
2366
2407
|
arcs: content?.arcs,
|
|
2367
2408
|
},
|
|
@@ -3418,8 +3459,8 @@ function stateModelFactory({
|
|
|
3418
3459
|
/**
|
|
3419
3460
|
* #action
|
|
3420
3461
|
*/
|
|
3421
|
-
setColumnTracks(tracks: ColumnTrackSpec[]) {
|
|
3422
|
-
self.columnTracks.replace(tracks)
|
|
3462
|
+
setColumnTracks(tracks: (ColumnTrackSpec | LegacyColumnTrack)[]) {
|
|
3463
|
+
self.columnTracks.replace(tracks.map(readColumnTrack))
|
|
3423
3464
|
},
|
|
3424
3465
|
/**
|
|
3425
3466
|
* #action
|
|
@@ -3433,15 +3474,15 @@ function stateModelFactory({
|
|
|
3433
3474
|
* #action
|
|
3434
3475
|
* replace what the viewer's marks read from the row table
|
|
3435
3476
|
*/
|
|
3436
|
-
setEncodings(encodings: Encoding[]) {
|
|
3437
|
-
self.encodings.replace(encodings)
|
|
3477
|
+
setEncodings(encodings: (Encoding | LegacyEncoding)[]) {
|
|
3478
|
+
self.encodings.replace(encodings.map(readEncoding))
|
|
3438
3479
|
},
|
|
3439
3480
|
/**
|
|
3440
3481
|
* #action
|
|
3441
3482
|
* replace the panels drawn between the tree and the alignment
|
|
3442
3483
|
*/
|
|
3443
|
-
setRowPanels(panels: RowPanelSpec[]) {
|
|
3444
|
-
self.rowPanels.replace(panels)
|
|
3484
|
+
setRowPanels(panels: (RowPanelSpec | LegacyRowPanel)[]) {
|
|
3485
|
+
self.rowPanels.replace(panels.map(readRowPanel))
|
|
3445
3486
|
},
|
|
3446
3487
|
/**
|
|
3447
3488
|
* #action
|
|
@@ -3554,12 +3595,20 @@ function stateModelFactory({
|
|
|
3554
3595
|
/**
|
|
3555
3596
|
* #getter
|
|
3556
3597
|
* categorical feature types (InterPro domains and the like) that each get
|
|
3557
|
-
* their own color and a legend entry
|
|
3598
|
+
* their own color and a legend entry, in palette order: grouped by
|
|
3599
|
+
* feature type, types in the order they first appear. A GenPept
|
|
3600
|
+
* record's CDD Sites follow every Region, so the palette spends the
|
|
3601
|
+
* colors after the domains' on the sites inside them
|
|
3558
3602
|
*/
|
|
3559
3603
|
get categoricalDomainTypes() {
|
|
3560
|
-
|
|
3604
|
+
const types = [...self.annotationTypes.values()].filter(
|
|
3561
3605
|
d => !segmentFeatureTypes.has(d.featureType ?? ''),
|
|
3562
3606
|
)
|
|
3607
|
+
const typeOrder = [...new Set(types.map(d => d.featureType))]
|
|
3608
|
+
return types.toSorted(
|
|
3609
|
+
(a, b) =>
|
|
3610
|
+
typeOrder.indexOf(a.featureType) - typeOrder.indexOf(b.featureType),
|
|
3611
|
+
)
|
|
3563
3612
|
},
|
|
3564
3613
|
get fillPalette() {
|
|
3565
3614
|
const segments = Object.fromEntries(
|
|
@@ -3643,42 +3692,41 @@ function stateModelFactory({
|
|
|
3643
3692
|
* the categorical color keys drawn for this view, shared by the on-screen
|
|
3644
3693
|
* legend overlay and the SVG export's reserved column. The domain overlay
|
|
3645
3694
|
* produces the first, listing the `featureFill` scale where an encoding
|
|
3646
|
-
* names one. Every
|
|
3647
|
-
* produces one more, so two channels
|
|
3648
|
-
*
|
|
3649
|
-
*
|
|
3650
|
-
* legend between them
|
|
3695
|
+
* names one. Every scale a row-table encoding or a row panel reads
|
|
3696
|
+
* produces one more, keyed by its declaration, so two channels or two
|
|
3697
|
+
* strips declaring one scale list it once, and strips over eight fields
|
|
3698
|
+
* naming one `title` list one legend between them
|
|
3651
3699
|
*/
|
|
3652
3700
|
get legends(): Legend[] {
|
|
3653
3701
|
const { featureFillEncoding, fillPalette, visibleDomainTypes } = this
|
|
3654
3702
|
const byKey = new Map<string, Legend>()
|
|
3703
|
+
const ids = new Set<string>()
|
|
3655
3704
|
// the accession palette keys on no field, so it takes a key of its own
|
|
3656
3705
|
const add = (
|
|
3657
|
-
|
|
3706
|
+
key: string | undefined,
|
|
3707
|
+
title: string | undefined,
|
|
3658
3708
|
entries: LegendEntry[],
|
|
3659
|
-
|
|
3709
|
+
overlay = false,
|
|
3660
3710
|
) => {
|
|
3661
|
-
|
|
3711
|
+
const k = key ?? 'domains'
|
|
3712
|
+
if (entries.length === 0 || byKey.has(k)) {
|
|
3662
3713
|
return
|
|
3663
3714
|
}
|
|
3664
|
-
const
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
} else {
|
|
3670
|
-
byKey.set(key, {
|
|
3671
|
-
id: id ?? (field === undefined ? 'domains' : `rowData-${field}`),
|
|
3672
|
-
title: field ?? 'Domains',
|
|
3673
|
-
entries: [...entries],
|
|
3674
|
-
})
|
|
3715
|
+
const base =
|
|
3716
|
+
overlay || key === undefined ? 'domains' : `rowData-${title}`
|
|
3717
|
+
let id = base
|
|
3718
|
+
for (let n = 2; ids.has(id); n++) {
|
|
3719
|
+
id = `${base}-${n}`
|
|
3675
3720
|
}
|
|
3721
|
+
ids.add(id)
|
|
3722
|
+
byKey.set(k, { id, title: title ?? 'Domains', entries })
|
|
3676
3723
|
}
|
|
3677
3724
|
// the overlay draws the domain key; with no columns it draws nothing,
|
|
3678
3725
|
// and a features panel taking the same colors lists them instead
|
|
3679
3726
|
if (self.actuallyShowDomains && self.numColumns > 0) {
|
|
3680
3727
|
add(
|
|
3681
|
-
featureFillEncoding?.
|
|
3728
|
+
featureFillEncoding?.key,
|
|
3729
|
+
featureFillEncoding?.title,
|
|
3682
3730
|
featureFillEncoding
|
|
3683
3731
|
? featureFillEncoding.legend
|
|
3684
3732
|
: visibleDomainTypes.map(d => ({
|
|
@@ -3686,16 +3734,16 @@ function stateModelFactory({
|
|
|
3686
3734
|
label: d.name,
|
|
3687
3735
|
color: d.color ?? fillPalette[d.accession]!,
|
|
3688
3736
|
})),
|
|
3689
|
-
|
|
3737
|
+
true,
|
|
3690
3738
|
)
|
|
3691
3739
|
}
|
|
3692
|
-
for (const { channel,
|
|
3740
|
+
for (const { channel, key, title, legend } of this.resolvedEncodings) {
|
|
3693
3741
|
if (!featureChannels.has(channel)) {
|
|
3694
|
-
add(
|
|
3742
|
+
add(key, title, legend)
|
|
3695
3743
|
}
|
|
3696
3744
|
}
|
|
3697
|
-
for (const { legendTitle, legend } of this.rowPanelScales) {
|
|
3698
|
-
add(legendTitle, legend)
|
|
3745
|
+
for (const { legendKey, legendTitle, legend } of this.rowPanelScales) {
|
|
3746
|
+
add(legendKey, legendTitle, legend)
|
|
3699
3747
|
}
|
|
3700
3748
|
return [...byKey.values()]
|
|
3701
3749
|
},
|
|
@@ -4001,23 +4049,68 @@ function stateModelFactory({
|
|
|
4001
4049
|
* table or the encodings, never per row per frame.
|
|
4002
4050
|
*/
|
|
4003
4051
|
get resolvedEncodings(): ResolvedEncoding[] {
|
|
4004
|
-
const rows = Object.values(self.rowData)
|
|
4005
4052
|
return self.encodings.map(encoding => ({
|
|
4006
4053
|
...encoding,
|
|
4007
|
-
...
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
self.allAnnotations,
|
|
4012
|
-
self.filteredAnnotations,
|
|
4013
|
-
)
|
|
4014
|
-
: resolveScale(
|
|
4015
|
-
encoding.scale,
|
|
4016
|
-
rows.map(row => row?.[encoding.field]).filter(notEmpty),
|
|
4017
|
-
)),
|
|
4054
|
+
...this.scaleOf(
|
|
4055
|
+
encoding,
|
|
4056
|
+
featureChannels.has(encoding.channel) ? 'features' : 'rows',
|
|
4057
|
+
),
|
|
4018
4058
|
}))
|
|
4019
4059
|
},
|
|
4020
4060
|
|
|
4061
|
+
/**
|
|
4062
|
+
* #getter
|
|
4063
|
+
* every color object the encodings and the row panels declare, keyed
|
|
4064
|
+
* by declaration (`declarationKey`) and resolved once over the union
|
|
4065
|
+
* of the values its records' fields take, so a value two strips share
|
|
4066
|
+
* takes one color in both. A feature field reads every feature loaded,
|
|
4067
|
+
* so hiding a type in the filter dialog leaves the rest in their
|
|
4068
|
+
* colors, and its legend lists the values of the drawn features.
|
|
4069
|
+
*/
|
|
4070
|
+
get resolvedScales() {
|
|
4071
|
+
const rows = Object.values(self.rowData)
|
|
4072
|
+
const featureValues = (list: Annotation[], field: string) =>
|
|
4073
|
+
list.map(a => featureField(a, field)).filter(notEmpty)
|
|
4074
|
+
const use = (channel: ColorChannel, table: ScaleTable): ScaleUse => ({
|
|
4075
|
+
spec: channel,
|
|
4076
|
+
field: channel.field,
|
|
4077
|
+
table,
|
|
4078
|
+
...(table === 'rows'
|
|
4079
|
+
? {
|
|
4080
|
+
values: rows.map(row => row?.[channel.field]).filter(notEmpty),
|
|
4081
|
+
}
|
|
4082
|
+
: {
|
|
4083
|
+
values: featureValues(self.allAnnotations, channel.field),
|
|
4084
|
+
shown: featureValues(self.filteredAnnotations, channel.field),
|
|
4085
|
+
}),
|
|
4086
|
+
})
|
|
4087
|
+
return resolveShared([
|
|
4088
|
+
...self.encodings.map(encoding =>
|
|
4089
|
+
use(
|
|
4090
|
+
encoding,
|
|
4091
|
+
featureChannels.has(encoding.channel) ? 'features' : 'rows',
|
|
4092
|
+
),
|
|
4093
|
+
),
|
|
4094
|
+
...self.rowPanels.flatMap(panel =>
|
|
4095
|
+
panel.kind === 'strip'
|
|
4096
|
+
? [use(panel, 'rows')]
|
|
4097
|
+
: panel.encoding?.color
|
|
4098
|
+
? [use(panel.encoding.color, 'features')]
|
|
4099
|
+
: [],
|
|
4100
|
+
),
|
|
4101
|
+
])
|
|
4102
|
+
},
|
|
4103
|
+
|
|
4104
|
+
/**
|
|
4105
|
+
* #method
|
|
4106
|
+
* the shared scale a record's color object resolves to
|
|
4107
|
+
*/
|
|
4108
|
+
scaleOf(channel: ColorChannel, table: ScaleTable): SharedScale {
|
|
4109
|
+
return this.resolvedScales.get(
|
|
4110
|
+
declarationKey({ spec: channel, field: channel.field, table }),
|
|
4111
|
+
)!
|
|
4112
|
+
},
|
|
4113
|
+
|
|
4021
4114
|
/**
|
|
4022
4115
|
* #getter
|
|
4023
4116
|
* each row panel's scale resolved against the values its field takes,
|
|
@@ -4032,12 +4125,7 @@ function stateModelFactory({
|
|
|
4032
4125
|
if (panel.kind === 'features') {
|
|
4033
4126
|
const color = panel.encoding?.color
|
|
4034
4127
|
const encoding = color
|
|
4035
|
-
?
|
|
4036
|
-
color.field,
|
|
4037
|
-
color.scale,
|
|
4038
|
-
self.allAnnotations,
|
|
4039
|
-
annotations,
|
|
4040
|
-
)
|
|
4128
|
+
? { field: color.field, ...this.scaleOf(color, 'features') }
|
|
4041
4129
|
: featureFillEncoding
|
|
4042
4130
|
const label = panel.encoding?.label
|
|
4043
4131
|
return {
|
|
@@ -4045,7 +4133,8 @@ function stateModelFactory({
|
|
|
4045
4133
|
x: panel.x,
|
|
4046
4134
|
header: panel.header ?? '',
|
|
4047
4135
|
field: encoding?.field,
|
|
4048
|
-
|
|
4136
|
+
legendKey: encoding?.key,
|
|
4137
|
+
legendTitle: encoding?.title,
|
|
4049
4138
|
colors: color
|
|
4050
4139
|
? featureColorMap(annotations, encoding, fillPalette)
|
|
4051
4140
|
: this.featureColors,
|
|
@@ -4061,17 +4150,13 @@ function stateModelFactory({
|
|
|
4061
4150
|
})),
|
|
4062
4151
|
}
|
|
4063
4152
|
}
|
|
4064
|
-
const { colorOf, legend } =
|
|
4065
|
-
panel.scale,
|
|
4066
|
-
Object.values(rowData)
|
|
4067
|
-
.map(row => row?.[panel.field])
|
|
4068
|
-
.filter(notEmpty),
|
|
4069
|
-
)
|
|
4153
|
+
const { colorOf, legend, key, title } = this.scaleOf(panel, 'rows')
|
|
4070
4154
|
return {
|
|
4071
4155
|
kind: panel.kind,
|
|
4072
4156
|
field: panel.field,
|
|
4073
4157
|
header: panel.header ?? panel.field,
|
|
4074
|
-
|
|
4158
|
+
legendKey: key,
|
|
4159
|
+
legendTitle: title,
|
|
4075
4160
|
colors: colorByRow(rowData, panel.field, colorOf),
|
|
4076
4161
|
legend,
|
|
4077
4162
|
}
|