react-msaview 6.5.0 → 7.0.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 (212) hide show
  1. package/README.md +6 -5
  2. package/bundle/index.js +100 -101
  3. package/bundle/index.js.map +4 -4
  4. package/dist/components/Loading.js +2 -2
  5. package/dist/components/Loading.js.map +1 -1
  6. package/dist/components/MSAViewer.d.ts +10 -2
  7. package/dist/components/MSAViewer.js +90 -2
  8. package/dist/components/MSAViewer.js.map +1 -1
  9. package/dist/components/ResizeHandles.d.ts +3 -3
  10. package/dist/components/ResizeHandles.js +28 -21
  11. package/dist/components/ResizeHandles.js.map +1 -1
  12. package/dist/components/dialogs/{InterProScanDialog.d.ts → AnnotationFileDialog.d.ts} +2 -2
  13. package/dist/components/dialogs/AnnotationFileDialog.js +92 -0
  14. package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -0
  15. package/dist/components/dialogs/FeatureDialog.js +13 -9
  16. package/dist/components/dialogs/FeatureDialog.js.map +1 -1
  17. package/dist/components/header/Header.js +4 -0
  18. package/dist/components/header/Header.js.map +1 -1
  19. package/dist/components/header/HeaderStatusArea.js +0 -2
  20. package/dist/components/header/HeaderStatusArea.js.map +1 -1
  21. package/dist/components/header/LoadWarnings.d.ts +6 -0
  22. package/dist/components/header/LoadWarnings.js +36 -0
  23. package/dist/components/header/LoadWarnings.js.map +1 -0
  24. package/dist/components/header/MultiAlignmentSelector.js +0 -0
  25. package/dist/components/header/MultiAlignmentSelector.js.map +1 -1
  26. package/dist/components/header/TreeSettingsMenu.js +9 -1
  27. package/dist/components/header/TreeSettingsMenu.js.map +1 -1
  28. package/dist/components/header/UnshareableDataWarning.d.ts +6 -0
  29. package/dist/components/header/UnshareableDataWarning.js +42 -0
  30. package/dist/components/header/UnshareableDataWarning.js.map +1 -0
  31. package/dist/components/header/getAnnotationsMenu.js +14 -14
  32. package/dist/components/header/getAnnotationsMenu.js.map +1 -1
  33. package/dist/components/import/ImportForm.js +1 -1
  34. package/dist/components/import/ImportForm.js.map +1 -1
  35. package/dist/components/import/ImportFormExamples.js +2 -6
  36. package/dist/components/import/ImportFormExamples.js.map +1 -1
  37. package/dist/components/import/util.js +3 -0
  38. package/dist/components/import/util.js.map +1 -1
  39. package/dist/components/msa/MSACanvasBlock.js +4 -5
  40. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  41. package/dist/components/msa/msaRaster.d.ts +13 -3
  42. package/dist/components/msa/msaRaster.js +173 -61
  43. package/dist/components/msa/msaRaster.js.map +1 -1
  44. package/dist/components/msa/renderHighlights.d.ts +39 -1
  45. package/dist/components/msa/renderHighlights.js +71 -6
  46. package/dist/components/msa/renderHighlights.js.map +1 -1
  47. package/dist/components/msa/renderMSABlock.d.ts +1 -2
  48. package/dist/components/msa/renderMSABlock.js +24 -28
  49. package/dist/components/msa/renderMSABlock.js.map +1 -1
  50. package/dist/components/msa/renderMSAMouseover.js +9 -30
  51. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  52. package/dist/components/renderCtx.d.ts +1 -1
  53. package/dist/components/tracks/TrackBlocks.js +10 -11
  54. package/dist/components/tracks/TrackBlocks.js.map +1 -1
  55. package/dist/components/tracks/drawTracks.d.ts +26 -8
  56. package/dist/components/tracks/drawTracks.js +123 -13
  57. package/dist/components/tracks/drawTracks.js.map +1 -1
  58. package/dist/components/tree/TreeCanvas.js +2 -3
  59. package/dist/components/tree/TreeCanvas.js.map +1 -1
  60. package/dist/components/tree/TreeCanvasBlock.js +4 -2
  61. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  62. package/dist/components/tree/TreeRuler.js +13 -3
  63. package/dist/components/tree/TreeRuler.js.map +1 -1
  64. package/dist/components/tree/renderTreeCanvas.d.ts +0 -1
  65. package/dist/components/tree/renderTreeCanvas.js +70 -30
  66. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  67. package/dist/components/tree/scaleBar.d.ts +10 -0
  68. package/dist/components/tree/scaleBar.js +24 -0
  69. package/dist/components/tree/scaleBar.js.map +1 -0
  70. package/dist/constants.d.ts +3 -1
  71. package/dist/constants.js +18 -1
  72. package/dist/constants.js.map +1 -1
  73. package/dist/fetchUtils.d.ts +7 -7
  74. package/dist/fetchUtils.js +4 -32
  75. package/dist/fetchUtils.js.map +1 -1
  76. package/dist/hierarchy.d.ts +2 -1
  77. package/dist/hierarchy.js +16 -10
  78. package/dist/hierarchy.js.map +1 -1
  79. package/dist/index.d.ts +2 -1
  80. package/dist/index.js +8 -2
  81. package/dist/index.js.map +1 -1
  82. package/dist/model/DataModel.js +5 -2
  83. package/dist/model/DataModel.js.map +1 -1
  84. package/dist/model.d.ts +454 -177
  85. package/dist/model.js +1180 -511
  86. package/dist/model.js.map +1 -1
  87. package/dist/renderToStaticMarkup.d.ts +11 -0
  88. package/dist/renderToStaticMarkup.js +30 -7
  89. package/dist/renderToStaticMarkup.js.map +1 -1
  90. package/dist/renderToSvg.js +25 -19
  91. package/dist/renderToSvg.js.map +1 -1
  92. package/dist/rowCoordinateCalculations.d.ts +25 -25
  93. package/dist/rowCoordinateCalculations.js +12 -36
  94. package/dist/rowCoordinateCalculations.js.map +1 -1
  95. package/dist/seqPosToGlobalCol.js +3 -1
  96. package/dist/seqPosToGlobalCol.js.map +1 -1
  97. package/dist/svgTestUtil.d.ts +144 -82
  98. package/dist/types.d.ts +97 -3
  99. package/dist/util.d.ts +11 -4
  100. package/dist/util.js +30 -10
  101. package/dist/util.js.map +1 -1
  102. package/dist/version.d.ts +1 -1
  103. package/dist/version.js +1 -1
  104. package/dist/wuss.d.ts +14 -0
  105. package/dist/wuss.js +55 -0
  106. package/dist/wuss.js.map +1 -0
  107. package/package.json +10 -8
  108. package/src/components/Loading.tsx +12 -2
  109. package/src/components/MSAViewer.tsx +106 -2
  110. package/src/components/ResizeHandles.tsx +31 -31
  111. package/src/components/dialogs/AnnotationFileDialog.tsx +162 -0
  112. package/src/components/dialogs/FeatureDialog.tsx +13 -9
  113. package/src/components/header/Header.tsx +4 -0
  114. package/src/components/header/HeaderStatusArea.tsx +0 -5
  115. package/src/components/header/LoadWarnings.tsx +49 -0
  116. package/src/components/header/MultiAlignmentSelector.tsx +0 -0
  117. package/src/components/header/TreeSettingsMenu.tsx +9 -1
  118. package/src/components/header/UnshareableDataWarning.tsx +58 -0
  119. package/src/components/header/getAnnotationsMenu.ts +16 -17
  120. package/src/components/import/ImportForm.tsx +2 -2
  121. package/src/components/import/ImportFormExamples.tsx +2 -6
  122. package/src/components/import/util.ts +3 -0
  123. package/src/components/msa/MSACanvasBlock.tsx +4 -5
  124. package/src/components/msa/msaRaster.ts +216 -59
  125. package/src/components/msa/renderHighlights.ts +103 -5
  126. package/src/components/msa/renderMSABlock.ts +25 -36
  127. package/src/components/msa/renderMSAMouseover.ts +9 -39
  128. package/src/components/renderCtx.ts +1 -0
  129. package/src/components/tracks/TrackBlocks.tsx +10 -13
  130. package/src/components/tracks/drawTracks.ts +165 -17
  131. package/src/components/tree/TreeCanvas.tsx +2 -3
  132. package/src/components/tree/TreeCanvasBlock.tsx +4 -2
  133. package/src/components/tree/TreeRuler.tsx +38 -3
  134. package/src/components/tree/renderTreeCanvas.ts +79 -35
  135. package/src/components/tree/scaleBar.ts +23 -0
  136. package/src/constants.ts +20 -1
  137. package/src/fetchUtils.ts +7 -40
  138. package/src/hierarchy.ts +20 -11
  139. package/src/index.ts +18 -2
  140. package/src/model/DataModel.ts +6 -2
  141. package/src/model.ts +1352 -616
  142. package/src/rawImports.d.ts +9 -0
  143. package/src/renderToStaticMarkup.ts +34 -7
  144. package/src/renderToSvg.tsx +33 -27
  145. package/src/rowCoordinateCalculations.ts +15 -59
  146. package/src/seqPosToGlobalCol.ts +3 -1
  147. package/src/types.ts +131 -4
  148. package/src/util.ts +31 -14
  149. package/src/version.ts +1 -1
  150. package/src/wuss.ts +62 -0
  151. package/dist/components/dialogs/InterProScanDialog.js +0 -137
  152. package/dist/components/dialogs/InterProScanDialog.js.map +0 -1
  153. package/dist/components/dialogs/InterProScanFileDialog.d.ts +0 -7
  154. package/dist/components/dialogs/InterProScanFileDialog.js +0 -64
  155. package/dist/components/dialogs/InterProScanFileDialog.js.map +0 -1
  156. package/dist/launchInterProScan.d.ts +0 -13
  157. package/dist/launchInterProScan.js +0 -110
  158. package/dist/launchInterProScan.js.map +0 -1
  159. package/dist/useColorContrast.d.ts +0 -10
  160. package/dist/useColorContrast.js +0 -13
  161. package/dist/useColorContrast.js.map +0 -1
  162. package/src/clustalX.test.ts +0 -74
  163. package/src/colSpace.test.ts +0 -67
  164. package/src/collapseLogic.test.ts +0 -115
  165. package/src/colorSchemes.docs.test.ts +0 -57
  166. package/src/columnCounts.test.ts +0 -73
  167. package/src/columnStats.test.ts +0 -94
  168. package/src/columnTracks.test.ts +0 -97
  169. package/src/components/dialogs/InterProScanDialog.tsx +0 -214
  170. package/src/components/dialogs/InterProScanFileDialog.tsx +0 -138
  171. package/src/components/minimap/minimapLayout.test.ts +0 -71
  172. package/src/components/msa/msaRaster.test.ts +0 -145
  173. package/src/components/msa/renderBoxFeatureCanvasBlock.test.ts +0 -51
  174. package/src/components/tree/renderTreeCanvas.test.ts +0 -296
  175. package/src/dataInitialized.test.ts +0 -42
  176. package/src/domainBands.test.ts +0 -122
  177. package/src/domainLetterColor.test.ts +0 -80
  178. package/src/exportFileName.test.ts +0 -40
  179. package/src/fetchUtils.test.ts +0 -83
  180. package/src/flatToTree.test.ts +0 -51
  181. package/src/hierarchy.test.ts +0 -228
  182. package/src/highlights.test.ts +0 -70
  183. package/src/impgRender.test.tsx +0 -68
  184. package/src/largeTree.test.ts +0 -67
  185. package/src/launchInterProScan.ts +0 -167
  186. package/src/loadErrorDisplay.test.tsx +0 -102
  187. package/src/modelFilehandleLoaders.test.ts +0 -137
  188. package/src/modelReset.test.ts +0 -120
  189. package/src/neighborJoining.test.ts +0 -164
  190. package/src/parseAsn1.test.ts +0 -36
  191. package/src/propertyConservation.test.ts +0 -36
  192. package/src/renderColumnTracksSvg.test.tsx +0 -108
  193. package/src/renderDomainsSvg.test.tsx +0 -118
  194. package/src/renderHighlightsSvg.test.tsx +0 -85
  195. package/src/renderMinimapSvg.test.tsx +0 -62
  196. package/src/renderRasterSvg.test.tsx +0 -144
  197. package/src/renderSequenceLogoSvg.test.tsx +0 -165
  198. package/src/renderToSvg.test.tsx +0 -65
  199. package/src/reset.test.ts +0 -61
  200. package/src/rowCoordinateCalculations.test.ts +0 -211
  201. package/src/rowSpace.test.ts +0 -46
  202. package/src/segmentOverlay.test.ts +0 -70
  203. package/src/seqPosToGlobalCol.test.ts +0 -59
  204. package/src/sequenceLogo.test.ts +0 -88
  205. package/src/setFontSize.test.ts +0 -33
  206. package/src/stripDefault.test.ts +0 -48
  207. package/src/stripDefaultSnapshot.test.ts +0 -87
  208. package/src/treeMetadata.test.ts +0 -39
  209. package/src/useCanvasAutorun.test.tsx +0 -98
  210. package/src/useColorContrast.ts +0 -18
  211. package/src/viewChrome.test.tsx +0 -134
  212. package/src/wheelZoom.test.ts +0 -46
