react-msaview 9.0.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +19 -10
  2. package/bundle/index.js +74 -74
  3. package/bundle/index.js.map +4 -4
  4. package/dist/colorSchemes.js +1 -3
  5. package/dist/colorSchemes.js.map +1 -1
  6. package/dist/columnCounts.d.ts +0 -1
  7. package/dist/columnCounts.js +1 -1
  8. package/dist/columnCounts.js.map +1 -1
  9. package/dist/components/Loading.js +5 -5
  10. package/dist/components/Loading.js.map +1 -1
  11. package/dist/components/MSAViewer.d.ts +1891 -4
  12. package/dist/components/MSAViewer.js +144 -201
  13. package/dist/components/MSAViewer.js.map +1 -1
  14. package/dist/components/ResizeHandles.js +3 -2
  15. package/dist/components/ResizeHandles.js.map +1 -1
  16. package/dist/components/Track.js +2 -1
  17. package/dist/components/Track.js.map +1 -1
  18. package/dist/components/controls.d.ts +11 -0
  19. package/dist/components/controls.js +48 -0
  20. package/dist/components/controls.js.map +1 -0
  21. package/dist/components/dialogs/AnnotationFileDialog.js +5 -16
  22. package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -1
  23. package/dist/components/dialogs/ExportSVGDialog.js +5 -7
  24. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  25. package/dist/components/dialogs/HelpDialog.d.ts +6 -0
  26. package/dist/components/dialogs/HelpDialog.js +30 -0
  27. package/dist/components/dialogs/HelpDialog.js.map +1 -0
  28. package/dist/components/header/FileMenu.js +33 -7
  29. package/dist/components/header/FileMenu.js.map +1 -1
  30. package/dist/components/header/Header.js +4 -6
  31. package/dist/components/header/Header.js.map +1 -1
  32. package/dist/components/header/TreeSettingsMenu.d.ts +1 -1
  33. package/dist/components/header/TreeSettingsMenu.js +1 -32
  34. package/dist/components/header/TreeSettingsMenu.js.map +1 -1
  35. package/dist/components/header/ZoomControls.js +28 -44
  36. package/dist/components/header/ZoomControls.js.map +1 -1
  37. package/dist/components/header/settingsMenuItems.d.ts +1 -0
  38. package/dist/components/header/settingsMenuItems.js +44 -0
  39. package/dist/components/header/settingsMenuItems.js.map +1 -1
  40. package/dist/components/import/data/seq2.d.ts +0 -1
  41. package/dist/components/import/data/seq2.js +0 -17
  42. package/dist/components/import/data/seq2.js.map +1 -1
  43. package/dist/components/msa/MSACanvas.js +2 -1
  44. package/dist/components/msa/MSACanvas.js.map +1 -1
  45. package/dist/components/msa/MSACanvasBlock.js +2 -5
  46. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  47. package/dist/components/msa/drawFeatureSpans.d.ts +50 -11
  48. package/dist/components/msa/drawFeatureSpans.js +99 -56
  49. package/dist/components/msa/drawFeatureSpans.js.map +1 -1
  50. package/dist/components/msa/msaRaster.d.ts +14 -1
  51. package/dist/components/msa/msaRaster.js +19 -9
  52. package/dist/components/msa/msaRaster.js.map +1 -1
  53. package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +9 -0
  54. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +19 -8
  55. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  56. package/dist/components/msa/renderHighlights.d.ts +2 -2
  57. package/dist/components/msa/renderHighlights.js +2 -3
  58. package/dist/components/msa/renderHighlights.js.map +1 -1
  59. package/dist/components/msa/renderMSAMouseover.js +2 -1
  60. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  61. package/dist/components/renderCtx.d.ts +1 -1
  62. package/dist/components/rowpanels/renderFeaturePanel.d.ts +5 -1
  63. package/dist/components/rowpanels/renderFeaturePanel.js +12 -5
  64. package/dist/components/rowpanels/renderFeaturePanel.js.map +1 -1
  65. package/dist/components/tracks/drawTracks.d.ts +1 -53
  66. package/dist/components/tracks/drawTracks.js +5 -5
  67. package/dist/components/tracks/drawTracks.js.map +1 -1
  68. package/dist/components/tree/CladeLabels.d.ts +1 -1
  69. package/dist/components/tree/CladeLabels.js +22 -34
  70. package/dist/components/tree/CladeLabels.js.map +1 -1
  71. package/dist/components/tree/cladeBrackets.d.ts +17 -10
  72. package/dist/components/tree/cladeBrackets.js +33 -3
  73. package/dist/components/tree/cladeBrackets.js.map +1 -1
  74. package/dist/components/tree/renderTreeCanvas.js +2 -8
  75. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  76. package/dist/components/tree/renderTreeOverview.js +4 -11
  77. package/dist/components/tree/renderTreeOverview.js.map +1 -1
  78. package/dist/element.js +3 -6
  79. package/dist/element.js.map +1 -1
  80. package/dist/exportLayout.d.ts +39 -0
  81. package/dist/exportLayout.js +78 -0
  82. package/dist/exportLayout.js.map +1 -0
  83. package/dist/headlessRenderEnv.d.ts +12 -39
  84. package/dist/headlessRenderEnv.js +18 -12
  85. package/dist/headlessRenderEnv.js.map +1 -1
  86. package/dist/hierarchy.d.ts +5 -8
  87. package/dist/hierarchy.js +5 -79
  88. package/dist/hierarchy.js.map +1 -1
  89. package/dist/index.d.ts +3 -1
  90. package/dist/index.js +4 -1
  91. package/dist/index.js.map +1 -1
  92. package/dist/measureTextCanvas.d.ts +7 -0
  93. package/dist/measureTextCanvas.js +8 -6
  94. package/dist/measureTextCanvas.js.map +1 -1
  95. package/dist/model/treeModel.d.ts +0 -5
  96. package/dist/model/treeModel.js +0 -7
  97. package/dist/model/treeModel.js.map +1 -1
  98. package/dist/model.d.ts +38 -44
  99. package/dist/model.js +153 -112
  100. package/dist/model.js.map +1 -1
  101. package/dist/renderToSvg.d.ts +2 -7
  102. package/dist/renderToSvg.js +32 -120
  103. package/dist/renderToSvg.js.map +1 -1
  104. package/dist/svgTestUtil.d.ts +12 -20
  105. package/dist/types.d.ts +0 -1
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/dist/viewerSnapshot.d.ts +169 -0
  109. package/dist/viewerSnapshot.js +100 -0
  110. package/dist/viewerSnapshot.js.map +1 -0
  111. package/package.json +3 -3
  112. package/src/colorSchemes.ts +1 -4
  113. package/src/columnCounts.ts +1 -1
  114. package/src/components/Loading.tsx +19 -12
  115. package/src/components/MSAViewer.tsx +275 -251
  116. package/src/components/ResizeHandles.tsx +9 -2
  117. package/src/components/Track.tsx +2 -1
  118. package/src/components/controls.ts +61 -0
  119. package/src/components/dialogs/AnnotationFileDialog.tsx +5 -21
  120. package/src/components/dialogs/ExportSVGDialog.tsx +8 -19
  121. package/src/components/dialogs/HelpDialog.tsx +79 -0
  122. package/src/components/header/FileMenu.tsx +33 -7
  123. package/src/components/header/Header.tsx +4 -6
  124. package/src/components/header/TreeSettingsMenu.tsx +2 -32
  125. package/src/components/header/ZoomControls.tsx +30 -66
  126. package/src/components/header/settingsMenuItems.ts +59 -0
  127. package/src/components/import/data/seq2.ts +0 -18
  128. package/src/components/msa/MSACanvas.tsx +2 -1
  129. package/src/components/msa/MSACanvasBlock.tsx +2 -6
  130. package/src/components/msa/drawFeatureSpans.ts +160 -79
  131. package/src/components/msa/msaRaster.ts +27 -9
  132. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +39 -24
  133. package/src/components/msa/renderHighlights.ts +2 -3
  134. package/src/components/msa/renderMSAMouseover.ts +5 -1
  135. package/src/components/renderCtx.ts +0 -1
  136. package/src/components/rowpanels/renderFeaturePanel.ts +26 -7
  137. package/src/components/tracks/drawTracks.ts +5 -5
  138. package/src/components/tree/CladeLabels.tsx +30 -54
  139. package/src/components/tree/cladeBrackets.ts +50 -3
  140. package/src/components/tree/renderTreeCanvas.ts +2 -13
  141. package/src/components/tree/renderTreeOverview.ts +7 -14
  142. package/src/element.ts +3 -7
  143. package/src/exportLayout.ts +134 -0
  144. package/src/headlessRenderEnv.ts +24 -11
  145. package/src/hierarchy.ts +5 -88
  146. package/src/index.ts +5 -5
  147. package/src/measureTextCanvas.ts +8 -6
  148. package/src/model/treeModel.ts +0 -7
  149. package/src/model.ts +170 -122
  150. package/src/renderToSvg.tsx +55 -194
  151. package/src/types.ts +0 -1
  152. package/src/version.ts +1 -1
  153. package/src/viewerSnapshot.ts +147 -0
  154. package/dist/components/header/ZoomMenu.d.ts +0 -6
  155. package/dist/components/header/ZoomMenu.js +0 -37
  156. package/dist/components/header/ZoomMenu.js.map +0 -1
  157. package/src/components/header/ZoomMenu.tsx +0 -48
