react-msaview 5.4.1 → 5.6.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 +487 -149
- package/bundle/index.js.map +7 -1
- package/dist/colorSchemes.js +11 -7
- package/dist/colorSchemes.js.map +1 -1
- package/dist/components/ConservationTrack.js +20 -26
- package/dist/components/ConservationTrack.js.map +1 -1
- package/dist/components/MSAViewer.d.ts +13 -1
- package/dist/components/MSAViewer.js +13 -7
- package/dist/components/MSAViewer.js.map +1 -1
- package/dist/components/SequenceTextArea.js +1 -1
- package/dist/components/SequenceTextArea.js.map +1 -1
- package/dist/components/TextTrack.js +20 -27
- package/dist/components/TextTrack.js.map +1 -1
- package/dist/components/Track.js +9 -6
- 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/dialogs/TrackInfoDialog.js +1 -1
- package/dist/components/dialogs/TrackInfoDialog.js.map +1 -1
- package/dist/components/header/ColorSchemeMenu.js +1 -1
- package/dist/components/header/ColorSchemeMenu.js.map +1 -1
- package/dist/components/header/FileMenu.js +1 -16
- package/dist/components/header/FileMenu.js.map +1 -1
- package/dist/components/header/Header.js +6 -5
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/HeaderInfoArea.js +1 -1
- package/dist/components/header/HeaderInfoArea.js.map +1 -1
- package/dist/components/header/HeaderStatusArea.js +1 -1
- package/dist/components/header/HeaderStatusArea.js.map +1 -1
- package/dist/components/header/MSASettingsMenu.js +1 -1
- package/dist/components/header/MSASettingsMenu.js.map +1 -1
- package/dist/components/header/MultiAlignmentSelector.js +0 -0
- package/dist/components/header/MultiAlignmentSelector.js.map +1 -1
- package/dist/components/header/TreeSettingsMenu.js +1 -1
- package/dist/components/header/TreeSettingsMenu.js.map +1 -1
- package/dist/components/header/ZoomControls.js +14 -12
- package/dist/components/header/ZoomControls.js.map +1 -1
- package/dist/components/header/ZoomMenu.js +1 -10
- package/dist/components/header/ZoomMenu.js.map +1 -1
- package/dist/components/header/settingsMenuItems.js +9 -1
- package/dist/components/header/settingsMenuItems.js.map +1 -1
- package/dist/components/{header/HeaderMenu.d.ts → msa/ColumnStats.d.ts} +3 -3
- package/dist/components/msa/ColumnStats.js +42 -0
- package/dist/components/msa/ColumnStats.js.map +1 -0
- 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 +41 -29
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/MSAMouseoverCanvas.js +6 -12
- package/dist/components/msa/MSAMouseoverCanvas.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 +60 -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 +33 -6
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/overlayColors.d.ts +3 -0
- package/dist/components/overlayColors.js +11 -0
- package/dist/components/overlayColors.js.map +1 -0
- package/dist/components/renderCtx.d.ts +1 -1
- package/dist/components/tracks/renderTracksSvg.d.ts +6 -3
- package/dist/components/tracks/renderTracksSvg.js +20 -11
- package/dist/components/tracks/renderTracksSvg.js.map +1 -1
- package/dist/components/tree/TreeCanvas.js +31 -38
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +12 -47
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/clickMap.d.ts +24 -0
- package/dist/components/tree/clickMap.js +33 -0
- package/dist/components/tree/clickMap.js.map +1 -0
- package/dist/components/tree/renderTreeCanvas.d.ts +1 -21
- package/dist/components/tree/renderTreeCanvas.js +8 -7
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +17 -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/fetchUtils.d.ts +2 -1
- package/dist/fetchUtils.js +3 -2
- package/dist/fetchUtils.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/launchInterProScan.d.ts +2 -0
- package/dist/launchInterProScan.js.map +1 -1
- package/dist/model/msaModel.d.ts +25 -2
- package/dist/model/msaModel.js +15 -3
- package/dist/model/msaModel.js.map +1 -1
- package/dist/model/treeModel.d.ts +46 -5
- package/dist/model/treeModel.js +20 -7
- package/dist/model/treeModel.js.map +1 -1
- package/dist/model.d.ts +641 -133
- package/dist/model.js +191 -77
- package/dist/model.js.map +1 -1
- package/dist/propertyConservation.d.ts +7 -0
- package/dist/propertyConservation.js +66 -0
- package/dist/propertyConservation.js.map +1 -0
- package/dist/renderToSvg.js +104 -74
- package/dist/renderToSvg.js.map +1 -1
- package/dist/types.d.ts +4 -1
- package/dist/{webpack.js → umd.js} +1 -1
- package/dist/umd.js.map +1 -0
- package/dist/useCanvasAutorun.d.ts +2 -0
- package/dist/useCanvasAutorun.js +23 -0
- package/dist/useCanvasAutorun.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -8
- package/src/colorSchemes.docs.test.ts +55 -0
- package/src/colorSchemes.ts +12 -7
- package/src/columnStats.test.ts +94 -0
- package/src/components/ConservationTrack.tsx +17 -15
- package/src/components/MSAViewer.tsx +42 -18
- package/src/components/SequenceTextArea.tsx +1 -1
- package/src/components/TextTrack.tsx +7 -11
- package/src/components/Track.tsx +9 -6
- package/src/components/dialogs/FeatureDialog.tsx +62 -48
- package/src/components/dialogs/TrackInfoDialog.tsx +1 -1
- package/src/components/header/ColorSchemeMenu.tsx +1 -0
- package/src/components/header/FileMenu.tsx +1 -15
- package/src/components/header/Header.tsx +11 -8
- package/src/components/header/HeaderInfoArea.tsx +1 -1
- package/src/components/header/HeaderStatusArea.tsx +1 -1
- package/src/components/header/MSASettingsMenu.tsx +1 -0
- package/src/components/header/MultiAlignmentSelector.tsx +0 -0
- package/src/components/header/TreeSettingsMenu.tsx +1 -0
- package/src/components/header/ZoomControls.tsx +24 -17
- package/src/components/header/ZoomMenu.tsx +1 -10
- package/src/components/header/settingsMenuItems.ts +9 -1
- package/src/components/msa/ColumnStats.tsx +54 -0
- package/src/components/msa/DomainLegend.tsx +78 -0
- package/src/components/msa/MSACanvasBlock.tsx +46 -25
- package/src/components/msa/MSAMouseoverCanvas.tsx +10 -13
- package/src/components/msa/MSAPanel.tsx +2 -0
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +75 -17
- package/src/components/msa/renderMSABlock.ts +3 -3
- package/src/components/msa/renderMSAMouseover.ts +34 -6
- package/src/components/overlayColors.ts +12 -0
- package/src/components/renderCtx.ts +1 -0
- package/src/components/tracks/renderTracksSvg.ts +25 -11
- package/src/components/tree/TreeCanvas.tsx +47 -54
- package/src/components/tree/TreeCanvasBlock.tsx +10 -68
- package/src/components/tree/clickMap.ts +55 -0
- package/src/components/tree/renderTreeCanvas.ts +10 -34
- package/src/constants.ts +19 -0
- package/src/createPaletteMap.ts +18 -5
- package/src/fetchUtils.ts +6 -3
- package/src/index.ts +1 -1
- package/src/launchInterProScan.ts +3 -1
- package/src/model/msaModel.ts +21 -3
- package/src/model/treeModel.ts +21 -7
- package/src/model.ts +221 -119
- package/src/propertyConservation.test.ts +47 -0
- package/src/propertyConservation.ts +73 -0
- package/src/renderDomainLegendSvg.test.tsx +134 -0
- package/src/renderToSvg.tsx +210 -147
- package/src/segmentOverlay.test.ts +70 -0
- package/src/stripDefaultSnapshot.test.ts +54 -0
- package/src/types.ts +7 -1
- package/src/useCanvasAutorun.ts +28 -0
- package/src/version.ts +1 -1
- package/bundle/index.js.LICENSE.txt +0 -100
- package/dist/components/dialogs/SettingsDialog.d.ts +0 -7
- package/dist/components/dialogs/SettingsDialog.js +0 -100
- package/dist/components/dialogs/SettingsDialog.js.map +0 -1
- package/dist/components/header/HeaderMenu.js +0 -27
- package/dist/components/header/HeaderMenu.js.map +0 -1
- 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/dialogs/SettingsDialog.tsx +0 -234
- package/src/components/header/HeaderMenu.tsx +0 -38
- 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
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Disjoint physicochemical classes for the 20 standard amino acids. Property
|
|
2
|
+
// conservation asks a different question than identity conservation: a column
|
|
3
|
+
// can vary in exact residue yet stay within one property class (a conservative
|
|
4
|
+
// substitution), which often marks a functionally constrained site that plain
|
|
5
|
+
// identity conservation reports as variable.
|
|
6
|
+
//
|
|
7
|
+
// Histidine is grouped with the aromatics (it is only weakly/conditionally
|
|
8
|
+
// positive at physiological pH); this mirrors common substitution-class schemes.
|
|
9
|
+
const aminoPropertyClass: Record<string, string> = {
|
|
10
|
+
A: 'hydrophobic',
|
|
11
|
+
V: 'hydrophobic',
|
|
12
|
+
L: 'hydrophobic',
|
|
13
|
+
I: 'hydrophobic',
|
|
14
|
+
M: 'hydrophobic',
|
|
15
|
+
C: 'hydrophobic',
|
|
16
|
+
F: 'aromatic',
|
|
17
|
+
W: 'aromatic',
|
|
18
|
+
Y: 'aromatic',
|
|
19
|
+
H: 'aromatic',
|
|
20
|
+
K: 'positive',
|
|
21
|
+
R: 'positive',
|
|
22
|
+
D: 'negative',
|
|
23
|
+
E: 'negative',
|
|
24
|
+
S: 'polar',
|
|
25
|
+
T: 'polar',
|
|
26
|
+
N: 'polar',
|
|
27
|
+
Q: 'polar',
|
|
28
|
+
G: 'special',
|
|
29
|
+
P: 'special',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const numPropertyClasses = new Set(Object.values(aminoPropertyClass)).size
|
|
33
|
+
const maxPropertyEntropy = Math.log2(numPropertyClasses)
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Per-column property conservation using Shannon entropy over physicochemical
|
|
37
|
+
* classes rather than exact residues. Returns 0-1 where 1 = every non-gap
|
|
38
|
+
* residue shares one property class. Mirrors the identity-conservation formula
|
|
39
|
+
* so the two tracks are directly comparable: (1 - H/Hmax) * (1 - gapFraction).
|
|
40
|
+
*/
|
|
41
|
+
export function calculatePropertyConservation(
|
|
42
|
+
colStats: Record<string, number>[],
|
|
43
|
+
colStatsSums: number[],
|
|
44
|
+
) {
|
|
45
|
+
return colStats.map((stats, i) => {
|
|
46
|
+
const total = colStatsSums[i]
|
|
47
|
+
if (!total) {
|
|
48
|
+
return 0
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let nonGapTotal = 0
|
|
52
|
+
const classCounts: Record<string, number> = {}
|
|
53
|
+
for (const [letter, count] of Object.entries(stats)) {
|
|
54
|
+
const cls = aminoPropertyClass[letter]
|
|
55
|
+
if (cls) {
|
|
56
|
+
classCounts[cls] = (classCounts[cls] ?? 0) + count
|
|
57
|
+
nonGapTotal += count
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (nonGapTotal === 0) {
|
|
61
|
+
return 0
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let entropy = 0
|
|
65
|
+
for (const count of Object.values(classCounts)) {
|
|
66
|
+
const freq = count / nonGapTotal
|
|
67
|
+
entropy -= freq * Math.log2(freq)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const gapFraction = (total - nonGapTotal) / total
|
|
71
|
+
return Math.max(0, 1 - entropy / maxPropertyEntropy) * (1 - gapFraction)
|
|
72
|
+
})
|
|
73
|
+
}
|
|
@@ -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
|
+
})
|
package/src/renderToSvg.tsx
CHANGED
|
@@ -21,86 +21,119 @@ export interface ExportSvgOptions {
|
|
|
21
21
|
includeTracks?: boolean
|
|
22
22
|
exportType: 'entire' | 'viewport'
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
|
|
25
|
+
// domain-legend geometry, shared by the width calculation and the renderer so
|
|
26
|
+
// the reserved column on the right exactly fits the drawn legend
|
|
27
|
+
const LEGEND_PAD = 8
|
|
28
|
+
const LEGEND_ROW_H = 16
|
|
29
|
+
const LEGEND_SWATCH = 12
|
|
30
|
+
const LEGEND_FONT = 12
|
|
31
|
+
const LEGEND_CHAR_W = 7
|
|
32
|
+
|
|
33
|
+
function getLegendWidth(model: MsaViewModel) {
|
|
34
|
+
const names = model.visibleDomainTypes.map(d => d.name)
|
|
35
|
+
const maxLen = Math.max(0, ...names.map(n => n.length))
|
|
36
|
+
const w = LEGEND_PAD * 2 + LEGEND_SWATCH + 6 + maxLen * LEGEND_CHAR_W
|
|
37
|
+
return Math.min(360, Math.max(120, w))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// resolved sizes/offsets (in svg user units) for the chosen export, shared by
|
|
41
|
+
// every layer; 'entire' renders the whole alignment unscrolled, 'viewport'
|
|
42
|
+
// mirrors the live scroll position
|
|
43
|
+
interface Layout {
|
|
44
|
+
width: number
|
|
45
|
+
height: number
|
|
46
|
+
contentHeight: number
|
|
47
|
+
trackHeight: number
|
|
48
|
+
offsetX: number
|
|
49
|
+
offsetY: number
|
|
50
|
+
includeMinimap: boolean
|
|
51
|
+
legendWidth: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getLayout(model: MsaViewModel, opts: ExportSvgOptions): Layout {
|
|
26
55
|
const {
|
|
27
56
|
width,
|
|
28
57
|
height,
|
|
29
58
|
scrollX,
|
|
30
59
|
scrollY,
|
|
60
|
+
totalWidth,
|
|
61
|
+
totalHeight,
|
|
62
|
+
treeAreaWidth,
|
|
31
63
|
totalTrackAreaHeight,
|
|
32
64
|
minimapHeight,
|
|
33
65
|
} = model
|
|
34
|
-
const
|
|
35
|
-
const trackHeight = includeTracks ? totalTrackAreaHeight : 0
|
|
66
|
+
const trackHeight = opts.includeTracks ? totalTrackAreaHeight : 0
|
|
36
67
|
// the minimap reflects the live viewport scroll position, so it's only
|
|
37
68
|
// meaningful for a viewport export, never for the entire alignment
|
|
38
|
-
const includeMinimap = exportType === 'viewport' && !!opts.includeMinimap
|
|
69
|
+
const includeMinimap = opts.exportType === 'viewport' && !!opts.includeMinimap
|
|
70
|
+
const legendWidth =
|
|
71
|
+
model.actuallyShowDomains && model.visibleDomainTypes.length > 0
|
|
72
|
+
? getLegendWidth(model)
|
|
73
|
+
: 0
|
|
39
74
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
includeMinimap,
|
|
64
|
-
includeTracks,
|
|
65
|
-
})
|
|
75
|
+
// width stays content-only (the renderers derive msaAreaWidth from it); the
|
|
76
|
+
// legend occupies an extra column added at the svg root in MsaSvg
|
|
77
|
+
return opts.exportType === 'entire'
|
|
78
|
+
? {
|
|
79
|
+
width: totalWidth + treeAreaWidth,
|
|
80
|
+
height: totalHeight + trackHeight,
|
|
81
|
+
contentHeight: totalHeight,
|
|
82
|
+
trackHeight,
|
|
83
|
+
offsetX: 0,
|
|
84
|
+
offsetY: 0,
|
|
85
|
+
includeMinimap,
|
|
86
|
+
legendWidth,
|
|
87
|
+
}
|
|
88
|
+
: {
|
|
89
|
+
width,
|
|
90
|
+
height: height + (includeMinimap ? minimapHeight : 0) + trackHeight,
|
|
91
|
+
contentHeight: height,
|
|
92
|
+
trackHeight,
|
|
93
|
+
offsetX: -scrollX,
|
|
94
|
+
offsetY: -scrollY,
|
|
95
|
+
includeMinimap,
|
|
96
|
+
legendWidth,
|
|
97
|
+
}
|
|
66
98
|
}
|
|
67
99
|
|
|
68
|
-
async function
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
100
|
+
export async function renderToSvg(model: MsaViewModel, opts: ExportSvgOptions) {
|
|
101
|
+
await when(() => model.dataInitialized)
|
|
102
|
+
const { Context } = await import('@jbrowse/svgcanvas')
|
|
103
|
+
return renderToStaticMarkup(
|
|
104
|
+
<MsaSvg
|
|
105
|
+
model={model}
|
|
106
|
+
theme={opts.theme}
|
|
107
|
+
Context={Context}
|
|
108
|
+
layout={getLayout(model, opts)}
|
|
109
|
+
/>,
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function MsaSvg({
|
|
76
114
|
model,
|
|
77
|
-
|
|
78
|
-
|
|
115
|
+
theme,
|
|
116
|
+
Context,
|
|
117
|
+
layout,
|
|
79
118
|
}: {
|
|
80
|
-
width: number
|
|
81
|
-
height: number
|
|
82
|
-
contentHeight: number
|
|
83
|
-
trackHeight: number
|
|
84
|
-
offsetX: number
|
|
85
|
-
offsetY: number
|
|
86
|
-
theme: Theme
|
|
87
119
|
model: MsaViewModel
|
|
88
|
-
|
|
89
|
-
|
|
120
|
+
theme: Theme
|
|
121
|
+
Context: typeof ContextType
|
|
122
|
+
layout: Layout
|
|
90
123
|
}) {
|
|
91
|
-
const {
|
|
124
|
+
const { width, height, trackHeight, includeMinimap, legendWidth } = layout
|
|
92
125
|
const { treeAreaWidth, minimapHeight } = model
|
|
126
|
+
const totalWidth = width + legendWidth
|
|
127
|
+
const legendTop = includeMinimap ? minimapHeight : 0
|
|
93
128
|
|
|
94
|
-
const
|
|
129
|
+
const body = (
|
|
95
130
|
<>
|
|
96
|
-
{
|
|
131
|
+
{trackHeight > 0 ? (
|
|
97
132
|
<TrackRendering
|
|
98
133
|
Context={Context}
|
|
99
134
|
model={model}
|
|
100
135
|
theme={theme}
|
|
101
|
-
|
|
102
|
-
msaAreaWidth={width - treeAreaWidth}
|
|
103
|
-
trackHeight={trackHeight}
|
|
136
|
+
layout={layout}
|
|
104
137
|
/>
|
|
105
138
|
) : null}
|
|
106
139
|
<g
|
|
@@ -110,184 +143,214 @@ async function render({
|
|
|
110
143
|
Context={Context}
|
|
111
144
|
model={model}
|
|
112
145
|
theme={theme}
|
|
113
|
-
|
|
114
|
-
offsetY={offsetY}
|
|
115
|
-
msaAreaWidth={width - treeAreaWidth}
|
|
116
|
-
contentHeight={contentHeight}
|
|
146
|
+
layout={layout}
|
|
117
147
|
/>
|
|
118
148
|
</g>
|
|
119
149
|
</>
|
|
120
150
|
)
|
|
121
151
|
|
|
122
|
-
return
|
|
123
|
-
<
|
|
152
|
+
return (
|
|
153
|
+
<svg
|
|
154
|
+
width={totalWidth}
|
|
155
|
+
height={height}
|
|
156
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
157
|
+
xmlnsXlink="http://www.w3.org/1999/xlink"
|
|
158
|
+
viewBox={`0 0 ${totalWidth} ${height}`}
|
|
159
|
+
>
|
|
160
|
+
<rect width="100%" height="100%" fill="white" />
|
|
124
161
|
{includeMinimap ? (
|
|
125
162
|
<>
|
|
126
163
|
<g transform={`translate(${treeAreaWidth} 0)`}>
|
|
127
164
|
<MinimapSVG model={model} />
|
|
128
165
|
</g>
|
|
129
|
-
<g transform={`translate(0 ${minimapHeight})`}>{
|
|
166
|
+
<g transform={`translate(0 ${minimapHeight})`}>{body}</g>
|
|
130
167
|
</>
|
|
131
168
|
) : (
|
|
132
|
-
|
|
169
|
+
body
|
|
133
170
|
)}
|
|
134
|
-
|
|
171
|
+
{legendWidth > 0 ? (
|
|
172
|
+
<g transform={`translate(${width} ${legendTop})`}>
|
|
173
|
+
<LegendSVG model={model} width={legendWidth} />
|
|
174
|
+
</g>
|
|
175
|
+
) : null}
|
|
176
|
+
</svg>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// the domain color key drawn as a reserved column to the right of the
|
|
181
|
+
// alignment, mirroring the on-screen DomainLegend overlay
|
|
182
|
+
function LegendSVG({ model, width }: { model: MsaViewModel; width: number }) {
|
|
183
|
+
const { visibleDomainTypes, fillPalette, strokePalette } = model
|
|
184
|
+
const boxHeight = LEGEND_PAD * 2 + visibleDomainTypes.length * LEGEND_ROW_H
|
|
185
|
+
return (
|
|
186
|
+
<g>
|
|
187
|
+
<rect
|
|
188
|
+
x={0}
|
|
189
|
+
y={0}
|
|
190
|
+
width={width - 4}
|
|
191
|
+
height={boxHeight}
|
|
192
|
+
fill="white"
|
|
193
|
+
stroke="#ccc"
|
|
194
|
+
rx={2}
|
|
195
|
+
/>
|
|
196
|
+
{visibleDomainTypes.map((d, i) => {
|
|
197
|
+
const y = LEGEND_PAD + i * LEGEND_ROW_H
|
|
198
|
+
return (
|
|
199
|
+
<g key={d.accession}>
|
|
200
|
+
<rect
|
|
201
|
+
x={LEGEND_PAD}
|
|
202
|
+
y={y}
|
|
203
|
+
width={LEGEND_SWATCH}
|
|
204
|
+
height={LEGEND_SWATCH}
|
|
205
|
+
fill={fillPalette[d.accession]}
|
|
206
|
+
stroke={strokePalette[d.accession]}
|
|
207
|
+
/>
|
|
208
|
+
<text
|
|
209
|
+
x={LEGEND_PAD + LEGEND_SWATCH + 6}
|
|
210
|
+
y={y + LEGEND_SWATCH - 1}
|
|
211
|
+
fontSize={LEGEND_FONT}
|
|
212
|
+
>
|
|
213
|
+
{d.name}
|
|
214
|
+
</text>
|
|
215
|
+
</g>
|
|
216
|
+
)
|
|
217
|
+
})}
|
|
218
|
+
</g>
|
|
135
219
|
)
|
|
136
220
|
}
|
|
137
221
|
|
|
138
222
|
function CoreRendering({
|
|
139
223
|
model,
|
|
140
224
|
theme,
|
|
141
|
-
|
|
142
|
-
contentHeight,
|
|
143
|
-
offsetX,
|
|
144
|
-
offsetY,
|
|
225
|
+
layout,
|
|
145
226
|
Context,
|
|
146
227
|
}: {
|
|
147
228
|
model: MsaViewModel
|
|
148
229
|
theme: Theme
|
|
149
|
-
|
|
150
|
-
contentHeight: number
|
|
151
|
-
offsetX: number
|
|
152
|
-
offsetY: number
|
|
230
|
+
layout: Layout
|
|
153
231
|
Context: typeof ContextType
|
|
154
232
|
}) {
|
|
233
|
+
const { contentHeight, offsetX, offsetY, width } = layout
|
|
155
234
|
const { treeAreaWidth, colorScheme, id } = model
|
|
156
|
-
const
|
|
157
|
-
const clipId2 = `msa-${id}`
|
|
235
|
+
const msaAreaWidth = width - treeAreaWidth
|
|
158
236
|
const contrastScheme = colorContrast(colorScheme, theme)
|
|
159
237
|
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
renderBoxFeatureCanvasBlock({
|
|
163
|
-
ctx: ctx2,
|
|
164
|
-
offsetX,
|
|
165
|
-
offsetY,
|
|
238
|
+
const treeCtx = new Context(treeAreaWidth, contentHeight)
|
|
239
|
+
renderTreeCanvas({
|
|
166
240
|
model,
|
|
241
|
+
theme,
|
|
242
|
+
ctx: treeCtx,
|
|
243
|
+
offsetY,
|
|
167
244
|
blockSizeYOverride: contentHeight,
|
|
168
245
|
highResScaleFactorOverride: 1,
|
|
169
246
|
})
|
|
170
|
-
|
|
247
|
+
|
|
248
|
+
const msaCtx = new Context(msaAreaWidth, contentHeight)
|
|
249
|
+
renderBoxFeatureCanvasBlock({
|
|
171
250
|
model,
|
|
251
|
+
ctx: msaCtx,
|
|
252
|
+
offsetX,
|
|
172
253
|
offsetY,
|
|
173
|
-
ctx: ctx1,
|
|
174
|
-
theme,
|
|
175
254
|
blockSizeYOverride: contentHeight,
|
|
176
255
|
highResScaleFactorOverride: 1,
|
|
177
256
|
})
|
|
178
257
|
renderMSABlock({
|
|
179
258
|
model,
|
|
180
259
|
theme,
|
|
181
|
-
|
|
182
|
-
offsetX,
|
|
260
|
+
ctx: msaCtx,
|
|
183
261
|
contrastScheme,
|
|
184
|
-
|
|
262
|
+
offsetX,
|
|
263
|
+
offsetY,
|
|
185
264
|
blockSizeXOverride: msaAreaWidth,
|
|
186
265
|
blockSizeYOverride: contentHeight,
|
|
187
266
|
highResScaleFactorOverride: 1,
|
|
188
267
|
})
|
|
268
|
+
|
|
189
269
|
return (
|
|
190
270
|
<>
|
|
191
|
-
<defs>
|
|
192
|
-
<clipPath id={clipId1}>
|
|
193
|
-
<rect x={0} y={0} width={treeAreaWidth} height={contentHeight} />
|
|
194
|
-
</clipPath>
|
|
195
|
-
<clipPath id={clipId2}>
|
|
196
|
-
<rect x={0} y={0} width={msaAreaWidth} height={contentHeight} />
|
|
197
|
-
</clipPath>
|
|
198
|
-
</defs>
|
|
199
|
-
|
|
200
|
-
<ClipGroup clipId={clipId1} html={ctx1.getSvg().innerHTML} />
|
|
201
271
|
<ClipGroup
|
|
202
|
-
clipId={
|
|
272
|
+
clipId={`tree-${id}`}
|
|
273
|
+
width={treeAreaWidth}
|
|
274
|
+
height={contentHeight}
|
|
275
|
+
ctx={treeCtx}
|
|
276
|
+
/>
|
|
277
|
+
<ClipGroup
|
|
278
|
+
clipId={`msa-${id}`}
|
|
279
|
+
width={msaAreaWidth}
|
|
280
|
+
height={contentHeight}
|
|
203
281
|
transform={`translate(${treeAreaWidth} 0)`}
|
|
204
|
-
|
|
282
|
+
ctx={msaCtx}
|
|
205
283
|
/>
|
|
206
284
|
</>
|
|
207
285
|
)
|
|
208
286
|
}
|
|
209
287
|
|
|
210
|
-
// Wraps SVG markup produced by a svgcanvas Context in a clipped group. The
|
|
211
|
-
// markup is injected verbatim since it's already serialized SVG, not React.
|
|
212
|
-
function ClipGroup({
|
|
213
|
-
clipId,
|
|
214
|
-
html,
|
|
215
|
-
transform,
|
|
216
|
-
}: {
|
|
217
|
-
clipId: string
|
|
218
|
-
html: string
|
|
219
|
-
transform?: string
|
|
220
|
-
}) {
|
|
221
|
-
return (
|
|
222
|
-
<g
|
|
223
|
-
clipPath={`url(#${clipId})`}
|
|
224
|
-
transform={transform}
|
|
225
|
-
dangerouslySetInnerHTML={{ __html: html }}
|
|
226
|
-
/>
|
|
227
|
-
)
|
|
228
|
-
}
|
|
229
|
-
|
|
230
288
|
function TrackRendering({
|
|
231
289
|
model,
|
|
232
290
|
theme,
|
|
233
|
-
|
|
234
|
-
trackHeight,
|
|
235
|
-
offsetX,
|
|
291
|
+
layout,
|
|
236
292
|
Context,
|
|
237
293
|
}: {
|
|
238
294
|
model: MsaViewModel
|
|
239
295
|
theme: Theme
|
|
240
|
-
|
|
241
|
-
trackHeight: number
|
|
242
|
-
offsetX: number
|
|
296
|
+
layout: Layout
|
|
243
297
|
Context: typeof ContextType
|
|
244
298
|
}) {
|
|
299
|
+
const { trackHeight, offsetX, width } = layout
|
|
245
300
|
const { treeAreaWidth, colorScheme, id } = model
|
|
246
|
-
const
|
|
301
|
+
const msaAreaWidth = width - treeAreaWidth
|
|
247
302
|
const contrastScheme = colorContrast(colorScheme, theme)
|
|
248
303
|
|
|
249
304
|
const ctx = new Context(msaAreaWidth, trackHeight)
|
|
250
|
-
|
|
251
305
|
renderAllTracks({
|
|
252
306
|
model,
|
|
253
307
|
ctx,
|
|
254
|
-
offsetX,
|
|
255
308
|
contrastScheme,
|
|
309
|
+
offsetX,
|
|
256
310
|
blockSizeXOverride: msaAreaWidth,
|
|
257
311
|
highResScaleFactorOverride: 1,
|
|
258
312
|
})
|
|
259
313
|
|
|
260
314
|
return (
|
|
261
315
|
<g transform={`translate(${treeAreaWidth} 0)`}>
|
|
262
|
-
<
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
316
|
+
<ClipGroup
|
|
317
|
+
clipId={`tracks-${id}`}
|
|
318
|
+
width={msaAreaWidth}
|
|
319
|
+
height={trackHeight}
|
|
320
|
+
ctx={ctx}
|
|
321
|
+
/>
|
|
268
322
|
</g>
|
|
269
323
|
)
|
|
270
324
|
}
|
|
271
325
|
|
|
272
|
-
|
|
326
|
+
// Clips a svgcanvas Context to its box and injects its markup verbatim (it is
|
|
327
|
+
// already serialized SVG, not React).
|
|
328
|
+
function ClipGroup({
|
|
329
|
+
clipId,
|
|
273
330
|
width,
|
|
274
331
|
height,
|
|
275
|
-
|
|
332
|
+
transform,
|
|
333
|
+
ctx,
|
|
276
334
|
}: {
|
|
335
|
+
clipId: string
|
|
277
336
|
width: number
|
|
278
337
|
height: number
|
|
279
|
-
|
|
338
|
+
transform?: string
|
|
339
|
+
ctx: ContextType
|
|
280
340
|
}) {
|
|
281
341
|
return (
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
342
|
+
<>
|
|
343
|
+
<defs>
|
|
344
|
+
<clipPath id={clipId}>
|
|
345
|
+
<rect x={0} y={0} width={width} height={height} />
|
|
346
|
+
</clipPath>
|
|
347
|
+
</defs>
|
|
348
|
+
<g
|
|
349
|
+
clipPath={`url(#${clipId})`}
|
|
350
|
+
transform={transform}
|
|
351
|
+
// eslint-disable-next-line @eslint-react/dom-no-dangerously-set-innerhtml
|
|
352
|
+
dangerouslySetInnerHTML={{ __html: ctx.getSvg().innerHTML }}
|
|
353
|
+
/>
|
|
354
|
+
</>
|
|
292
355
|
)
|
|
293
356
|
}
|