react-msaview 5.4.1 → 5.5.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/LICENSE +21 -0
- package/bundle/index.js +465 -138
- package/bundle/index.js.map +7 -1
- package/dist/colorSchemes.js +11 -7
- package/dist/colorSchemes.js.map +1 -1
- package/dist/components/MSAViewer.d.ts +7 -1
- package/dist/components/MSAViewer.js +10 -7
- package/dist/components/MSAViewer.js.map +1 -1
- package/dist/components/Track.js +8 -5
- package/dist/components/Track.js.map +1 -1
- package/dist/components/dialogs/FeatureDialog.js +27 -23
- package/dist/components/dialogs/FeatureDialog.js.map +1 -1
- package/dist/components/header/MultiAlignmentSelector.js +0 -0
- package/dist/components/header/MultiAlignmentSelector.js.map +1 -1
- package/dist/components/msa/DomainLegend.d.ts +6 -0
- package/dist/components/msa/DomainLegend.js +41 -0
- package/dist/components/msa/DomainLegend.js.map +1 -0
- package/dist/components/msa/MSACanvasBlock.js +17 -5
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/MSAPanel.js +3 -1
- package/dist/components/msa/MSAPanel.js.map +1 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js +58 -14
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSABlock.js +3 -3
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +32 -4
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/renderCtx.d.ts +1 -1
- package/dist/components/tracks/renderTracksSvg.js +3 -3
- package/dist/components/tracks/renderTracksSvg.js.map +1 -1
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +16 -0
- package/dist/constants.js.map +1 -1
- package/dist/createPaletteMap.d.ts +5 -2
- package/dist/createPaletteMap.js +15 -5
- package/dist/createPaletteMap.js.map +1 -1
- package/dist/launchInterProScan.d.ts +2 -0
- package/dist/launchInterProScan.js.map +1 -1
- package/dist/model.d.ts +96 -1
- package/dist/model.js +91 -3
- package/dist/model.js.map +1 -1
- package/dist/renderToSvg.js +104 -74
- package/dist/renderToSvg.js.map +1 -1
- package/dist/{webpack.js → umd.js} +1 -1
- package/dist/umd.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -6
- package/src/colorSchemes.ts +12 -7
- package/src/components/MSAViewer.tsx +30 -18
- package/src/components/Track.tsx +8 -5
- package/src/components/dialogs/FeatureDialog.tsx +62 -48
- package/src/components/header/MultiAlignmentSelector.tsx +0 -0
- package/src/components/msa/DomainLegend.tsx +74 -0
- package/src/components/msa/MSACanvasBlock.tsx +22 -4
- package/src/components/msa/MSAPanel.tsx +2 -0
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +73 -17
- package/src/components/msa/renderMSABlock.ts +3 -3
- package/src/components/msa/renderMSAMouseover.ts +32 -4
- package/src/components/renderCtx.ts +1 -0
- package/src/components/tracks/renderTracksSvg.ts +3 -3
- package/src/constants.ts +18 -0
- package/src/createPaletteMap.ts +18 -5
- package/src/launchInterProScan.ts +3 -1
- package/src/model.ts +105 -2
- package/src/renderDomainLegendSvg.test.tsx +134 -0
- package/src/renderToSvg.tsx +210 -147
- package/src/segmentOverlay.test.ts +70 -0
- package/src/version.ts +1 -1
- package/bundle/index.js.LICENSE.txt +0 -100
- package/dist/components/util.d.ts +0 -1
- package/dist/components/util.js +0 -7
- package/dist/components/util.js.map +0 -1
- package/dist/webpack.js.map +0 -1
- package/src/components/util.ts +0 -9
- /package/dist/{webpack.d.ts → umd.d.ts} +0 -0
- /package/src/{webpack.ts → umd.ts} +0 -0
package/src/createPaletteMap.ts
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
|
+
import { colord } from 'colord'
|
|
2
|
+
|
|
1
3
|
import palettes from './ggplotPalettes.ts'
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
|
-
* Creates a map from keys to colors
|
|
5
|
-
*
|
|
6
|
+
* Creates a map from keys to colors. Up to the largest ggplot palette (8 keys)
|
|
7
|
+
* we use the hand-picked ggplot colors; beyond that — e.g. a gene-structure
|
|
8
|
+
* overlay with one feature per exon — we generate evenly spaced HSL hues so
|
|
9
|
+
* every key still gets a distinct, stable color instead of running off the end
|
|
10
|
+
* of the palette.
|
|
6
11
|
*/
|
|
7
12
|
export function createPaletteMap(keys: string[]) {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
const n = keys.length
|
|
14
|
+
if (n <= palettes.length) {
|
|
15
|
+
const palette = palettes[n - 1]!
|
|
16
|
+
return Object.fromEntries(keys.map((key, i) => [key, palette[i]!]))
|
|
17
|
+
}
|
|
18
|
+
return Object.fromEntries(
|
|
19
|
+
keys.map((key, i) => [
|
|
20
|
+
key,
|
|
21
|
+
colord({ h: (i * 360) / n, s: 60, l: 60 }).toHex(),
|
|
22
|
+
]),
|
|
23
|
+
)
|
|
11
24
|
}
|
|
@@ -13,9 +13,11 @@ export interface InterProScanResults {
|
|
|
13
13
|
name: string
|
|
14
14
|
description: string
|
|
15
15
|
accession: string
|
|
16
|
+
// original GFF feature type (exon, CDS, ...) when sourced from GFF
|
|
17
|
+
featureType?: string
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
|
-
locations: { start: number; end: number }[]
|
|
20
|
+
locations: { start: number; end: number; strand?: number }[]
|
|
19
21
|
}[]
|
|
20
22
|
xref: { id: string }[]
|
|
21
23
|
}
|
package/src/model.ts
CHANGED
|
@@ -43,6 +43,8 @@ import {
|
|
|
43
43
|
maxCellSize,
|
|
44
44
|
minColWidth,
|
|
45
45
|
minRowHeight,
|
|
46
|
+
segmentFeatureTypes,
|
|
47
|
+
segmentShades,
|
|
46
48
|
} from './constants.ts'
|
|
47
49
|
import { createPaletteMap } from './createPaletteMap.ts'
|
|
48
50
|
import { fetchTextWithProgress, isAbortError } from './fetchUtils.ts'
|
|
@@ -95,6 +97,14 @@ function parseTreeText(text: string) {
|
|
|
95
97
|
|
|
96
98
|
/**
|
|
97
99
|
* #stateModel MsaView
|
|
100
|
+
*
|
|
101
|
+
* The main MSAView state model. Holds the loaded alignment, tree, and optional
|
|
102
|
+
* InterProScan domain annotations, plus all display state (color scheme, zoom,
|
|
103
|
+
* scroll, collapsed clades). It composes in members from `DialogQueueSessionMixin`,
|
|
104
|
+
* `Tree`, and `MSAModel` (see Inherited members below). Data is loaded reactively
|
|
105
|
+
* from the `msaFilehandle` / `treeFilehandle` / `gffFilehandle` properties, or set
|
|
106
|
+
* directly with `setData`. Most state is persisted into the shareable URL.
|
|
107
|
+
*
|
|
98
108
|
* #example
|
|
99
109
|
* ```js
|
|
100
110
|
* import { MSAModelF } from 'react-msaview'
|
|
@@ -373,12 +383,15 @@ function stateModelFactory() {
|
|
|
373
383
|
.actions(self => ({
|
|
374
384
|
/**
|
|
375
385
|
* #action
|
|
386
|
+
* draw the alignment with positions numbered relative to the given row's
|
|
387
|
+
* sequence (its node id), instead of in raw MSA-column coordinates
|
|
376
388
|
*/
|
|
377
389
|
drawRelativeTo(id: string | undefined) {
|
|
378
390
|
self.relativeTo = id
|
|
379
391
|
},
|
|
380
392
|
/**
|
|
381
393
|
* #action
|
|
394
|
+
* hide columns that are entirely (or mostly, see allowedGappyness) gaps
|
|
382
395
|
*/
|
|
383
396
|
setHideGaps(arg: boolean) {
|
|
384
397
|
self.hideGaps = arg
|
|
@@ -457,6 +470,7 @@ function stateModelFactory() {
|
|
|
457
470
|
},
|
|
458
471
|
/**
|
|
459
472
|
* #action
|
|
473
|
+
* toggle the InterProScan protein-domain overlay on the alignment
|
|
460
474
|
*/
|
|
461
475
|
setShowDomains(arg: boolean) {
|
|
462
476
|
self.showDomains = arg
|
|
@@ -510,6 +524,7 @@ function stateModelFactory() {
|
|
|
510
524
|
|
|
511
525
|
/**
|
|
512
526
|
* #action
|
|
527
|
+
* collapse or un-collapse the subtree rooted at the given tree node id
|
|
513
528
|
*/
|
|
514
529
|
toggleCollapsed(node: string) {
|
|
515
530
|
if (self.collapsed.includes(node)) {
|
|
@@ -521,6 +536,8 @@ function stateModelFactory() {
|
|
|
521
536
|
|
|
522
537
|
/**
|
|
523
538
|
* #action
|
|
539
|
+
* show only the subtree rooted at the given node id (pass undefined to
|
|
540
|
+
* show the whole tree again)
|
|
524
541
|
*/
|
|
525
542
|
setShowOnly(node?: string) {
|
|
526
543
|
self.showOnly = node
|
|
@@ -528,6 +545,8 @@ function stateModelFactory() {
|
|
|
528
545
|
|
|
529
546
|
/**
|
|
530
547
|
* #action
|
|
548
|
+
* set the alignment/tree/metadata/domain data directly from strings,
|
|
549
|
+
* bypassing the filehandle loaders
|
|
531
550
|
*/
|
|
532
551
|
setData(data: {
|
|
533
552
|
msa?: string
|
|
@@ -1232,6 +1251,7 @@ function stateModelFactory() {
|
|
|
1232
1251
|
|
|
1233
1252
|
/**
|
|
1234
1253
|
* #action
|
|
1254
|
+
* restore the default column width and row height
|
|
1235
1255
|
*/
|
|
1236
1256
|
resetZoom() {
|
|
1237
1257
|
self.setColWidth(defaultColWidth)
|
|
@@ -1592,13 +1612,15 @@ function stateModelFactory() {
|
|
|
1592
1612
|
val.matches.flatMap(({ signature, locations }) => {
|
|
1593
1613
|
const { entry } = signature
|
|
1594
1614
|
return entry
|
|
1595
|
-
? locations.map(({ start, end }) => ({
|
|
1615
|
+
? locations.map(({ start, end, strand }) => ({
|
|
1596
1616
|
id,
|
|
1597
1617
|
name: entry.name,
|
|
1598
1618
|
accession: entry.accession,
|
|
1599
1619
|
description: entry.description,
|
|
1620
|
+
featureType: entry.featureType,
|
|
1600
1621
|
start,
|
|
1601
1622
|
end,
|
|
1623
|
+
strand,
|
|
1602
1624
|
}))
|
|
1603
1625
|
: []
|
|
1604
1626
|
}),
|
|
@@ -1630,8 +1652,38 @@ function stateModelFactory() {
|
|
|
1630
1652
|
get verticalScrollbarWidth() {
|
|
1631
1653
|
return self.showVerticalScrollbar ? 20 : 0
|
|
1632
1654
|
},
|
|
1655
|
+
/**
|
|
1656
|
+
* #getter
|
|
1657
|
+
* ordinal segment types (exons etc.), ordered by sequence position so
|
|
1658
|
+
* exon-1..exon-14 read left-to-right; colored by alternating shade and
|
|
1659
|
+
* labeled by number rather than each getting a distinct hue + legend row
|
|
1660
|
+
*/
|
|
1661
|
+
get segmentDomainTypes() {
|
|
1662
|
+
return [...self.tidyInterProAnnotationTypes.values()]
|
|
1663
|
+
.filter(d => segmentFeatureTypes.has(d.featureType ?? ''))
|
|
1664
|
+
.toSorted((a, b) => a.start - b.start)
|
|
1665
|
+
},
|
|
1666
|
+
/**
|
|
1667
|
+
* #getter
|
|
1668
|
+
* categorical feature types (InterPro domains and the like) that each get
|
|
1669
|
+
* their own color and a legend entry
|
|
1670
|
+
*/
|
|
1671
|
+
get categoricalDomainTypes() {
|
|
1672
|
+
return [...self.tidyInterProAnnotationTypes.values()].filter(
|
|
1673
|
+
d => !segmentFeatureTypes.has(d.featureType ?? ''),
|
|
1674
|
+
)
|
|
1675
|
+
},
|
|
1633
1676
|
get fillPalette() {
|
|
1634
|
-
|
|
1677
|
+
const segments = Object.fromEntries(
|
|
1678
|
+
this.segmentDomainTypes.map((d, i) => [
|
|
1679
|
+
d.accession,
|
|
1680
|
+
segmentShades[i % segmentShades.length]!,
|
|
1681
|
+
]),
|
|
1682
|
+
)
|
|
1683
|
+
const categorical = createPaletteMap(
|
|
1684
|
+
this.categoricalDomainTypes.map(d => d.accession),
|
|
1685
|
+
)
|
|
1686
|
+
return { ...segments, ...categorical }
|
|
1635
1687
|
},
|
|
1636
1688
|
get strokePalette() {
|
|
1637
1689
|
return transform(this.fillPalette, ([key, val]) => [
|
|
@@ -1640,6 +1692,57 @@ function stateModelFactory() {
|
|
|
1640
1692
|
])
|
|
1641
1693
|
},
|
|
1642
1694
|
|
|
1695
|
+
/**
|
|
1696
|
+
* #getter
|
|
1697
|
+
* accession -> number drawn on each segment band: the trailing number of
|
|
1698
|
+
* the feature name ("exon-3" -> "3"), else its 1-based position
|
|
1699
|
+
*/
|
|
1700
|
+
get segmentLabels() {
|
|
1701
|
+
return new Map(
|
|
1702
|
+
this.segmentDomainTypes.map((d, i) => {
|
|
1703
|
+
const m = /(\d+)\s*$/.exec(d.name)
|
|
1704
|
+
return [d.accession, m ? m[1]! : `${i + 1}`]
|
|
1705
|
+
}),
|
|
1706
|
+
)
|
|
1707
|
+
},
|
|
1708
|
+
|
|
1709
|
+
/**
|
|
1710
|
+
* #getter
|
|
1711
|
+
* the domain types currently drawn on the alignment (filtered-on), shared
|
|
1712
|
+
* by the on-screen legend and the SVG export legend. Ordinal segments
|
|
1713
|
+
* (exons) are excluded — they read as a numbered gene model, not a color
|
|
1714
|
+
* key — so this is the categorical types ordered by sequence position
|
|
1715
|
+
*/
|
|
1716
|
+
get visibleDomainTypes() {
|
|
1717
|
+
return this.categoricalDomainTypes
|
|
1718
|
+
.filter(d => self.featureFilters.get(d.accession))
|
|
1719
|
+
.toSorted((a, b) => a.start - b.start)
|
|
1720
|
+
},
|
|
1721
|
+
|
|
1722
|
+
/**
|
|
1723
|
+
* #getter
|
|
1724
|
+
* domain annotations under the mouse, hit-tested against the exact visible
|
|
1725
|
+
* column span each box is drawn at (so it matches the overlay across gaps)
|
|
1726
|
+
*/
|
|
1727
|
+
get mouseOverDomains() {
|
|
1728
|
+
const { mouseCol } = self
|
|
1729
|
+
const name = self.mouseOverRowName
|
|
1730
|
+
if (name !== undefined && mouseCol !== undefined) {
|
|
1731
|
+
const entry = self.tidyFilteredGatheredInterProAnnotations[name] ?? []
|
|
1732
|
+
return entry.filter(d => {
|
|
1733
|
+
const m1 = self.seqPosToVisibleCol(name, d.start - 1)
|
|
1734
|
+
const m2 = self.seqPosToVisibleCol(name, d.end)
|
|
1735
|
+
return (
|
|
1736
|
+
m1 !== undefined &&
|
|
1737
|
+
m2 !== undefined &&
|
|
1738
|
+
mouseCol >= m1 &&
|
|
1739
|
+
mouseCol < m2
|
|
1740
|
+
)
|
|
1741
|
+
})
|
|
1742
|
+
}
|
|
1743
|
+
return []
|
|
1744
|
+
},
|
|
1745
|
+
|
|
1643
1746
|
/**
|
|
1644
1747
|
* #method
|
|
1645
1748
|
*/
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// Verifies the domain color-key legend is drawn as a reserved column on the
|
|
4
|
+
// right of the SVG export when domains are shown. Polyfills mirror
|
|
5
|
+
// impgRender.test.tsx (svgcanvas needs DOMMatrix/DOMPoint + a measureText).
|
|
6
|
+
import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
|
|
7
|
+
import { enableStaticRendering } from 'mobx-react'
|
|
8
|
+
import { beforeAll, expect, test } from 'vitest'
|
|
9
|
+
|
|
10
|
+
import MSAModelF from './model.ts'
|
|
11
|
+
import { renderToSvg } from './renderToSvg.tsx'
|
|
12
|
+
|
|
13
|
+
class Mat {
|
|
14
|
+
constructor(
|
|
15
|
+
public a = 1,
|
|
16
|
+
public b = 0,
|
|
17
|
+
public c = 0,
|
|
18
|
+
public d = 1,
|
|
19
|
+
public e = 0,
|
|
20
|
+
public f = 0,
|
|
21
|
+
) {}
|
|
22
|
+
multiply(o: Mat) {
|
|
23
|
+
return new Mat(
|
|
24
|
+
this.a * o.a + this.c * o.b,
|
|
25
|
+
this.b * o.a + this.d * o.b,
|
|
26
|
+
this.a * o.c + this.c * o.d,
|
|
27
|
+
this.b * o.c + this.d * o.d,
|
|
28
|
+
this.a * o.e + this.c * o.f + this.e,
|
|
29
|
+
this.b * o.e + this.d * o.f + this.f,
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
translate(x: number, y = 0) {
|
|
33
|
+
return this.multiply(new Mat(1, 0, 0, 1, x, y))
|
|
34
|
+
}
|
|
35
|
+
scale(x: number, y = x) {
|
|
36
|
+
return this.multiply(new Mat(x, 0, 0, y, 0, 0))
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
class Pt {
|
|
40
|
+
constructor(
|
|
41
|
+
public x = 0,
|
|
42
|
+
public y = 0,
|
|
43
|
+
) {}
|
|
44
|
+
matrixTransform(m: Mat) {
|
|
45
|
+
return new Pt(
|
|
46
|
+
m.a * this.x + m.c * this.y + m.e,
|
|
47
|
+
m.b * this.x + m.d * this.y + m.f,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
beforeAll(() => {
|
|
53
|
+
enableStaticRendering(true)
|
|
54
|
+
const g = globalThis as Record<string, unknown>
|
|
55
|
+
g.DOMMatrix = Mat
|
|
56
|
+
g.DOMPoint = Pt
|
|
57
|
+
HTMLCanvasElement.prototype.getContext = function () {
|
|
58
|
+
let font = '10px sans-serif'
|
|
59
|
+
return {
|
|
60
|
+
get font() {
|
|
61
|
+
return font
|
|
62
|
+
},
|
|
63
|
+
set font(v: string) {
|
|
64
|
+
font = v
|
|
65
|
+
},
|
|
66
|
+
measureText(t: string) {
|
|
67
|
+
const size = Number.parseFloat(font) || 10
|
|
68
|
+
return { width: t.length * size * 0.6 } as TextMetrics
|
|
69
|
+
},
|
|
70
|
+
} as unknown as CanvasRenderingContext2D
|
|
71
|
+
} as unknown as typeof HTMLCanvasElement.prototype.getContext
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const msa = `>seq1
|
|
75
|
+
ACDEFGHIKL
|
|
76
|
+
>seq2
|
|
77
|
+
ACDE-GHIKL`
|
|
78
|
+
|
|
79
|
+
function makeModel() {
|
|
80
|
+
const model = MSAModelF().create({
|
|
81
|
+
id: 'domain-legend-test',
|
|
82
|
+
type: 'MsaView',
|
|
83
|
+
height: 400,
|
|
84
|
+
msaFormat: 'fasta',
|
|
85
|
+
data: { msa },
|
|
86
|
+
})
|
|
87
|
+
model.setWidth(800)
|
|
88
|
+
model.setDomains({
|
|
89
|
+
seq1: {
|
|
90
|
+
xref: [{ id: 'seq1' }],
|
|
91
|
+
matches: [
|
|
92
|
+
{
|
|
93
|
+
signature: {
|
|
94
|
+
entry: {
|
|
95
|
+
name: 'Kinase',
|
|
96
|
+
accession: 'PF00069',
|
|
97
|
+
description: 'Protein kinase domain',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
locations: [{ start: 1, end: 5 }],
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
return model
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
test('domain legend is drawn in the svg export when domains are shown', async () => {
|
|
109
|
+
const model = makeModel()
|
|
110
|
+
expect(model.actuallyShowDomains).toBe(true)
|
|
111
|
+
expect(model.visibleDomainTypes.map(d => d.accession)).toEqual(['PF00069'])
|
|
112
|
+
|
|
113
|
+
const svg = await renderToSvg(model, {
|
|
114
|
+
theme: createJBrowseTheme(),
|
|
115
|
+
exportType: 'entire',
|
|
116
|
+
includeMinimap: false,
|
|
117
|
+
includeTracks: false,
|
|
118
|
+
})
|
|
119
|
+
expect(svg).toContain('Kinase')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('no legend column when domains are hidden', async () => {
|
|
123
|
+
const model = makeModel()
|
|
124
|
+
model.setShowDomains(false)
|
|
125
|
+
expect(model.actuallyShowDomains).toBe(false)
|
|
126
|
+
|
|
127
|
+
const svg = await renderToSvg(model, {
|
|
128
|
+
theme: createJBrowseTheme(),
|
|
129
|
+
exportType: 'entire',
|
|
130
|
+
includeMinimap: false,
|
|
131
|
+
includeTracks: false,
|
|
132
|
+
})
|
|
133
|
+
expect(svg).not.toContain('Kinase')
|
|
134
|
+
})
|