@@ -37,6 +37,42 @@ export interface SpanLayout {
37
37
  headRise?: (height: number) => number
38
38
  }
39
39
 
40
+ /**
41
+ * What a renderer hands the span mark: its rows, where a band lands in them,
42
+ * its x mapping, the colors and the label each feature takes.
43
+ */
44
+ export interface SpanSource<T extends SpanBand> {
45
+ rows: SpanRow<T>[]
46
+ layout: SpanLayout
47
+ xOf: (band: T) => [number, number]
48
+ colors: Map<Annotation, { fill: string; stroke: string }>
49
+ labelOf?: (band: T) => string | undefined
50
+ labelDrawn?: (band: T) => void
51
+ }
52
+
53
+ /** a span as drawn: a box, or an arrow's outline where it carries a strand */
54
+ export interface SpanShape<T extends SpanBand> {
55
+ band: T
56
+ fill: string
57
+ stroke: string
58
+ box?: { x: number; y: number; w: number; h: number }
59
+ outline?: [number, number][]
60
+ }
61
+
62
+ export interface SpanLabel {
63
+ text: string
64
+ x: number
65
+ y: number
66
+ fill: string
67
+ }
68
+
69
+ /** one row's shapes, then the labels drawn over them at `fontSize` */
70
+ export interface SpanMarkRow<T extends SpanBand> {
71
+ shapes: SpanShape<T>[]
72
+ labels: SpanLabel[]
73
+ fontSize: number
74
+ }
75
+
40
76
  /** the rows of `bandsByRow` that `leaves` covers, in display order */