@@ -63,6 +63,20 @@ export function cssColorToPixel(css: string) {
63
63
  return packed
64
64
  }
65
65
 
66
+ function cellPixelFn(spec: RasterSpec, toPixel: (css: string) => number) {
67
+ const { columns, relativeTo, colorAt, bg, hover } = spec
68
+ const bgPixel = toPixel(bg)
69
+ const hoverPixel = toPixel(hover)
70
+ const reference = relativeTo ? columns.get(relativeTo) : undefined
71
+ return (name: string, col: number, letter: string) => {
72
+ if (reference && name !== relativeTo && letter === reference[col]) {
73
+ return hoverPixel
74
+ }
75
+ const color = colorAt(col, letter)
76
+ return color === undefined ? bgPixel : toPixel(color)
77
+ }
78
+ }
79
+
66
80
  /**
67
81
  * Packs a rectangle of the alignment into one pixel per cell, in `rowNames`
68
82
  * order, ready to hand to putImageData. Cells past the end of a row, and rows
@@ -70,7 +84,9 @@ export function cssColorToPixel(css: string) {
70
84
  * also leaves behind.
71
85
  *
72
86
  * `colStep`/`rowStep` sample instead of covering, which is how the minimap fits
73
- * a whole alignment into a 12px bar.
87
+ * a whole alignment into a 12px bar. `colSpan`/`rowSpan` average each block of
88
+ * that many cells into one pixel instead, so a zoom below a device pixel per
89
+ * column still shows every column's color without blending neighbouring rows.
74
90
  */
