react-msaview 6.4.2 → 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
@@ -0,0 +1,9 @@
1
+ // Vite resolves a `?raw` import to the file's text. The lib does not build with
2
+ // Vite, but realResidueMapping.test.ts reads the examples package's alignment
3
+ // files that way (they are files, not string constants), so tsc has to be told
4
+ // what the suffix means. tsconfig.build.json excludes this, and tsc emits
5
+ // nothing for a .d.ts anyway, so it stays out of the published types.
6
+ declare module '*?raw' {
7
+ const content: string
8
+ export default content
9
+ }
@@ -1,3 +1,4 @@
1
+ import { colord } from 'colord'
1
2
  import { flushSync } from 'react-dom'
2
3
  import { createRoot } from 'react-dom/client'
3
4
 
@@ -37,11 +38,37 @@ export function renderToStaticMarkup(node: React.ReactElement) {
37
38
  // export ever taken. The markup is a string by here and needs no DOM.
38
39
  root.unmount()
39
40
  }
40
- // SVG 1.1 presentation attributes (`fill`, `stroke`) take a <color>, which
41
- // excludes rgba() -- alpha belongs in a separate fill-opacity. Illustrator and
42
- // older Inkscape drop an element whose fill they cannot parse, so alpha is
43
- // stripped rather than left to break the shape. MUI palette values are the
44
- // usual source. Dropping rather than converting is what the figure snapshots
45
- // encode.
46
- return html.replaceAll(/\brgba\((.+?),[^,]+?\)/g, 'rgb($1)')
41
+ return html
42
+ }
43
+
44
+ const tag = /<[a-zA-Z][^>]*>/g
45
+ const colorAttr = /\b(fill|stroke|stop-color)="([^"]+)"/g
46
+
47
+ /**
48
+ * Rewrites every color an SVG 1.1 presentation attribute cannot hold.
49
+ *
50
+ * `fill`, `stroke` and `stop-color` take a <color>, which is a keyword, a hex
51
+ * triplet or rgb() -- not rgba(), which MUI palette values are full of, and not
52
+ * hsl(), which the percent-identity scheme colors with. Illustrator and older
53
+ * Inkscape drop an element whose fill they cannot parse, so the minimap thumb
54
+ * came out opaque and the identity coloring came out missing. Alpha moves to
55
+ * the matching -opacity attribute, unless the element already carries one.
56
+ */
57
+ export function svgSafeColors(html: string) {
58
+ return html.replaceAll(tag, el =>
59
+ el.replaceAll(colorAttr, (match, attr: string, value: string) => {
60
+ if (!/^(rgba|hsl)/i.test(value)) {
61
+ return match
62
+ }
63
+ const color = colord(value)
64
+ if (!color.isValid()) {
65
+ return match
66
+ }
67
+ const alpha = color.alpha()
68
+ const hex = color.alpha(1).toHex()
69
+ return alpha === 1 || el.includes(`${attr}-opacity=`)
70
+ ? `${attr}="${hex}"`
71
+ : `${attr}="${hex}" ${attr}-opacity="${alpha}"`
72
+ }),
73
+ )
47
74
  }
@@ -7,13 +7,12 @@ import { visibleRowRange } from './components/getVisibleLeaves.ts'
7
7
  import MinimapSVG from './components/minimap/MinimapSVG.tsx'
8
8
  import { rasterImageHref, rasterSupported } from './components/msa/msaRaster.ts'
9
9
  import { renderBoxFeatureCanvasBlock } from './components/msa/renderBoxFeatureCanvasBlock.ts'
10
- import { renderHighlights } from './components/msa/renderHighlights.ts'
10
+ import { renderPersistentHighlights } from './components/msa/renderHighlights.ts'
11
11
  import { renderMSABlock } from './components/msa/renderMSABlock.ts'
12
12
  import { visibleColRange } from './components/msa/visibleColRange.ts'
13
13
  import { renderAllTracks } from './components/tracks/drawTracks.ts'