41
77
  export function spanRows<T extends SpanBand>(
42
78
  leaves: HierarchyNode<NodeWithIdsAndLength>[],
@@ -53,56 +89,49 @@ export function spanRows<T extends SpanBand>(
53
89
  }
54
90
 
55
91
  /**
56
- * The span mark, in whatever x space the caller maps its bands into: alignment
57
- * columns for the domain overlay, panel pixels for a `features` row panel. A
58
- * feature with a strand draws as an arrow, one without as a box, and a label
59
- * draws inside the span wherever the text fits.
92
+ * The span mark as geometry, in whatever x space `xOf` maps its bands into:
93
+ * alignment columns for the domain overlay, panel pixels for a `features` row
94
+ * panel. A feature with a strand is an arrow, one without a box, and a label
95
+ * lands inside the span wherever `measureText` says the text fits.
60
96
  *
61
- * `xOf` returns the span's `[xStart, xEnd]` in the context's coordinates, and
62
- * the context is translated so that `y - rowHeight` is the row's top edge.
97
+ * `y - rowHeight` is the row's top edge in the coordinates `rows` gives.
63
98
  */
64
- export function drawFeatureSpans<T extends SpanBand>({
65
- ctx,
99
+ export function featureSpanMarks<T extends SpanBand>({
66
100
  rows,
67
101
  layout,
68
102
  xOf,
69
103
  colors,
70
104
  labelOf,
71
105
  labelDrawn,
72
- contrastText,
106
+ measureText,
73
107
  xMin,
74
108
  xMax,
75
- }: {
76
- ctx: RenderCtx
77
- rows: SpanRow<T>[]
78
- layout: SpanLayout
79
- xOf: (band: T) => [number, number]
80
- colors: Map<Annotation, { fill: string; stroke: string }>
81
- labelOf?: (band: T) => string | undefined
82
- labelDrawn?: (band: T) => void
83
- contrastText: (color: string) => string
109
+ }: SpanSource<T> & {
110
+ measureText: (text: string, fontSize: number) => number
84
111
  xMin: number
85
112
  xMax: number
86
- }) {
87
- // one row's spans are drawn before any of its labels. Bands are ordered
88
- // longest-first so a nested one lands on top, and drawing each label with its
89
- // own span put the child's box over the parent's text: `S` covered `RBD`, and
90
- // `RBD` read as `RB`. Reused across rows rather than allocated per row.
91
- const placed: {
92
- band: T
93
- xStart: number
94
- w: number
95
- t: number
96
- head: number
97
- fill: string
98
- }[] = []
99
-
113
+ }): SpanMarkRow<T>[] {
114
+ const out: SpanMarkRow<T>[] = []
100
115
  for (const { y, bands } of rows) {
101
116
  const h = layout.height(bands[0]!.laneCount)
102
117
  const headLen = layout.headLength(h)
103
118
  const rise = layout.headRise?.(h) ?? 0
104
119
  const labelled = labelOf !== undefined && h >= minFeatureLabelHeight
105
- placed.length = 0
120
+ const fontSize = Math.min(h - 2, 11)
121
+ const shapes: SpanShape<T>[] = []
122
+ const labels: SpanLabel[] = []
123
+ // one row's spans are drawn before any of its labels. Bands are ordered
124
+ // longest-first so a nested one lands on top, and drawing each label with
125
+ // its own span put the child's box over the parent's text: `S` covered
126
+ // `RBD`, and `RBD` read as `RB`.
127
+ const placed: {
128
+ band: T
129
+ xStart: number
130
+ w: number
131
+ t: number
132
+ head: number
133
+ fill: string
134
+ }[] = []
106
135
 
107
136
  for (const band of bands) {
108
137
  const [xStart, xEnd] = xOf(band)
@@ -112,55 +141,111 @@ export function drawFeatureSpans<T extends SpanBand>({
112
141
  }
113
142
  const t = layout.top(y, band.lane, h)
114
143
  const { fill, stroke } = colors.get(band.annotation)!
115
- ctx.fillStyle = fill
116
- ctx.strokeStyle = stroke
117
144
  const { strand } = band.annotation
118
145
  const head = strand === undefined ? 0 : Math.min(headLen, w)
119
- if (strand === undefined) {
120
- ctx.fillRect(xStart, t, w, h)
121
- ctx.strokeRect(xStart, t, w, h)
122
- } else {
123
- drawGeneArrow({ ctx, x: xStart, t, w, h, head, rise, strand })
124
- }
146
+ shapes.push(
147
+ strand === undefined
148
+ ? { band, fill, stroke, box: { x: xStart, y: t, w, h } }
149
+ : {
150
+ band,
151
+ fill,
152
+ stroke,
153
+ outline: geneArrow({ x: xStart, t, w, h, head, rise, strand }),
154
+ },
155
+ )
125
156
  if (labelled) {
126
157
  placed.push({ band, xStart, w, t, head, fill })
127
158
  }
128
159
  }
129
160
 
130
- if (placed.length > 0) {
131
- ctx.font = `${Math.min(h - 2, 11)}px sans-serif`
161
+ for (const { band, xStart, w, t, head, fill } of placed) {
162
+ // asking for the label here rather than in the pass above keeps the
163
+ // once-per-block segment numbering in drawing order
164
+ const text = labelOf!(band)
165
+ // the head narrows to a point, so only about half of it holds text
166
+ if (
167
+ text !== undefined &&
168
+ measureText(text, fontSize) + 2 <= w - head / 2
169
+ ) {
170
+ labelDrawn?.(band)
171
+ labels.push({ text, x: xStart + w / 2, y: t + h / 2, fill })
172
+ }
173
+ }
174
+ out.push({ shapes, labels, fontSize })
175
+ }
176
+ return out
177
+ }
178
+
179
+ /**
180
+ * The span mark, drawn into `ctx` from `featureSpanMarks`, with the context
181
+ * translated so that `y - rowHeight` is the row's top edge.
182
+ */
183
+ export function drawFeatureSpans<T extends SpanBand>({
184
+ ctx,
185
+ contrastText,
186
+ ...source
187
+ }: SpanSource<T> & {
188
+ ctx: RenderCtx
189
+ contrastText: (color: string) => string
190
+ xMin: number
191
+ xMax: number
192
+ }) {
193
+ let font = ''
194
+ const setFont = (fontSize: number) => {
195
+ const next = `${fontSize}px sans-serif`
196
+ if (next !== font) {
197
+ ctx.font = next
198
+ font = next
199
+ }
200
+ }
201
+ const rows = featureSpanMarks({
202
+ ...source,
203
+ measureText: (text, fontSize) => {
204
+ setFont(fontSize)
205
+ return ctx.measureText(text).width
206
+ },
207
+ })
208
+ for (const { shapes, labels, fontSize } of rows) {
209
+ for (const { fill, stroke, box, outline } of shapes) {
210
+ ctx.fillStyle = fill
211
+ ctx.strokeStyle = stroke
212
+ if (box) {
213
+ ctx.fillRect(box.x, box.y, box.w, box.h)
214
+ ctx.strokeRect(box.x, box.y, box.w, box.h)
215
+ } else if (outline) {
216
+ ctx.beginPath()
217
+ ctx.moveTo(...outline[0]!)
218
+ for (let i = 1; i < outline.length; i++) {
219
+ ctx.lineTo(...outline[i]!)
220
+ }
221
+ ctx.closePath()
222
+ ctx.fill()
223
+ ctx.stroke()
224
+ }
225
+ }
226
+ if (labels.length > 0) {
227
+ setFont(fontSize)
132
228
  ctx.textAlign = 'center'
133
229
  ctx.textBaseline = 'middle'
134
- for (const { band, xStart, w, t, head, fill } of placed) {
135
- // asking for the label here rather than in the pass above keeps the
136
- // once-per-block segment numbering in drawing order
137
- const label = labelOf!(band)
138
- // the head narrows to a point, so only about half of it holds text
139
- if (
140
- label !== undefined &&
141
- ctx.measureText(label).width + 2 <= w - head / 2
142
- ) {
143
- labelDrawn?.(band)
144
- ctx.fillStyle = contrastText(fill)
145
- ctx.fillText(label, xStart + w / 2, t + h / 2)
146
- }
230
+ for (const { text, x, y, fill } of labels) {
231
+ ctx.fillStyle = contrastText(fill)
232
+ ctx.fillText(text, x, y)
147
233
  }
148
234
  }
149
235
  }
150
236
  }
151
237
 
152
238
  /**
153
- * A gene arrow, drawn the way gggenes and gggenomes draw one: the head is the
154
- * last `head` pixels *of* the feature, tapering to a point at its end, and a
155
- * feature shorter than the head is all head. The glyph therefore covers its
239
+ * A gene arrow's outline, the way gggenes and gggenomes draw one: the head is
240
+ * the last `head` pixels *of* the feature, tapering to a point at its end, and
241
+ * a feature shorter than the head is all head. The glyph therefore covers its
156
242
  * start..end span and nothing else, so adjacent genes butt together instead of
157
243
  * biting triangles out of each other, a neighbour never draws over a label, and
158
244
  * a gene 20 columns wide reads as 20 columns wide.
159
245
  *
160
246
  * `rise` lifts the head above the band, for a bar too thin to taper.
161
247
  */
162
- function drawGeneArrow({
163
- ctx,
248
+ function geneArrow({
164
249
  x,
165
250
  t,
166
251
  w,
@@ -169,7 +254,6 @@ function drawGeneArrow({
169
254
  rise,
170
255
  strand,
171
256
  }: {
172
- ctx: RenderCtx
173
257
  x: number
174
258
  t: number
175
259
  w: number
@@ -177,29 +261,26 @@ function drawGeneArrow({
177
261
  head: number
178
262
  rise: number
179
263
  strand: number
180
- }) {
264
+ }): [number, number][] {
181
265
  const tip = strand > 0 ? x + w : x
182
266
  const tail = strand > 0 ? x : x + w
183
267
  const flange = strand > 0 ? tip - head : tip + head
184
268
  const bottom = t + h
185
269
  const crown = t - rise
186
270
  const point = (crown + bottom) / 2
187
- ctx.beginPath()
188
271
  if (head >= w) {
189
- ctx.moveTo(tail, crown)
190
- ctx.lineTo(tip, point)
191
- ctx.lineTo(tail, bottom)
192
- } else {
193
- ctx.moveTo(tail, t)
194
- ctx.lineTo(flange, t)
195
- if (rise > 0) {
196
- ctx.lineTo(flange, crown)
197
- }
198
- ctx.lineTo(tip, point)
199
- ctx.lineTo(flange, bottom)
200
- ctx.lineTo(tail, bottom)
272
+ return [
273
+ [tail, crown],
274
+ [tip, point],
275
+ [tail, bottom],
276
+ ]
201
277
  }
202
- ctx.closePath()
203
- ctx.fill()
204
- ctx.stroke()
278
+ return [
279
+ [tail, t],
280
+ [flange, t],
281
+ ...(rise > 0 ? [[flange, crown] as [number, number]] : []),
282
+ [tip, point],
283
+ [flange, bottom],
284
+ [tail, bottom],
285
+ ]
205
286
  }
@@ -242,17 +242,30 @@ function usableRasterCtx(ctx: unknown): ctx is RasterCtx {
242
242
  )
243
243
  }
244
244
 
245
- function makeRasterCanvas(width: number, height: number) {
245
+ /**
246
+ * A canvas off the page, an OffscreenCanvas where there is one, with its 2d
247
+ * context, or undefined where the context fails `usable` (jsdom has none).
248
+ */
249
+ export function makeOffscreenCanvas<Ctx>(
250
+ width: number,
251
+ height: number,
252
+ usable: (ctx: unknown) => ctx is Ctx,
253
+ settings?: CanvasRenderingContext2DSettings,
254
+ ) {
246
255
  const canvas =
247
256
  typeof OffscreenCanvas !== 'undefined'
248
257
  ? new OffscreenCanvas(width, height)
249
258
  : typeof document !== 'undefined'
250
259
  ? Object.assign(document.createElement('canvas'), { width, height })
251
260
  : undefined
252
- const ctx = canvas?.getContext('2d', { willReadFrequently: true })
253
- return usableRasterCtx(ctx)
254
- ? { canvas: canvas as RasterCanvas, ctx }
255
- : undefined
261
+ const ctx = canvas?.getContext('2d', settings)
262
+ return usable(ctx) ? { canvas: canvas as RasterCanvas, ctx } : undefined
263
+ }
264
+
265
+ function makeRasterCanvas(width: number, height: number) {
266
+ return makeOffscreenCanvas(width, height, usableRasterCtx, {
267
+ willReadFrequently: true,
268
+ })
256
269
  }
257
270
 
258
271
  function paint(
@@ -272,11 +285,16 @@ function paint(
272
285
 
273
286
  let supported: boolean | undefined
274
287
 
275
- // jsdom, and any browser without a 2d context, has no raster to draw; the
276
- // per-cell painter in renderMSABlock stays the fallback
277
- export function rasterSupported() {
288
+ /**
289
+ * Whether the alignment background comes from the raster: true with
290
+ * background coloring on, the domain overlay off, and a 2d context that reads
291
+ * back, which jsdom lacks. renderMSABlock paints each cell otherwise.
292
+ */
293
+ export function rasterApplies(
294
+ model: Pick<MsaViewModel, 'actuallyShowDomains' | 'bgColor'>,
295
+ ) {
278
296
  supported ??= !!makeRasterCanvas(1, 1)
279
- return supported
297
+ return !model.actuallyShowDomains && model.bgColor && supported
280
298
  }
281
299
 
282
300
  interface RasterCache {
@@ -3,9 +3,11 @@ import { contrastTextFn } from '../../util.ts'
3
3
  import { getVisibleLeaves } from '../getVisibleLeaves.ts'
4
4
  import { drawFeatureSpans, spanRows } from './drawFeatureSpans.ts'
5
5
 
6
+ import type { HierarchyNode } from '../../hierarchy.ts'
6
7
  import type { MsaViewModel } from '../../model.ts'
7
- import type { DomainBand } from '../../types.ts'
8
+ import type { DomainBand, NodeWithIdsAndLength } from '../../types.ts'
8
9
  import type { RenderCtx } from '../renderCtx.ts'
10
+ import type { SpanSource } from './drawFeatureSpans.ts'
9
11
  import type { Theme } from '@mui/material'
10
12
 
11
13
  export function renderBoxFeatureCanvasBlock({
@@ -27,11 +29,41 @@ export function renderBoxFeatureCanvasBlock({
27
29
  blockSizeXOverride?: number
28
30
  blockSizeYOverride?: number
29
31
  }) {
32
+ const { blockSize, rowHeight, highResScaleFactor, actuallyShowDomains } =
33
+ model
34
+ if (!actuallyShowDomains) {
35
+ return
36
+ }
37
+ const k = highResScaleFactorOverride ?? highResScaleFactor
38
+ const bx = blockSizeXOverride ?? blockSize
39
+ const by = blockSizeYOverride ?? blockSize
40
+ ctx.resetTransform()
41
+ ctx.scale(k, k)
42
+ ctx.translate(-offsetX, rowHeight / 2 - offsetY)
43
+
44
+ drawFeatureSpans<DomainBand>({
45
+ ctx,
46
+ ...domainOverlaySource(
47
+ model,
48
+ getVisibleLeaves({ model, offsetY, blockSizeY: by }),
49
+ ),
50
+ contrastText: contrastTextFn(theme),
51
+ xMin: offsetX,
52
+ xMax: offsetX + bx,
53
+ })
54
+ }
55
+
56
+ /**
57
+ * The domain overlay's spans over `leaves`, in alignment pixels: filled rows,
58
+ * an underline bar, or thin sub-rows, as `domainUnderline` and
59
+ * `subFeatureRows` decide.
60
+ */
61
+ export function domainOverlaySource(
62
+ model: MsaViewModel,
63
+ leaves: HierarchyNode<NodeWithIdsAndLength>[],
64
+ ): SpanSource<DomainBand> {
30
65
  const {
31
- blockSize,
32
66
  rowHeight,
33
- highResScaleFactor,
34
- actuallyShowDomains,
35
67
  subFeatureRows,
36
68
  colWidth,
37
69
  featureColors,
@@ -41,16 +73,6 @@ export function renderBoxFeatureCanvasBlock({
41
73
  domainUnderline,
42
74
  domainBands,
43
75
  } = model
44
- if (!actuallyShowDomains) {
45
- return
46
- }
47
- const k = highResScaleFactorOverride ?? highResScaleFactor
48
- const bx = blockSizeXOverride ?? blockSize
49
- const by = blockSizeYOverride ?? blockSize
50
- ctx.resetTransform()
51
- ctx.scale(k, k)
52
- ctx.translate(-offsetX, rowHeight / 2 - offsetY)
53
-
54
76
  // the plain and underline modes give every band the same height; a sub-row
55
77
  // band is thinner, and how thin depends on how many lanes its row needs
56
78
  const barHeight = domainUnderline
@@ -67,12 +89,8 @@ export function renderBoxFeatureCanvasBlock({
67
89
  // carrying the gene model are often not the first ones in the alignment
68
90
  const labelled = drawSegmentLabels ? new Set<string>() : undefined
69
91
 
70
- drawFeatureSpans<DomainBand>({
71
- ctx,
72
- rows: spanRows(
73
- getVisibleLeaves({ model, offsetY, blockSizeY: by }),
74
- domainBands,
75
- ),
92
+ return {
93
+ rows: spanRows(leaves, domainBands),
76
94
  layout: {
77
95
  // sub-rows are thin, but a row whose lanes would spill onto the row below
78
96
  // shares out the row height between them instead
@@ -98,8 +116,5 @@ export function renderBoxFeatureCanvasBlock({
98
116
  ? segmentLabels.get(band.annotation.accession)
99
117
  : undefined),
100
118
  labelDrawn: band => labelled?.add(band.annotation.accession),
101
- contrastText: contrastTextFn(theme),
102
- xMin: offsetX,
103
- xMax: offsetX + bx,
104
- })
119
+ }
105
120
  }
@@ -215,8 +215,8 @@ export function renderSelection({
215
215
 
216
216
  /**
217
217
  * The overlay parts that come from the snapshot: the reference row's tint, a
218
- * bordered band per run of highlighted columns, the `highlights` layer and the
219
- * selected block. The live overlay draws these under its hover and click
218
+ * bordered band per run of highlighted columns and the `highlights` layer. The
219
+ * live overlay draws these under the selected block and its hover and click
220
220
  * bands; the SVG export draws only these.
221
221
  */
222
222
  export function renderPersistentHighlights({
@@ -255,5 +255,4 @@ export function renderPersistentHighlights({
255
255
  }
256
256
 
257
257
  renderHighlights({ ctx, model, theme, offsetX, offsetY, width, height })
258
- renderSelection({ ctx, model, offsetX, offsetY, height })
259
258
  }
@@ -1,5 +1,8 @@
1
1
  import { clickColor, hoverColor, multiRowHoverColor } from '../overlayColors.ts'
2
- import { renderPersistentHighlights } from './renderHighlights.ts'
2
+ import {
3
+ renderPersistentHighlights,
4
+ renderSelection,
5
+ } from './renderHighlights.ts'
3
6
 
4
7
  import type { MsaViewModel } from '../../model.ts'
5
8
  import type { Theme } from '@mui/material'
@@ -49,6 +52,7 @@ export function renderMouseover({
49
52
  width,
50
53
  height,
51
54
  })
55
+ renderSelection({ ctx, model, offsetX: -scrollX, offsetY: -scrollY, height })
52
56
 
53
57
  // every tip under a hovered tree node stays lit
54
58
  ctx.fillStyle = multiRowHoverColor
@@ -8,7 +8,6 @@ export type RenderCtx = Pick<
8
8
  CanvasRenderingContext2D,
9
9
  | 'arc'
10
10
  | 'beginPath'
11
- | 'clearRect'
12
11
  | 'closePath'
13
12
  | 'fill'
14
13
  | 'fillRect'
@@ -2,8 +2,14 @@ import { contrastTextFn } from '../../util.ts'
2
2
  import { getVisibleLeaves } from '../getVisibleLeaves.ts'
3
3
  import { drawFeatureSpans, spanRows } from '../msa/drawFeatureSpans.ts'
4
4
 
5
+ import type { HierarchyNode } from '../../hierarchy.ts'
5
6
  import type { MsaViewModel } from '../../model.ts'
6
- import type { ResolvedFeaturePanel, RowPanelSpan } from '../../types.ts'
7
+ import type {
8
+ NodeWithIdsAndLength,
9
+ ResolvedFeaturePanel,
10
+ RowPanelSpan,
11
+ } from '../../types.ts'
12
+ import type { SpanSource } from '../msa/drawFeatureSpans.ts'
7
13
  import type { RenderCtx } from '../renderCtx.ts'
8
14
  import type { Theme } from '@mui/material'
9
15
 
@@ -46,10 +52,26 @@ export function renderFeaturePanel({
46
52
 
47
53
  drawFeatureSpans<RowPanelSpan>({
48
54
  ctx,
49
- rows: spanRows(
55
+ ...featurePanelSource(
56
+ model,
57
+ panel,
50
58
  getVisibleLeaves({ model, offsetY, blockSizeY: by }),
51
- panel.spans,
52
59
  ),
60
+ contrastText: contrastTextFn(theme),
61
+ xMin: 0,
62
+ xMax: panel.width,
63
+ })
64
+ }
65
+
66
+ /** a features panel's spans over `leaves`, in panel pixels, a lane apiece */
67
+ export function featurePanelSource(
68
+ model: MsaViewModel,
69
+ panel: ResolvedFeaturePanel,
70
+ leaves: HierarchyNode<NodeWithIdsAndLength>[],
71
+ ): SpanSource<RowPanelSpan> {
72
+ const { rowHeight } = model
73
+ return {
74
+ rows: spanRows(leaves, panel.spans),
53
75
  layout: {
54
76
  height: laneCount => Math.max(1, rowHeight / laneCount - rowPadding),
55
77
  top: (y, lane, h) =>
@@ -59,8 +81,5 @@ export function renderFeaturePanel({
59
81
  xOf: span => [span.xStart, span.xEnd],
60
82
  colors: panel.colors,
61
83
  labelOf: span => panel.labels?.get(span.annotation),
62
- contrastText: contrastTextFn(theme),
63
- xMin: 0,
64
- xMax: panel.width,
65
- })
84
+ }
66
85
  }
@@ -9,7 +9,7 @@ import type { Arc, BasicTrack } from '../../types.ts'
9
9
  import type { RenderCtx } from '../renderCtx.ts'
10
10
  import type { Theme } from '@mui/material'
11
11
 
12
- export function drawConservationBars({
12
+ function drawConservationBars({
13
13
  ctx,
14
14
  values,
15
15
  color,
@@ -55,7 +55,7 @@ export function barTrackValues(model: MsaViewModel, trackId: string) {
55
55
 
56
56
  // An arc's apex scales with the square root of its span. Contact-map spans skew
57
57
  // short, and a linear scale flattens them along the baseline.
58
- export function drawArcs({
58
+ function drawArcs({
59
59
  ctx,
60
60
  arcs,
61
61
  color,
@@ -103,7 +103,7 @@ export function drawArcs({
103
103
  }
104
104
  }
105
105
 
106
- export function drawTextTrackContent({
106
+ function drawTextTrackContent({
107
107
  ctx,
108
108
  data,
109
109
  colorScheme,
@@ -176,7 +176,7 @@ export function rulerCols(xStart: number, end: number, colWidth: number) {
176
176
  const RULER_FONT_SIZE = 10
177
177
  const TICK_HEIGHT = 4
178
178
 
179
- export function drawColumnRuler({
179
+ function drawColumnRuler({
180
180
  ctx,
181
181
  numColumns,
182
182
  label,
@@ -229,7 +229,7 @@ export function drawColumnRuler({
229
229
  const CAP_HEIGHT_RATIO = 0.72
230
230
  const MIN_LETTER_PX = 3
231
231
 
232
- export function drawSequenceLogo({
232
+ function drawSequenceLogo({
233
233
  ctx,
234
234
  colStats,
235
235
  colorScheme,