75
91
  export function rasterPixels({
76
92
  spec,
@@ -80,7 +96,10 @@ export function rasterPixels({
80
96
  height,
81
97
  colStep = 1,
82
98
  rowStep = 1,
99
+ colSpan = 1,
100
+ rowSpan = 1,
83
101
  toPixel = cssColorToPixel,
102
+ out = new Uint32Array(width * height),
84
103
  }: {
85
104
  spec: RasterSpec
86
105
  col0: number
@@ -89,13 +108,27 @@ export function rasterPixels({
89
108
  height: number
90
109
  colStep?: number
91
110
  rowStep?: number
111
+ colSpan?: number
112
+ rowSpan?: number
92
113
  toPixel?: (css: string) => number
114
+ out?: Uint32Array
93
115
  }) {
94
- const { rowNames, columns, relativeTo, colorAt, bg, hover } = spec
95
- const out = new Uint32Array(width * height)
96
- const bgPixel = toPixel(bg)
97
- const hoverPixel = toPixel(hover)
98
- const reference = relativeTo ? columns.get(relativeTo) : undefined
116
+ if (colSpan > 1 || rowSpan > 1) {
117
+ averagePixels({
118
+ spec,
119
+ col0,
120
+ row0,
121
+ width,
122
+ height,
123
+ colSpan,
124
+ rowSpan,
125
+ toPixel,
126
+ out,
127
+ })
128
+ return out
129
+ }
130
+ const { rowNames, columns } = spec
131
+ const cellPixel = cellPixelFn(spec, toPixel)
99
132
 
100
133
  for (let r = 0; r < height; r++) {
101
134
  const name = rowNames[row0 + r * rowStep]
@@ -106,7 +139,6 @@ export function rasterPixels({
106
139
  if (!str) {
107
140
  continue
108
141
  }
109
- const isReference = name === relativeTo
110
142
  const base = r * width
111
143
  for (let c = 0; c < width; c++) {
112
144
  const col = col0 + c * colStep
@@ -114,21 +146,92 @@ export function rasterPixels({
114
146
  if (letter === undefined) {
115
147
  break
116
148
  }
117
- if (reference && !isReference && letter === reference[col]) {
118
- out[base + c] = hoverPixel
119
- } else {
120
- const color = colorAt(col, letter)
121
- out[base + c] = color === undefined ? bgPixel : toPixel(color)
122
- }
149
+ out[base + c] = cellPixel(name, col, letter)
123
150
  }
124
151
  }
125
152
  return out
126
153
  }
127
154
 
128
- // Getting a context object back is not the same as getting one that can do
129
- // this: the headless render shim hands back a context carrying only measureText,
130
- // which a truthiness test accepts and putImageData then does not survive. Name
131
- // the methods the raster actually calls, so every caller degrades together.
155
+ function averagePixels({
156
+ spec,
157
+ col0,
158
+ row0,
159
+ width,
160
+ height,
161
+ colSpan,
162
+ rowSpan,
163
+ toPixel,
164
+ out,
165
+ }: {
166
+ spec: RasterSpec
167
+ col0: number
168
+ row0: number
169
+ width: number
170
+ height: number
171
+ colSpan: number
172
+ rowSpan: number
173
+ toPixel: (css: string) => number
174
+ out: Uint32Array
175
+ }) {
176
+ const { rowNames, columns } = spec
177
+ const cellPixel = cellPixelFn(spec, toPixel)
178
+ const cells = colSpan * rowSpan
179
+ const rShift = littleEndian ? 0 : 24
180
+ const gShift = littleEndian ? 8 : 16
181
+ const bShift = littleEndian ? 16 : 8
182
+ const aShift = littleEndian ? 24 : 0
183
+ const names: string[] = []
184
+ const strs: string[] = []
185
+
186
+ for (let r = 0; r < height; r++) {
187
+ names.length = 0
188
+ strs.length = 0
189
+ for (let dr = 0; dr < rowSpan; dr++) {
190
+ const name = rowNames[row0 + r * rowSpan + dr]
191
+ const str = name === undefined ? undefined : columns.get(name)
192
+ if (name !== undefined && str) {
193
+ names.push(name)
194
+ strs.push(str)
195
+ }
196
+ }
197
+ const base = r * width
198
+ for (let c = 0; c < width; c++) {
199
+ // premultiplied, so a cell past the end of a row dilutes the alpha of its
200
+ // pixel rather than darkening its color
201
+ let sumR = 0
202
+ let sumG = 0
203
+ let sumB = 0
204
+ let sumA = 0
205
+ for (let i = 0; i < strs.length; i++) {
206
+ const str = strs[i]!
207
+ const name = names[i]!
208
+ for (let dc = 0; dc < colSpan; dc++) {
209
+ const col = col0 + c * colSpan + dc
210
+ const letter = str[col]
211
+ if (letter === undefined) {
212
+ break
213
+ }
214
+ const p = cellPixel(name, col, letter)
215
+ const a = (p >>> aShift) & 255
216
+ sumR += ((p >>> rShift) & 255) * a
217
+ sumG += ((p >>> gShift) & 255) * a
218
+ sumB += ((p >>> bShift) & 255) * a
219
+ sumA += a
220
+ }
221
+ }
222
+ out[base + c] =
223
+ sumA === 0
224
+ ? 0
225
+ : packRgba(
226
+ Math.round(sumR / sumA),
227
+ Math.round(sumG / sumA),
228
+ Math.round(sumB / sumA),
229
+ Math.round(sumA / cells),
230
+ )
231
+ }
232
+ }
233
+ }
234
+
132
235
  function usableRasterCtx(ctx: unknown): ctx is RasterCtx {
133
236
  const c = ctx as Record<string, unknown> | null
134
237
  return (
@@ -154,13 +257,17 @@ function makeRasterCanvas(width: number, height: number) {
154
257
  : undefined
155
258
  }
156
259
 
157
- function paint(pixels: Uint32Array, width: number, height: number) {
260
+ function paint(
261
+ width: number,
262
+ height: number,
263
+ fill: (out: Uint32Array) => void,
264
+ ) {
158
265
  const made = makeRasterCanvas(width, height)
159
266
  if (!made) {
160
267
  return undefined
161
268
  }
162
269
  const image = made.ctx.createImageData(width, height)
163
- new Uint32Array(image.data.buffer).set(pixels)
270
+ fill(new Uint32Array(image.data.buffer))
164
271
  made.ctx.putImageData(image, 0, 0)
165
272
  return made.canvas
166
273
  }
@@ -223,8 +330,29 @@ function getCache(model: MsaViewModel, theme: Theme) {
223
330
  return next
224
331
  }
225
332
 
226
- function getTile(cache: RasterCache, tileRow: number, tileCol: number) {
227
- const key = `${tileRow}_${tileCol}`
333
+ // Cells averaged into one tile pixel along an axis: the smallest power of two
334
+ // that brings a pixel back to at least one device pixel. Powers of two keep the
335
+ // number of distinct tile sets a zoom sweep builds to a handful.
336
+ function cellsPerPixel(devicePixelsPerCell: number) {
337
+ return devicePixelsPerCell >= 1
338
+ ? 1
339
+ : 2 ** Math.ceil(Math.log2(1 / devicePixelsPerCell))
340
+ }
341
+
342
+ function getTile({
343
+ cache,
344
+ tileRow,
345
+ tileCol,
346
+ colSpan,
347
+ rowSpan,
348
+ }: {
349
+ cache: RasterCache
350
+ tileRow: number
351
+ tileCol: number
352
+ colSpan: number
353
+ rowSpan: number
354
+ }) {
355
+ const key = `${colSpan}_${rowSpan}_${tileRow}_${tileCol}`
228
356
  const hit = cache.tiles.get(key)
229
357
  if (hit) {
230
358
  // re-inserting makes this the newest entry: a Map iterates in insertion
@@ -233,18 +361,31 @@ function getTile(cache: RasterCache, tileRow: number, tileCol: number) {
233
361
  cache.tiles.set(key, hit)
234
362
  return hit
235
363
  }
236
- const col0 = tileCol * rasterTileSize
237
- const row0 = tileRow * rasterTileSize
238
- const width = Math.min(rasterTileSize, cache.numColumns - col0)
239
- const height = Math.min(rasterTileSize, cache.numRows - row0)
364
+ const col0 = tileCol * rasterTileSize * colSpan
365
+ const row0 = tileRow * rasterTileSize * rowSpan
366
+ const width = Math.min(
367
+ rasterTileSize,
368
+ Math.ceil((cache.numColumns - col0) / colSpan),
369
+ )
370
+ const height = Math.min(
371
+ rasterTileSize,
372
+ Math.ceil((cache.numRows - row0) / rowSpan),
373
+ )
240
374
  if (width <= 0 || height <= 0) {
241
375
  return undefined
242
376
  }
243
- const canvas = paint(
244
- rasterPixels({ spec: cache.spec, col0, row0, width, height }),
245
- width,
246
- height,
247
- )
377
+ const canvas = paint(width, height, out => {
378
+ rasterPixels({
379
+ spec: cache.spec,
380
+ col0,
381
+ row0,
382
+ width,
383
+ height,
384
+ colSpan,
385
+ rowSpan,
386
+ out,
387
+ })
388
+ })
248
389
  if (!canvas) {
249
390
  return undefined
250
391
  }
@@ -263,6 +404,11 @@ function getTile(cache: RasterCache, tileRow: number, tileCol: number) {
263
404
  * one-pixel-per-cell tiles. The tiles do not depend on the zoom level, so a zoom
264
405
  * frame costs a handful of drawImage calls instead of a fillRect per visible
265
406
  * cell -- which ran to 885ms per block at the minimum column width.
407
+ *
408
+ * Below a device pixel per cell the tiles average cells together instead, and
409
+ * only along the axis that needs it. Leaving the downsampling to the browser's
410
+ * image smoothing blurred both axes at once: at fit-to-width, where only the
411
+ * columns are narrow, every row boundary turned into a gradient.
266
412
  */
267
413
  export function drawMsaRaster({
268
414
  ctx,
@@ -279,17 +425,18 @@ export function drawMsaRaster({
279
425
  }) {
280
426
  const { colWidth, rowHeight, blockSize, highResScaleFactor } = model
281
427
  const cache = getCache(model, theme)
428
+ const colSpan = cellsPerPixel(colWidth * highResScaleFactor)
429
+ const rowSpan = cellsPerPixel(rowHeight * highResScaleFactor)
282
430
 
283
431
  ctx.resetTransform()
284
432
  ctx.scale(highResScaleFactor, highResScaleFactor)
285
433
  ctx.translate(-offsetX, -offsetY)
286
- // below a pixel per cell a tile carries more cells than the block has room
287
- // for, so let the browser downsample it rather than letting whichever cell
288
- // landed last win
289
- ctx.imageSmoothingEnabled = colWidth < 1 || rowHeight < 1
434
+ ctx.imageSmoothingEnabled = false
290
435
 
291
- const tileWidth = rasterTileSize * colWidth
292
- const tileHeight = rasterTileSize * rowHeight
436
+ const pixelWidth = colWidth * colSpan
437
+ const pixelHeight = rowHeight * rowSpan
438
+ const tileWidth = rasterTileSize * pixelWidth
439
+ const tileHeight = rasterTileSize * pixelHeight
293
440
  const firstCol = Math.max(0, Math.floor(offsetX / tileWidth))
294
441
  const lastCol = Math.floor((offsetX + blockSize) / tileWidth)
295
442
  const firstRow = Math.max(0, Math.floor(offsetY / tileHeight))
@@ -297,7 +444,7 @@ export function drawMsaRaster({
297
444
 
298
445
  for (let tileRow = firstRow; tileRow <= lastRow; tileRow++) {
299
446
  for (let tileCol = firstCol; tileCol <= lastCol; tileCol++) {
300
- const tile = getTile(cache, tileRow, tileCol)
447
+ const tile = getTile({ cache, tileRow, tileCol, colSpan, rowSpan })
301
448
  if (tile) {
302
449
  ctx.drawImage(
303
450
  tile,
@@ -307,8 +454,8 @@ export function drawMsaRaster({
307
454
  tile.height,
308
455
  tileCol * tileWidth,
309
456
  tileRow * tileHeight,
310
- tile.width * colWidth,
311
- tile.height * rowHeight,
457
+ tile.width * pixelWidth,
458
+ tile.height * pixelHeight,
312
459
  )
313
460
  }
314
461
  }
@@ -353,7 +500,9 @@ export function canvasHref(canvas: RasterCanvas | undefined) {
353
500
  }
354
501
 
355
502
  // A canvas much past this many pixels fails to allocate, and the browser's own
356
- // per-side limit is lower still, so a raster bigger than either samples down.
503
+ // per-side limit is lower still, so a raster bigger than either averages down,
504
+ // per axis: an alignment wide enough to hit the limit on columns alone keeps
505
+ // every row.
357
506
  // The result is still drawn across the same rectangle -- it loses cell-exact
358
507
  // detail at a size where no figure could show it anyway.
359
508
  const maxImagePixels = 64e6
@@ -391,31 +540,40 @@ export function rasterImageHref({
391
540
  return undefined
392
541
  }
393
542
  const cache = getCache(model, theme)
394
- let step = 1
543
+ let colSpan = 1
544
+ let rowSpan = 1
545
+ while (Math.ceil(numCols / colSpan) > maxImageSide) {
546
+ colSpan *= 2
547
+ }
548
+ while (Math.ceil(numRows / rowSpan) > maxImageSide) {
549
+ rowSpan *= 2
550
+ }
395
551
  while (
396
- Math.ceil(numCols / step) * Math.ceil(numRows / step) > maxImagePixels ||
397
- Math.ceil(numCols / step) > maxImageSide ||
398
- Math.ceil(numRows / step) > maxImageSide
552
+ Math.ceil(numCols / colSpan) * Math.ceil(numRows / rowSpan) >
553
+ maxImagePixels
399
554
  ) {
400
- step *= 2
555
+ if (numCols / colSpan >= numRows / rowSpan) {
556
+ colSpan *= 2
557
+ } else {
558
+ rowSpan *= 2
559
+ }
401
560
  }
402
- const width = Math.ceil(numCols / step)
403
- const height = Math.ceil(numRows / step)
561
+ const width = Math.ceil(numCols / colSpan)
562
+ const height = Math.ceil(numRows / rowSpan)
404
563
 
405
564
  return canvasHref(
406
- paint(
565
+ paint(width, height, out => {
407
566
  rasterPixels({
408
567
  spec: cache.spec,
409
568
  col0,
410
569
  row0,
411
570
  width,
412
571
  height,
413
- colStep: step,
414
- rowStep: step,
415
- }),
416
- width,
417
- height,
418
- ),
572
+ colSpan,
573
+ rowSpan,
574
+ out,
575
+ })
576
+ }),
419
577
  )
420
578
  }
421
579
 
@@ -447,7 +605,7 @@ export function msaThumbnail({
447
605
  const h = Math.ceil(numRows / rowStep)
448
606
  cache.thumbnail = {
449
607
  height,
450
- canvas: paint(
608
+ canvas: paint(w, h, out => {
451
609
  rasterPixels({
452
610
  spec: cache.spec,
453
611
  col0: 0,
@@ -456,10 +614,9 @@ export function msaThumbnail({
456
614
  height: h,
457
615
  colStep,
458
616
  rowStep,
459
- }),
460
- w,
461
- h,
462
- ),
617
+ out,
618
+ })
619
+ }),
463
620
  }
464
621
  }
465
622
  return cache.thumbnail.canvas
@@ -1,16 +1,40 @@
1
1
  import { setFontSize } from '../../setFontSize.ts'
2
+ import { referenceColor } from '../overlayColors.ts'
2
3
 
3
4
  import type { MsaViewModel } from '../../model.ts'
4
5
  import type { RenderCtx } from '../renderCtx.ts'
5
6
  import type { Theme } from '@mui/material'
6
7
 
8
+ // a stronger fill plus a solid border, so a domain/motif band reads clearly over
9
+ // the colored alignment cells (the faint hover-style wash alone is invisible
10
+ // against clustalx coloring)
7
11
  export const highlightFill = 'rgba(255,140,0,0.28)'
8
12
  export const highlightBorder = 'rgba(210,90,0,0.95)'
9
13
  export const highlightRowFill = 'rgba(255,140,0,0.18)'
10
14
 
11
15
  export const labelFontSize = 11
12
16
  const labelPad = 3
17
+ // the label box a highlight draws above its band, and the reason a tree block
18
+ // has to consider rows a little outside itself
19
+ export const highlightLabelHeight = labelFontSize + labelPad * 2
13
20
 
21
+ export interface LabelBox {
22
+ left: number
23
+ right: number
24
+ top: number
25
+ bottom: number
26
+ }
27
+
28
+ /**
29
+ * A highlight's label, over its band where it fits and beside it where it does
30
+ * not, kept inside `bounds` and out of the way of the labels already placed.
31
+ *
32
+ * Two single-residue highlights a column apart both want the same few pixels to
33
+ * the right of their bands, and a band scrolled off the left edge wants its
34
+ * label off-frame with it: the first overdrew two labels into an unreadable
35
+ * smudge, the second left the visible band untitled. Pass `placed` and each
36
+ * later label steps down a row instead of over the one before.
37
+ */
14
38
  export function drawHighlightLabel({
15
39
  ctx,
16
40
  theme,
@@ -18,6 +42,8 @@ export function drawHighlightLabel({
18
42
  x,
19
43
  y,
20
44
  spanWidth,
45
+ bounds,
46
+ placed,
21
47
  }: {
22
48
  ctx: RenderCtx
23
49
  theme: Theme
@@ -25,16 +51,33 @@ export function drawHighlightLabel({
25
51
  x: number
26
52
  y: number
27
53
  spanWidth: number
54
+ bounds?: { min: number; max: number }
55
+ placed?: LabelBox[]
28
56
  }) {
29
57
  setFontSize(ctx, labelFontSize)
30
58
  ctx.textAlign = 'start'
31
59
  const boxWidth = ctx.measureText(label).width + labelPad * 2
32
- const boxHeight = labelFontSize + labelPad * 2
33
- const left = boxWidth <= spanWidth ? x : x + spanWidth + 2
60
+ const boxHeight = highlightLabelHeight
61
+ let left = boxWidth <= spanWidth ? x : x + spanWidth + 2
62
+ if (bounds) {
63
+ left = Math.max(bounds.min, Math.min(left, bounds.max - boxWidth))
64
+ }
65
+ let top = y
66
+ if (placed) {
67
+ const overlaps = (box: LabelBox) =>
68
+ box.left < left + boxWidth &&
69
+ box.right > left &&
70
+ box.top < top + boxHeight &&
71
+ box.bottom > top
72
+ while (placed.some(overlaps)) {
73
+ top += boxHeight
74
+ }
75
+ placed.push({ left, right: left + boxWidth, top, bottom: top + boxHeight })
76
+ }
34
77
  ctx.fillStyle = theme.palette.background.paper
35
- ctx.fillRect(left, y, boxWidth, boxHeight)
78
+ ctx.fillRect(left, top, boxWidth, boxHeight)
36
79
  ctx.fillStyle = theme.palette.text.primary
37
- ctx.fillText(label, left + labelPad, y + labelPad + labelFontSize - 2)
80
+ ctx.fillText(label, left + labelPad, top + labelPad + labelFontSize - 2)
38
81
  }
39
82
 
40
83
  /**
@@ -62,6 +105,7 @@ export function renderHighlights({
62
105
  height: number
63
106
  }) {
64
107
  const { resolvedHighlights, colWidth, rowHeight } = model
108
+ const placed: LabelBox[] = []
65
109
  ctx.lineWidth = 2
66
110
  for (const { rowIndices, color } of resolvedHighlights) {
67
111
  ctx.fillStyle = color ?? highlightRowFill
@@ -80,7 +124,61 @@ export function renderHighlights({
80
124
  ctx.strokeStyle = color ?? highlightBorder
81
125
  ctx.strokeRect(x, 0, spanWidth, height)
82
126
  if (label) {
83
- drawHighlightLabel({ ctx, theme, label, x, y: 0, spanWidth })
127
+ drawHighlightLabel({
128
+ ctx,
129
+ theme,
130
+ label,
131
+ x,
132
+ y: 0,
133
+ spanWidth,
134
+ bounds: { min: 0, max: width },
135
+ placed,
136
+ })
84
137
  }
85
138
  }
86
139
  }
140
+
141
+ /**
142
+ * Everything the alignment overlay owes to the document rather than to the
143
+ * mouse: the reference row's tint, a bordered band per run of highlighted
144
+ * columns, and the `highlights` layer. The live overlay canvas draws these
145
+ * under its hover and click bands, and the SVG export draws them and stops --
146
+ * a shared link that opens with columns highlighted exports with them too.
147
+ */
148
+ export function renderPersistentHighlights({
149
+ ctx,
150
+ model,
151
+ theme,
152
+ offsetX,
153
+ offsetY,
154
+ width,
155
+ height,
156
+ }: {
157
+ ctx: RenderCtx
158
+ model: MsaViewModel
159
+ theme: Theme
160
+ offsetX: number
161
+ offsetY: number
162
+ width: number
163
+ height: number
164
+ }) {
165
+ const { colWidth, rowHeight, referenceRowIndex, highlightedColumnRuns } =
166
+ model
167
+
168
+ if (referenceRowIndex !== undefined) {
169
+ ctx.fillStyle = referenceColor
170
+ ctx.fillRect(0, referenceRowIndex * rowHeight - offsetY, width, rowHeight)
171
+ }
172
+
173
+ ctx.lineWidth = 2
174
+ for (const { start, end } of highlightedColumnRuns) {
175
+ const x = start * colWidth - offsetX
176
+ const w = (end - start + 1) * colWidth
177
+ ctx.fillStyle = highlightFill
178
+ ctx.fillRect(x, 0, w, height)
179
+ ctx.strokeStyle = highlightBorder
180
+ ctx.strokeRect(x, 0, w, height)
181
+ }
182
+
183
+ renderHighlights({ ctx, model, theme, offsetX, offsetY, width, height })
184
+ }
@@ -1,5 +1,5 @@
1
1
  import { setFontSize } from '../../setFontSize.ts'
2
- import { adjustColorForContrast } from '../../util.ts'
2
+ import { adjustColorForContrast, contrastTextFn } from '../../util.ts'
3
3
  import { getVisibleLeaves } from '../getVisibleLeaves.ts'
4
4
  import { domainBandCursor } from './domainBandCursor.ts'
5
5
  import { tileColorFn } from './tileColor.ts'
@@ -15,7 +15,6 @@ export function renderMSABlock({
15
15
  model,
16
16
  offsetX,
17
17
  offsetY,
18
- contrastScheme,
19
18
  ctx,
20
19
  theme,
21
20
  highResScaleFactorOverride,
@@ -27,7 +26,6 @@ export function renderMSABlock({
27
26
  offsetY: number
28
27
  theme: Theme
29
28
  model: MsaViewModel
30
- contrastScheme: Record<string, string>
31
29
  ctx: RenderCtx
32
30
  highResScaleFactorOverride?: number
33
31
  blockSizeXOverride?: number
@@ -68,7 +66,6 @@ export function renderMSABlock({
68
66
  model,
69
67
  ctx,
70
68
  theme,
71
- contrastScheme,
72
69
  xStart,
73
70
  xEnd,
74
71
  visibleLeaves,
@@ -90,23 +87,10 @@ export function renderMSABlock({
90
87
  ctx.resetTransform()
91
88
  }
92
89
 
93
- // Letter color to use over a domain box, keyed by accession. Domain fills come
94
- // from a categorical palette that has no fixed lightness, so ask the theme for a
95
- // readable text color per fill instead of assuming dark-on-light.
96
- function domainLetterColors(model: MsaViewModel, theme: Theme) {
97
- return new Map(
98
- Object.entries(model.fillPalette).map(([accession, fill]) => [
99
- accession,
100
- theme.palette.getContrastText(fill),
101
- ]),
102
- )
103
- }
104
-
105
90
  function drawTilesAndText({
106
91
  model,
107
92
  ctx,
108
93
  theme,
109
- contrastScheme,
110
94
  visibleLeaves,
111
95
  xStart,
112
96
  xEnd,
@@ -117,7 +101,6 @@ function drawTilesAndText({
117
101
  model: MsaViewModel
118
102
  theme: Theme
119
103
  ctx: RenderCtx
120
- contrastScheme: Record<string, string>
121
104
  visibleLeaves: HierarchyNode<NodeWithIdsAndLength>[]
122
105
  xStart: number
123
106
  xEnd: number
@@ -139,10 +122,14 @@ function drawTilesAndText({
139
122
  const tiles = drawTiles && bgColor
140
123
  const paintTiles = tiles && !rasterTiles
141
124
  if (paintTiles || showMsaLetters) {
142
- // over raster tiles the letters take their color from contrastScheme, so
143
- // the scheme lookup per cell is pure waste
144
- const needsColor = paintTiles || !tiles
125
+ // a letter takes its color from the cell it lands on, so the scheme lookup
126
+ // is only waste when nothing draws letters
127
+ const needsColor = paintTiles || showMsaLetters
145
128
  const tileColor = tileColorFn(model)
129
+ // Domain fills come from a categorical palette with no fixed lightness, and
130
+ // a dynamic scheme colors a cell from its column, so a readable letter color
131
+ // is a question about the cell's own color rather than about its letter.
132
+ const contrastText = contrastTextFn(theme)
146
133
  const offsetXAligned = xStart * colWidth
147
134
  const halfColWidth = colWidth / 2
148
135
  // note: -rowHeight/4 matches +rowHeight/4 in tree
@@ -151,9 +138,6 @@ function drawTilesAndText({
151
138
  // on a domain box or on the plain background; sub-row layout stacks the boxes
152
139
  // above the letters, so those rows are all plain background
153
140
  const overDomains = !drawTiles && !subFeatureRows
154
- const domainColors = overDomains
155
- ? domainLetterColors(model, theme)
156
- : undefined
157
141
 
158
142
  for (let i = 0, l1 = visibleLeaves.length; i < l1; i++) {
159
143
  const node = visibleLeaves[i]!
@@ -186,18 +170,23 @@ function drawTilesAndText({
186
170
  const covering = bandAt(col)
187
171
  ctx.fillStyle = covering
188
172
  ? // on top of a domain box: contrast against the box fill
189
- domainColors!.get(covering.annotation.accession)!
190
- : tiles
191
- ? // on top of a colored tile
192
- (contrastScheme[letter] ?? 'black')
193
- : !drawTiles || !color
194
- ? // plain background, uncolored letters
195
- theme.palette.text.primary
196
- : // letter-color mode: darken/lighten to stay readable
197
- adjustColorForContrast(
198
- color,
199
- theme.palette.background.default,
200
- )
173
+ contrastText(model.fillPalette[covering.annotation.accession])
174
+ : isMatchingReference
175
+ ? // the dot sits on the faint reference-match wash, which is
176
+ // the theme's background with a little of its text color in it
177
+ theme.palette.text.primary
178
+ : tiles
179
+ ? // on top of a colored tile, or on the background where the
180
+ // scheme gives that cell no color
181
+ contrastText(color)
182
+ : !drawTiles || !color
183
+ ? // plain background, uncolored letters
184
+ theme.palette.text.primary
185
+ : // letter-color mode: darken/lighten to stay readable
186
+ adjustColorForContrast(
187
+ color,
188
+ theme.palette.background.default,
189
+ )
201
190
  ctx.fillText(
202
191
  isMatchingReference ? '.' : letter,
203
192
  x + halfColWidth,