14
14
  import { renderTreeCanvas } from './components/tree/renderTreeCanvas.ts'
15
- import { renderToStaticMarkup } from './renderToStaticMarkup.ts'
16
- import { colorContrast } from './util.ts'
15
+ import { renderToStaticMarkup, svgSafeColors } from './renderToStaticMarkup.ts'
17
16
 
18
17
  import type { MsaViewModel } from './model.ts'
19
18
  import type { Context as ContextType } from '@jbrowse/svgcanvas'
@@ -131,14 +130,26 @@ export async function renderToSvg(model: MsaViewModel, opts: ExportSvgOptions) {
131
130
  throw model.error
132
131
  }
133
132
  const { Context } = await import('@jbrowse/svgcanvas')
134
- return renderToStaticMarkup(
133
+ // Each svgcanvas layer is already serialized SVG, and React would have to
134
+ // parse it into a second DOM only for the export to serialize it back out.
135
+ // The layers register themselves here instead, and their markup is spliced
136
+ // into the page as strings.
137
+ const layers = new Map<string, ContextType>()
138
+ const markup = renderToStaticMarkup(
135
139
  <MsaSvg
136
140
  model={model}
137
141
  theme={opts.theme}
138
142
  Context={Context}
139
143
  layout={getLayout(model, opts)}
144
+ layers={layers}
140
145
  />,
141
146
  )
147
+ return svgSafeColors(
148
+ markup.replaceAll(/<g data-layer="([^"]+)"><\/g>/g, (match, id: string) => {
149
+ const ctx = layers.get(id)
150
+ return ctx ? `<g>${ctx.getSvg().innerHTML}</g>` : match
151
+ }),
152
+ )
142
153
  }
143
154
 
