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.
Files changed (180) hide show
  1. package/LICENSE +21 -0
  2. package/bundle/index.js +487 -149
  3. package/bundle/index.js.map +7 -1
  4. package/dist/colorSchemes.js +11 -7
  5. package/dist/colorSchemes.js.map +1 -1
  6. package/dist/components/ConservationTrack.js +20 -26
  7. package/dist/components/ConservationTrack.js.map +1 -1
  8. package/dist/components/MSAViewer.d.ts +13 -1
  9. package/dist/components/MSAViewer.js +13 -7
  10. package/dist/components/MSAViewer.js.map +1 -1
  11. package/dist/components/SequenceTextArea.js +1 -1
  12. package/dist/components/SequenceTextArea.js.map +1 -1
  13. package/dist/components/TextTrack.js +20 -27
  14. package/dist/components/TextTrack.js.map +1 -1
  15. package/dist/components/Track.js +9 -6
  16. package/dist/components/Track.js.map +1 -1
  17. package/dist/components/dialogs/FeatureDialog.js +27 -23
  18. package/dist/components/dialogs/FeatureDialog.js.map +1 -1
  19. package/dist/components/dialogs/TrackInfoDialog.js +1 -1
  20. package/dist/components/dialogs/TrackInfoDialog.js.map +1 -1
  21. package/dist/components/header/ColorSchemeMenu.js +1 -1
  22. package/dist/components/header/ColorSchemeMenu.js.map +1 -1
  23. package/dist/components/header/FileMenu.js +1 -16
  24. package/dist/components/header/FileMenu.js.map +1 -1
  25. package/dist/components/header/Header.js +6 -5
  26. package/dist/components/header/Header.js.map +1 -1
  27. package/dist/components/header/HeaderInfoArea.js +1 -1
  28. package/dist/components/header/HeaderInfoArea.js.map +1 -1
  29. package/dist/components/header/HeaderStatusArea.js +1 -1
  30. package/dist/components/header/HeaderStatusArea.js.map +1 -1
  31. package/dist/components/header/MSASettingsMenu.js +1 -1
  32. package/dist/components/header/MSASettingsMenu.js.map +1 -1
  33. package/dist/components/header/MultiAlignmentSelector.js +0 -0
  34. package/dist/components/header/MultiAlignmentSelector.js.map +1 -1
  35. package/dist/components/header/TreeSettingsMenu.js +1 -1
  36. package/dist/components/header/TreeSettingsMenu.js.map +1 -1
  37. package/dist/components/header/ZoomControls.js +14 -12
  38. package/dist/components/header/ZoomControls.js.map +1 -1
  39. package/dist/components/header/ZoomMenu.js +1 -10
  40. package/dist/components/header/ZoomMenu.js.map +1 -1
  41. package/dist/components/header/settingsMenuItems.js +9 -1
  42. package/dist/components/header/settingsMenuItems.js.map +1 -1
  43. package/dist/components/{header/HeaderMenu.d.ts → msa/ColumnStats.d.ts} +3 -3
  44. package/dist/components/msa/ColumnStats.js +42 -0
  45. package/dist/components/msa/ColumnStats.js.map +1 -0
  46. package/dist/components/msa/DomainLegend.d.ts +6 -0
  47. package/dist/components/msa/DomainLegend.js +41 -0
  48. package/dist/components/msa/DomainLegend.js.map +1 -0
  49. package/dist/components/msa/MSACanvasBlock.js +41 -29
  50. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  51. package/dist/components/msa/MSAMouseoverCanvas.js +6 -12
  52. package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
  53. package/dist/components/msa/MSAPanel.js +3 -1
  54. package/dist/components/msa/MSAPanel.js.map +1 -1
  55. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +60 -14
  56. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  57. package/dist/components/msa/renderMSABlock.js +3 -3
  58. package/dist/components/msa/renderMSABlock.js.map +1 -1
  59. package/dist/components/msa/renderMSAMouseover.js +33 -6
  60. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  61. package/dist/components/overlayColors.d.ts +3 -0
  62. package/dist/components/overlayColors.js +11 -0
  63. package/dist/components/overlayColors.js.map +1 -0
  64. package/dist/components/renderCtx.d.ts +1 -1
  65. package/dist/components/tracks/renderTracksSvg.d.ts +6 -3
  66. package/dist/components/tracks/renderTracksSvg.js +20 -11
  67. package/dist/components/tracks/renderTracksSvg.js.map +1 -1
  68. package/dist/components/tree/TreeCanvas.js +31 -38
  69. package/dist/components/tree/TreeCanvas.js.map +1 -1
  70. package/dist/components/tree/TreeCanvasBlock.js +12 -47
  71. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  72. package/dist/components/tree/clickMap.d.ts +24 -0
  73. package/dist/components/tree/clickMap.js +33 -0
  74. package/dist/components/tree/clickMap.js.map +1 -0
  75. package/dist/components/tree/renderTreeCanvas.d.ts +1 -21
  76. package/dist/components/tree/renderTreeCanvas.js +8 -7
  77. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  78. package/dist/constants.d.ts +4 -0
  79. package/dist/constants.js +17 -0
  80. package/dist/constants.js.map +1 -1
  81. package/dist/createPaletteMap.d.ts +5 -2
  82. package/dist/createPaletteMap.js +15 -5
  83. package/dist/createPaletteMap.js.map +1 -1
  84. package/dist/fetchUtils.d.ts +2 -1
  85. package/dist/fetchUtils.js +3 -2
  86. package/dist/fetchUtils.js.map +1 -1
  87. package/dist/index.d.ts +1 -1
  88. package/dist/launchInterProScan.d.ts +2 -0
  89. package/dist/launchInterProScan.js.map +1 -1
  90. package/dist/model/msaModel.d.ts +25 -2
  91. package/dist/model/msaModel.js +15 -3
  92. package/dist/model/msaModel.js.map +1 -1
  93. package/dist/model/treeModel.d.ts +46 -5
  94. package/dist/model/treeModel.js +20 -7
  95. package/dist/model/treeModel.js.map +1 -1
  96. package/dist/model.d.ts +641 -133
  97. package/dist/model.js +191 -77
  98. package/dist/model.js.map +1 -1
  99. package/dist/propertyConservation.d.ts +7 -0
  100. package/dist/propertyConservation.js +66 -0
  101. package/dist/propertyConservation.js.map +1 -0
  102. package/dist/renderToSvg.js +104 -74
  103. package/dist/renderToSvg.js.map +1 -1
  104. package/dist/types.d.ts +4 -1
  105. package/dist/{webpack.js → umd.js} +1 -1
  106. package/dist/umd.js.map +1 -0
  107. package/dist/useCanvasAutorun.d.ts +2 -0
  108. package/dist/useCanvasAutorun.js +23 -0
  109. package/dist/useCanvasAutorun.js.map +1 -0
  110. package/dist/version.d.ts +1 -1
  111. package/dist/version.js +1 -1
  112. package/package.json +7 -8
  113. package/src/colorSchemes.docs.test.ts +55 -0
  114. package/src/colorSchemes.ts +12 -7
  115. package/src/columnStats.test.ts +94 -0
  116. package/src/components/ConservationTrack.tsx +17 -15
  117. package/src/components/MSAViewer.tsx +42 -18
  118. package/src/components/SequenceTextArea.tsx +1 -1
  119. package/src/components/TextTrack.tsx +7 -11
  120. package/src/components/Track.tsx +9 -6
  121. package/src/components/dialogs/FeatureDialog.tsx +62 -48
  122. package/src/components/dialogs/TrackInfoDialog.tsx +1 -1
  123. package/src/components/header/ColorSchemeMenu.tsx +1 -0
  124. package/src/components/header/FileMenu.tsx +1 -15
  125. package/src/components/header/Header.tsx +11 -8
  126. package/src/components/header/HeaderInfoArea.tsx +1 -1
  127. package/src/components/header/HeaderStatusArea.tsx +1 -1
  128. package/src/components/header/MSASettingsMenu.tsx +1 -0
  129. package/src/components/header/MultiAlignmentSelector.tsx +0 -0
  130. package/src/components/header/TreeSettingsMenu.tsx +1 -0
  131. package/src/components/header/ZoomControls.tsx +24 -17
  132. package/src/components/header/ZoomMenu.tsx +1 -10
  133. package/src/components/header/settingsMenuItems.ts +9 -1
  134. package/src/components/msa/ColumnStats.tsx +54 -0
  135. package/src/components/msa/DomainLegend.tsx +78 -0
  136. package/src/components/msa/MSACanvasBlock.tsx +46 -25
  137. package/src/components/msa/MSAMouseoverCanvas.tsx +10 -13
  138. package/src/components/msa/MSAPanel.tsx +2 -0
  139. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +75 -17
  140. package/src/components/msa/renderMSABlock.ts +3 -3
  141. package/src/components/msa/renderMSAMouseover.ts +34 -6
  142. package/src/components/overlayColors.ts +12 -0
  143. package/src/components/renderCtx.ts +1 -0
  144. package/src/components/tracks/renderTracksSvg.ts +25 -11
  145. package/src/components/tree/TreeCanvas.tsx +47 -54
  146. package/src/components/tree/TreeCanvasBlock.tsx +10 -68
  147. package/src/components/tree/clickMap.ts +55 -0
  148. package/src/components/tree/renderTreeCanvas.ts +10 -34
  149. package/src/constants.ts +19 -0
  150. package/src/createPaletteMap.ts +18 -5
  151. package/src/fetchUtils.ts +6 -3
  152. package/src/index.ts +1 -1
  153. package/src/launchInterProScan.ts +3 -1
  154. package/src/model/msaModel.ts +21 -3
  155. package/src/model/treeModel.ts +21 -7
  156. package/src/model.ts +221 -119
  157. package/src/propertyConservation.test.ts +47 -0
  158. package/src/propertyConservation.ts +73 -0
  159. package/src/renderDomainLegendSvg.test.tsx +134 -0
  160. package/src/renderToSvg.tsx +210 -147
  161. package/src/segmentOverlay.test.ts +70 -0
  162. package/src/stripDefaultSnapshot.test.ts +54 -0
  163. package/src/types.ts +7 -1
  164. package/src/useCanvasAutorun.ts +28 -0
  165. package/src/version.ts +1 -1
  166. package/bundle/index.js.LICENSE.txt +0 -100
  167. package/dist/components/dialogs/SettingsDialog.d.ts +0 -7
  168. package/dist/components/dialogs/SettingsDialog.js +0 -100
  169. package/dist/components/dialogs/SettingsDialog.js.map +0 -1
  170. package/dist/components/header/HeaderMenu.js +0 -27
  171. package/dist/components/header/HeaderMenu.js.map +0 -1
  172. package/dist/components/util.d.ts +0 -1
  173. package/dist/components/util.js +0 -7
  174. package/dist/components/util.js.map +0 -1
  175. package/dist/webpack.js.map +0 -1
  176. package/src/components/dialogs/SettingsDialog.tsx +0 -234
  177. package/src/components/header/HeaderMenu.tsx +0 -38
  178. package/src/components/util.ts +0 -9
  179. /package/dist/{webpack.d.ts → umd.d.ts} +0 -0
  180. /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