144
155
  function MsaSvg({
@@ -146,11 +157,13 @@ function MsaSvg({
146
157
  theme,
147
158
  Context,
148
159
  layout,
160
+ layers,
149
161
  }: {
150
162
  model: MsaViewModel
151
163
  theme: Theme
152
164
  Context: typeof ContextType
153
165
  layout: Layout
166
+ layers: LayerMap
154
167
  }) {
155
168
  const { width, trackHeight, includeMinimap, legendWidth } = layout
156
169
  const { treeAreaWidth, minimapHeight, visibleDomainTypes } = model
@@ -162,8 +175,7 @@ function MsaSvg({
162
175
  legendWidth > 0
163
176
  ? Math.max(layout.height, legendTop + legendHeight(visibleDomainTypes))
164
177
  : layout.height
165
- const contrastScheme = colorContrast(model.colorScheme, theme)
166
- const props = { Context, model, theme, layout, contrastScheme }
178
+ const props = { Context, model, theme, layout, layers }
167
179
 
168
180
  const body = (
169
181
  <>
@@ -269,21 +281,17 @@ function LegendSVG({
269
281
  )
270
282
  }
271
283
 
284
+ type LayerMap = Map<string, ContextType>
285
+
272
286
  interface LayerProps {
273
287
  model: MsaViewModel
274
288
  theme: Theme
275
289
  layout: Layout
276
290
  Context: typeof ContextType
277
- contrastScheme: Record<string, string>
291
+ layers: LayerMap
278
292
  }
279
293
 
280
- function CoreRendering({
281
- model,
282
- theme,
283
- layout,
284
- Context,
285
- contrastScheme,
286
- }: LayerProps) {
294
+ function CoreRendering({ model, theme, layout, Context, layers }: LayerProps) {
287
295
  const { contentHeight, offsetX, offsetY, msaAreaWidth } = layout
288
296
  const { treeAreaWidth, id } = model
289
297
 
@@ -315,7 +323,6 @@ function CoreRendering({
315
323
  model,
316
324
  theme,
317
325
  ctx: msaCtx,
318
- contrastScheme,
319
326
  offsetX,
320
327
  offsetY,
321
328
  blockSizeXOverride: msaAreaWidth,
@@ -324,7 +331,7 @@ function CoreRendering({
324
331
  rasterTiles: !!raster,
325
332
  })
326
333
  msaCtx.resetTransform()
327
- renderHighlights({
334
+ renderPersistentHighlights({
328
335
  ctx: msaCtx,
329
336
  model,
330
337
  theme,
@@ -341,6 +348,7 @@ function CoreRendering({
341
348
  width={treeAreaWidth}
342
349
  height={contentHeight}
343
350
  ctx={treeCtx}
351
+ layers={layers}
344
352
  />
345
353
  <ClipGroup
346
354
  clipId={`msa-${id}`}
@@ -348,6 +356,7 @@ function CoreRendering({
348
356
  height={contentHeight}
349
357
  transform={`translate(${treeAreaWidth} 0)`}
350
358
  ctx={msaCtx}
359
+ layers={layers}
351
360
  underlay={raster}
352
361
  />
353
362
  </>
@@ -412,13 +421,7 @@ function rasterBackground({
412
421
  ) : undefined
413
422
  }
414
423
 
415
- function TrackRendering({
416
- model,
417
- theme,
418
- layout,
419
- Context,
420
- contrastScheme,
421
- }: LayerProps) {
424
+ function TrackRendering({ model, theme, layout, Context, layers }: LayerProps) {
422
425
  const { trackHeight, offsetX, msaAreaWidth } = layout
423
426
  const { treeAreaWidth, id } = model
424
427
 
@@ -426,7 +429,6 @@ function TrackRendering({
426
429
  renderAllTracks({
427
430
  model,
428
431
  ctx,
429
- contrastScheme,
430
432
  theme,
431
433
  offsetX,
432
434
  blockSizeXOverride: msaAreaWidth,
@@ -442,6 +444,7 @@ function TrackRendering({
442
444
  width={msaAreaWidth}
443
445
  height={trackHeight}
444
446
  ctx={ctx}
447
+ layers={layers}
445
448
  />
446
449
  </g>
447
450
  </>
@@ -487,8 +490,8 @@ function TrackLabelsSVG({
487
490
  )
488
491
  }
489
492
 
490
- // Clips a svgcanvas Context to its box and injects its markup verbatim (it is
491
- // already serialized SVG, not React). `underlay` draws beneath that markup,
493
+ // Clips a svgcanvas Context to its box and leaves a placeholder for renderToSvg
494
+ // to splice the layer's own markup into. `underlay` draws beneath that markup,
492
495
  // inside the same clip, for the parts of a layer React emits directly.
493
496
  function ClipGroup({
494
497
  clipId,
@@ -496,6 +499,7 @@ function ClipGroup({
496
499
  height,
497
500
  transform,
498
501
  ctx,
502
+ layers,
499
503
  underlay,
500
504
  }: {
501
505
  clipId: string
@@ -503,8 +507,10 @@ function ClipGroup({
503
507
  height: number
504
508
  transform?: string
505
509
  ctx: ContextType
510
+ layers: LayerMap
506
511
  underlay?: React.ReactNode
507
512
  }) {
513
+ layers.set(clipId, ctx)
508
514
  return (
509
515
  <>
510
516
  <defs>
@@ -514,7 +520,7 @@ function ClipGroup({
514
520
  </defs>
515
521
  <g clipPath={`url(#${clipId})`} transform={transform}>
516
522
  {underlay}
517
- <g dangerouslySetInnerHTML={{ __html: ctx.getSvg().innerHTML }} />
523
+ <g data-layer={clipId} />
518
524
  </g>
519
525
  </>
520
526
  )
@@ -1,5 +1,3 @@
1
- import { isBlank } from './util.ts'
2
-
3
1
  /**
4
2
  * MSA Coordinate Systems:
5
3
  *
@@ -93,65 +91,23 @@ export function visibleColsBefore(blanks: number[], globalCol: number) {
93
91
  }
94
92
 
95
93
  /**
96
- * Convert a visible column to a row-specific sequence position.
97
- * Returns undefined if the position is a gap in the sequence.
94
+ * The ungapped position a row holds at a global column, or undefined when that
95
+ * column is a gap in the row or past its end.
98
96
  *
99
- * @param seq - The row's sequence string (including gaps)
100
- * @param blanks - Sorted array of global column indices that are hidden
101
- * @param visibleCol - The visible column index
102
- * @returns The sequence position, or undefined if it's a gap
97
+ * A binary search of the row's seqPos index (ascending), not a scan of the row:
98
+ * this answers on every mouse move, and scanning a 30k-column row per event is
99
+ * the whole frame.
100
+ *
101
+ * @param index - The row's seqPos -> global column index (buildSeqPosIndex)
102
+ * @param globalCol - The global column index in the full MSA
103
103
  */
104
- export function visibleColToSeqPos({
105
- seq,
106
- blanks,
107
- visibleCol,
108
- }: {
109
- seq: string
110
- blanks: number[]
111
- visibleCol: number
112
- }) {
113
- // First convert the visible column to global column
114
- const globalCol = visibleColToGlobalCol(blanks, visibleCol)
115
- const seqLen = seq.length
116
-
117
- // Check if the position in the sequence is a gap
118
- if (globalCol < seqLen && isBlank(seq[globalCol])) {
104
+ export function seqPosOfGlobalCol(
105
+ index: Int32Array | undefined,
106
+ globalCol: number,
107
+ ) {
108
+ if (!index) {
119
109
  return undefined
120
110
  }
121
-
122
- // Count non-gap characters up to the global position
123
- let seqPos = 0
124
- for (let i = 0; i < globalCol && i < seqLen; i++) {
125
- if (!isBlank(seq[i])) {
126
- seqPos++
127
- }
128
- }
129
-
130
- return globalCol < seqLen ? seqPos : undefined
131
- }
132
-
133
- /**
134
- * Convert a visible column to a row-specific sequence position, with row lookup.
135
- *
136
- * @param rowName - The name of the row
137
- * @param visibleCol - The visible column index
138
- * @param rowMap - Map from row name to sequence string
139
- * @param blanks - Sorted array of global column indices that are hidden
140
- * @returns The sequence position, or undefined if row not found or position is a gap
141
- */
142
- export function visibleColToSeqPosForRow({
143
- rowName,
144
- visibleCol,
145
- rowMap,
146
- blanks,
147
- }: {
148
- rowName: string
149
- visibleCol: number
150
- rowMap: Map<string, string>
151
- blanks: number[]
152
- }) {
153
- const seq = rowMap.get(rowName)
154
- return seq !== undefined
155
- ? visibleColToSeqPos({ seq, visibleCol, blanks })
156
- : undefined
111
+ const seqPos = partitionPoint(index.length, i => index[i]! >= globalCol)
112
+ return index[seqPos] === globalCol ? seqPos : undefined
157
113
  }
@@ -19,5 +19,7 @@ export function buildSeqPosIndex(row: string) {
19
19
  index[n++] = col
20
20
  }
21
21
  }
22
- return index.subarray(0, n)
22
+ // slice, not subarray: a subarray keeps the whole row-length buffer alive,
23
+ // and a gappy alignment's indexes are held for as long as the parse is
24
+ return index.slice(0, n)
23
25
  }
package/src/types.ts CHANGED
@@ -8,7 +8,7 @@ export interface Accession {
8
8
  // which renderer draws a track's content. Every track kind draws into the same
9
9
  // per-column coordinate space, so the kind picks the draw function rather than
10
10
  // the geometry -- see drawTracks.ts, which dispatches on it.
11
- export type TrackKind = 'text' | 'bar' | 'logo'
11
+ export type TrackKind = 'text' | 'bar' | 'logo' | 'arc' | 'ruler'
12
12
 
13
13
  export interface BasicTrackModel {
14
14
  id: string
@@ -16,6 +16,8 @@ export interface BasicTrackModel {
16
16
  associatedRowName?: string
17
17
  height: number
18
18
  kind: TrackKind
19
+ // hidden until the user asks for it, unless they have already chosen
20
+ defaultOff?: boolean
19
21
  }
20
22
 
21
23
  export interface TextTrackModel extends BasicTrackModel {
@@ -29,20 +31,45 @@ export interface BarTrackModel extends BasicTrackModel {
29
31
  barColor?: string
30
32
  }
31
33
 
34
+ // a pair of columns joined by an arc, already resolved to 0-based visible
35
+ // columns -- what the renderer consumes
36
+ export interface Arc {
37
+ start: number
38
+ end: number
39
+ color?: string
40
+ }
41
+
42
+ // a track that joins pairs of columns with arcs (RNA base pairs, disulfide
43
+ // bonds, residue contacts)
44
+ export interface ArcTrackModel extends BasicTrackModel {
45
+ arcs?: Arc[]
46
+ arcColor?: string
47
+ }
48
+
32
49
  export interface BasicTrack {
33
50
  ReactComponent: React.FC<any>
34
- model: TextTrackModel & BarTrackModel
51
+ model: TextTrackModel & BarTrackModel & ArcTrackModel
52
+ }
53
+
54
+ // one arc of an arc track, in the snapshot's own coordinates: 1-based and
55
+ // inclusive, alignment columns unless the track names a `row`
56
+ export interface ArcSpec {
57
+ start: number
58
+ end: number
59
+ color?: string
35
60
  }
36
61
 
37
62
  // a track supplied as data in the snapshot rather than computed from the
38
63
  // alignment. `values` (bar) or `data` (text) index alignment columns, or the
39
- // 1-based residues of `row` when it names one
64
+ // 1-based residues of `row` when it names one; `arcs` (arc) name two such
65
+ // positions each
40
66
  export interface ColumnTrackSpec {
41
67
  id: string
42
68
  name: string
43
- kind: 'bar' | 'text'
69
+ kind: 'bar' | 'text' | 'arc'
44
70
  values?: number[]
45
71
  data?: string
72
+ arcs?: ArcSpec[]
46
73
  max?: number
47
74
  color?: string
48
75
  colors?: Record<string, string>
@@ -50,6 +77,106 @@ export interface ColumnTrackSpec {
50
77
  height?: number
51
78
  }
52
79
 
80
+ // One contiguous run where a row's residues and a structure's line up 1:1.
81
+ // Segment-shaped because SIFTS is: a dozen numbers cover what a per-residue
82
+ // array would spend kilobytes on, and it makes the refusal rule structural
83
+ // rather than a vocabulary -- a position no segment covers is unmapped, and
84
+ // there is no status field to disagree with.
85
+ export interface ResidueSegment {
86
+ rowStart: number
87
+ rowEnd: number
88
+ structStart: number
89
+ structEnd: number
90
+ }
91
+
92
+ // the structure half of a mapping. `id` is whatever the producer calls the
93
+ // entry (a PDB id, an AlphaFold accession); `asymId` names the chain, which is
94
+ // what distinguishes two mappings onto the same entry
95
+ export interface MappedStructure {
96
+ id: string
97
+ kind?: 'experimental' | 'predicted'
98
+ asymId?: string
99
+ url?: string
100
+ }
101
+
102
+ /**
103
+ * Which residue of which structure a row's residue is, as data. Computed by
104
+ * whatever knows how -- SIFTS, an AlphaFold model, a curator -- and carried in
105
+ * the snapshot, because the viewer cannot work it out: matching a row to a
106
+ * structure by sequence equality fails for a construct with an expression tag,
107
+ * a truncation, an engineered residue, or a row that is a subsequence of the
108
+ * entry, and it fails in the direction that looks like it worked.
109
+ *
110
+ * Positions are 1-based and inclusive on both sides, as GFF and `highlights`
111
+ * are. Structure positions are `label_seq_id`, the index into the entity's
112
+ * SEQRES; author numbering carries insertion codes and stays out.
113
+ *
114
+ * `unobserved` is in structure positions: present in SEQRES with no
115
+ * coordinates. Worth distinguishing from unmapped, because "the
116
+ * crystallographer could not see it" and "this protein does not have that
117
+ * residue" mean different things to a reader.
118
+ */
119
+ export interface ResidueMapping {
120
+ row: string
121
+ accession?: string
122
+ structure: MappedStructure
123
+ segments: ResidueSegment[]
124
+ unobserved?: [number, number][]
125
+ /**
126
+ * ungapped length of the row this was computed against. Optional, and worth
127
+ * setting: a mapping outlives the alignment it was made for, and pointing at
128
+ * residue 58 of a sequence that has since been re-aligned, revised or swapped
129
+ * is the failure this whole layer exists to avoid. Declared here, the viewer
130
+ * can notice and refuse instead of answering from stale arithmetic.
131
+ */
132
+ rowLength?: number
133
+ generated?: {
134
+ by?: string
135
+ date?: string
136
+ sourceSha256?: string
137
+ }
138
+ }
139
+
140
+ // why a mapping, or one segment of it, is not being used. Refusing silently
141
+ // leaves a host unable to tell "no structure for this row" from "the mapping
142
+ // no longer matches what is loaded", which are different problems with
143
+ // different fixes.
144
+ export interface ResidueMappingProblem {
145
+ row: string
146
+ structureId: string
147
+ /**
148
+ * `mapping` means the whole mapping is out: the evidence is about the row it
149
+ * claims, so nothing it says can be trusted. `segment` takes only that
150
+ * segment, since the rest of the mapping still describes residues that exist.
151
+ */
152
+ scope: 'mapping' | 'segment'
153
+ reason: string
154
+ }
155
+
156
+ // what a lookup gives back: the structure residue a row residue is, or the row
157
+ // residue a structure residue is. `observed` is false for a residue the
158
+ // structure declares but did not resolve.
159
+ export interface StructureResidue {
160
+ structure: MappedStructure
161
+ position: number
162
+ observed: boolean
163
+ }
164
+
165
+ export interface RowResidue {
166
+ rowName: string
167
+ seqPos: number
168
+ }
169
+
170
+ /**
171
+ * One loaded document that the snapshot leaves behind, and how big it is.
172
+ * `what` names it the way the import form does, since that is where the reader
173
+ * would go to load it differently.
174
+ */
175
+ export interface UnshareableData {
176
+ what: 'alignment' | 'tree' | 'annotations' | 'row metadata' | 'data tracks'
177
+ bytes: number
178
+ }
179
+
53
180
  // the overlay annotation itself lives in msa-parsers, alongside the adapters
54
181
  // that build it. TidyDomainAnnotation is its former name, kept because
55
182
  // downstream plugins name it in their emitted declarations.
package/src/util.ts CHANGED
@@ -36,14 +36,37 @@ export function transform<T>(
36
36
  return Object.fromEntries(Object.entries(obj).map(cb))
37
37
  }
38
38
 
39
- export function colorContrast(
40
- colorScheme: Record<string, string>,
41
- theme: Theme,
42
- ) {
43
- return transform(colorScheme, ([letter, color]) => [
44
- letter,
45
- theme.palette.getContrastText(colord(color).toHex()),
46
- ])
39
+ const contrastTextCaches = new WeakMap<Theme, Map<string, string>>()
40
+
41
+ /**
42
+ * Readable letter color over a given cell color, memoized per theme.
43
+ *
44
+ * A dynamic scheme colors a cell from its column, so there is no letter->color
45
+ * table to precompute a contrast table from; asking per cell color covers the
46
+ * dynamic schemes, the static ones and a track's own `colors` alike. An
47
+ * uncolored cell sits on the theme's background, where the theme's own text
48
+ * color is what stays readable -- `black` turned every gap and every uncolored
49
+ * cell invisible in the dark theme.
50
+ */
51
+ export function contrastTextFn(theme: Theme) {
52
+ let cache = contrastTextCaches.get(theme)
53
+ if (!cache) {
54
+ cache = new Map()
55
+ contrastTextCaches.set(theme, cache)
56
+ }
57
+ const fallback = theme.palette.text.primary
58
+ return (color: string | undefined) => {
59
+ if (!color) {
60
+ return fallback
61
+ }
62
+ const hit = cache.get(color)
63
+ if (hit !== undefined) {
64
+ return hit
65
+ }
66
+ const out = theme.palette.getContrastText(colord(color).toHex())
67
+ cache.set(color, out)
68
+ return out
69
+ }
47
70
  }
48
71
 
49
72
  export function dropBlanks<T>(blanks: number[], arr: T[]) {
@@ -110,9 +133,3 @@ export function computeRowInsertions(blanks: number[], seq: string) {
110
133
  export function len(a: { end: number; start: number }) {
111
134
  return a.end - a.start
112
135
  }
113
-
114
- // https://sonnhammer.sbc.su.se/Stockholm.html
115
- // gaps can be a . or - in stockholm
116
- export function isBlank(s?: string) {
117
- return s === '-' || s === '.'
118
- }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '6.4.2'
1
+ export const version = '7.0.0'
package/src/wuss.ts ADDED
@@ -0,0 +1,62 @@
1
+ // WUSS notation (Rfam's `#=GC SS_cons`) names a consensus secondary structure
2
+ // by pairing columns. Nested helices use bracket pairs -- <>, (), [], {} --
3
+ // each type kept on its own stack, and a pseudoknot, which by definition
4
+ // crosses a helix it cannot nest inside, uses a letter pair instead: `A` opens
5
+ // and `a` closes, `B`/`b` for a second one. Everything else (. , _ - : ~) is
6
+ // unpaired.
7
+
8
+ const closeToOpen: Record<string, string> = {
9
+ '>': '<',
10
+ ')': '(',
11
+ ']': '[',
12
+ '}': '{',
13
+ }
14
+ const opens = new Set(Object.values(closeToOpen))
15
+
16
+ export interface BasePair {
17
+ /** 0-based column */
18
+ start: number
19
+ /** 0-based column */
20
+ end: number
21
+ /** the pair crosses a helix rather than nesting inside one */
22
+ pseudoknot: boolean
23
+ }
24
+
25
+ /**
26
+ * The base pairs a WUSS string encodes, as 0-based column indexes. Unmatched
27
+ * brackets are dropped rather than throwing: a hand-edited or truncated
28
+ * alignment should still draw the helices it does close.
29
+ */
30
+ export function parseWuss(ss: string): BasePair[] {
31
+ const stacks = new Map<string, number[]>()
32
+ const pairs: BasePair[] = []
33
+ const push = (key: string, col: number) => {
34
+ const stack = stacks.get(key)
35
+ if (stack) {
36
+ stack.push(col)
37
+ } else {
38
+ stacks.set(key, [col])
39
+ }
40
+ }
41
+ const pop = (key: string, col: number, pseudoknot: boolean) => {
42
+ const start = stacks.get(key)?.pop()
43
+ if (start !== undefined) {
44
+ pairs.push({ start, end: col, pseudoknot })
45
+ }
46
+ }
47
+
48
+ for (let col = 0; col < ss.length; col++) {
49
+ const ch = ss[col]!
50
+ const opener = closeToOpen[ch]
51
+ if (opens.has(ch)) {
52
+ push(ch, col)
53
+ } else if (opener) {
54
+ pop(opener, col, false)
55
+ } else if (ch >= 'A' && ch <= 'Z') {
56
+ push(ch, col)
57
+ } else if (ch >= 'a' && ch <= 'z') {
58
+ pop(ch.toUpperCase(), col, true)
59
+ }
60
+ }
61
+ return pairs.sort((a, b) => a.start - b.start)
62
+ }