+ })
@@ -21,86 +21,119 @@ export interface ExportSvgOptions {
21
21
  includeTracks?: boolean
22
22
  exportType: 'entire' | 'viewport'
23
23
  }
24
- export async function renderToSvg(model: MsaViewModel, opts: ExportSvgOptions) {
25
- await when(() => model.dataInitialized)
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 { exportType, theme, includeTracks } = opts
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
- if (exportType === 'entire') {
41
- return render({
42
- width: model.totalWidth + model.treeAreaWidth,
43
- height: model.totalHeight + trackHeight,
44
- contentHeight: model.totalHeight,
45
- trackHeight,
46
- theme,
47
- model,
48
- offsetY: 0,
49
- offsetX: 0,
50
- includeMinimap,
51
- includeTracks,
52
- })
53
- }
54
- return render({
55
- width,
56
- height: height + (includeMinimap ? minimapHeight : 0) + trackHeight,
57
- contentHeight: height,
58
- trackHeight,
59
- theme,
60
- model,
61
- offsetY: -scrollY,
62
- offsetX: -scrollX,
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 render({
69
- width,
70
- height,
71
- contentHeight,
72
- trackHeight,
73
- offsetX,
74
- offsetY,
75
- theme,
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
- includeMinimap,
78
- includeTracks,
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
- includeMinimap?: boolean
89
- includeTracks?: boolean
120
+ theme: Theme
121
+ Context: typeof ContextType
122
+ layout: Layout
90
123
  }) {
91
- const { Context } = await import('@jbrowse/svgcanvas')
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 content = (
129
+ const body = (
95
130
  <>
96
- {includeTracks && trackHeight > 0 ? (
131
+ {trackHeight > 0 ? (
97
132
  <TrackRendering
98
133
  Context={Context}
99
134
  model={model}
100
135
  theme={theme}
101
- offsetX={offsetX}
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
- offsetX={offsetX}
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 renderToStaticMarkup(
123
- <SvgWrapper width={width} height={height}>
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})`}>{content}</g>
166
+ <g transform={`translate(0 ${minimapHeight})`}>{body}</g>
130
167
  </>
131
168
  ) : (
132
- content
169
+ body
133
170
  )}
134
- </SvgWrapper>,
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
- msaAreaWidth,
142
- contentHeight,
143
- offsetX,
144
- offsetY,
225
+ layout,
145
226
  Context,
146
227
  }: {
147
228
  model: MsaViewModel
148
229
  theme: Theme
149
- msaAreaWidth: number
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 clipId1 = `tree-${id}`
157
- const clipId2 = `msa-${id}`
235
+ const msaAreaWidth = width - treeAreaWidth
158
236
  const contrastScheme = colorContrast(colorScheme, theme)
159
237
 
160
- const ctx1 = new Context(treeAreaWidth, contentHeight)
161
- const ctx2 = new Context(msaAreaWidth, contentHeight)
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
- renderTreeCanvas({
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
- offsetY,
182
- offsetX,
260
+ ctx: msaCtx,
183
261
  contrastScheme,
184
- ctx: ctx2,
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={clipId2}
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
- html={ctx2.getSvg().innerHTML}
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
- msaAreaWidth,
234
- trackHeight,
235
- offsetX,
291
+ layout,
236
292
  Context,
237
293
  }: {
238
294
  model: MsaViewModel
239
295
  theme: Theme
240
- msaAreaWidth: number
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 clipId = `tracks-${id}`
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
- <defs>
263
- <clipPath id={clipId}>
264
- <rect x={0} y={0} width={msaAreaWidth} height={trackHeight} />
265
- </clipPath>
266
- </defs>
267
- <ClipGroup clipId={clipId} html={ctx.getSvg().innerHTML} />
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
- function SvgWrapper({
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
- children,
332
+ transform,
333
+ ctx,
276
334
  }: {
335
+ clipId: string
277
336
  width: number
278
337
  height: number
279
- children: React.ReactNode
338
+ transform?: string
339
+ ctx: ContextType
280
340
  }) {
281
341
  return (
282
- <svg
283
- width={width}
284
- height={height}
285
- xmlns="http://www.w3.org/2000/svg"
286
- xmlnsXlink="http://www.w3.org/1999/xlink"
287
- viewBox={`0 0 ${width} ${height}`}
288
- >
289
- <rect width="100%" height="100%" fill="white" />
290
- {children}
291
- </svg>
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
  }