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
@@ -6,7 +6,7 @@ import { when } from 'mobx'
6
6
  import { visibleRowRange } from './components/getVisibleLeaves.ts'
7
7
  import MinimapSVG from './components/minimap/MinimapSVG.tsx'
8
8
  import { legendRows } from './components/msa/legendRows.ts'
9
- import { rasterImageHref, rasterSupported } from './components/msa/msaRaster.ts'
9
+ import { rasterApplies, rasterImageHref } from './components/msa/msaRaster.ts'
10
10
  import { renderBoxFeatureCanvasBlock } from './components/msa/renderBoxFeatureCanvasBlock.ts'
11
11
  import { renderPersistentHighlights } from './components/msa/renderHighlights.ts'
12
12
  import { renderMSABlock } from './components/msa/renderMSABlock.ts'
@@ -15,141 +15,32 @@ import { headerRunsAcross } from './components/rowpanels/headerLayout.ts'
15
15
  import { renderRowPanels } from './components/rowpanels/renderRowPanel.ts'
16
16
  import { renderAllTracks } from './components/tracks/drawTracks.ts'
17
17
  import ScaleBarSVG from './components/tree/ScaleBarSVG.tsx'
18
- import {
19
- bracketGap,
20
- cladeHeight,
21
- cladeInGutter,
22
- cladeLabelHorizontal,
23
- cladeLabelOffset,
24
- } from './components/tree/cladeBrackets.ts'
18
+ import { cladeLabelBoxes } from './components/tree/cladeBrackets.ts'
19
+ import { ellipsize } from './components/tree/ellipsize.ts'
25
20
  import { renderTreeCanvas } from './components/tree/renderTreeCanvas.ts'
26
21
  import { renderTreeOverview } from './components/tree/renderTreeOverview.ts'
27
22
  import { treeScaleBarHeight } from './constants.ts'
23
+ import {
24
+ LEGEND_CHAR_W,
25
+ LEGEND_FONT,
26
+ LEGEND_PAD,
27
+ LEGEND_ROW_H,
28
+ LEGEND_SWATCH,
29
+ LEGEND_TEXT_X,
30
+ exportPageSize,
31
+ getLayout,
32
+ legendHeight,
33
+ } from './exportLayout.ts'
28
34
  import { measureTextCanvas } from './measureTextCanvas.ts'
29
35
  import { renderToStaticMarkup, svgSafeColors } from './renderToStaticMarkup.ts'
30
36
  import { outlineColor } from './util.ts'
31
37
 
32
- import type { LegendRow } from './components/msa/legendRows.ts'
38
+ import type { ExportSvgOptions, Layout } from './exportLayout.ts'
33
39
  import type { MsaViewModel } from './model.ts'
34
40
  import type { Context as ContextType } from '@jbrowse/svgcanvas'
35
41
  import type { Theme } from '@mui/material'
36
42
 
37
- export interface ExportSvgOptions {
38
- theme: Theme
39
- includeMinimap?: boolean
40
- includeTracks?: boolean
41
- exportType: 'entire' | 'viewport'
42
- }
43
-
44
- // legend geometry, shared by the width calculation and the renderer so the
45
- // reserved column on the right exactly fits the drawn legends
46
- const LEGEND_PAD = 8
47
- const LEGEND_ROW_H = 16
48
- const LEGEND_SWATCH = 12
49
- const LEGEND_FONT = 12
50
- const LEGEND_CHAR_W = 7
51
-
52
- // the width of a character as a fraction of the font size, close enough for a
53
- // sans-serif average
54
- const CHAR_WIDTH = 0.6
55
-
56
- const LEGEND_MAX_W = 360
57
- const LEGEND_TEXT_X = LEGEND_PAD + LEGEND_SWATCH + 6
58
-
59
- function getLegendWidth(rows: LegendRow[]) {
60
- const maxLen = rows.reduce((a, r) => Math.max(a, r.label.length), 0)
61
- const w = LEGEND_TEXT_X + LEGEND_PAD + maxLen * LEGEND_CHAR_W
62
- return Math.min(LEGEND_MAX_W, Math.max(120, w))
63
- }
64
-
65
- function legendHeight(rows: LegendRow[]) {
66
- return LEGEND_PAD * 2 + rows.length * LEGEND_ROW_H
67
- }
68
-
69
- // resolved sizes/offsets (in svg user units) for the chosen export, shared by
70
- // every layer; 'entire' renders the whole alignment unscrolled, 'viewport'
71
- // mirrors the live scroll position
72
- interface Layout {
73
- width: number
74
- msaAreaWidth: number
75
- height: number
76
- contentHeight: number
77
- trackHeight: number
78
- // the band the minimap, the row panel headers and the tree overview stacked
79
- // on the scale bar share across the top
80
- topHeight: number
81
- offsetX: number
82
- offsetY: number
83
- includeMinimap: boolean
84
- legendWidth: number
85
- }
86
-
87
- function getLayout(model: MsaViewModel, opts: ExportSvgOptions): Layout {
88
- const {
89
- scrollX,
90
- scrollY,
91
- totalWidth,
92
- totalHeight,
93
- effectiveTreeAreaWidth,
94
- rowPanelsWidth,
95
- rowPanelsHeaderHeight,
96
- totalTrackAreaHeight,
97
- minimapHeight,
98
- msaAreaHeight,
99
- msaCanvasWidth,
100
- showHorizontalScrollbar,
101
- treeOverviewHeight,
102
- treeScaleBar,
103
- } = model
104
- const trackHeight = opts.includeTracks ? totalTrackAreaHeight : 0
105
- // the minimap reflects the live viewport scroll position, so it's only
106
- // meaningful for a viewport export, never for the entire alignment -- and
107
- // only when the live view shows one at all, since a minimap over an alignment
108
- // that already fits marks a viewport wider than the bar
109
- const includeMinimap =
110
- opts.exportType === 'viewport' &&
111
- !!opts.includeMinimap &&
112
- showHorizontalScrollbar
113
- const rows = legendRows(model.legends)
114
- const legendWidth = rows.length > 0 ? getLegendWidth(rows) : 0
115
- const topHeight = Math.max(
116
- includeMinimap ? minimapHeight : 0,
117
- rowPanelsHeaderHeight,
118
- treeOverviewHeight + (treeScaleBar ? treeScaleBarHeight : 0),
119
- )
120
- const gutterWidth = effectiveTreeAreaWidth + rowPanelsWidth
121
-
122
- // width stays content-only; the legend occupies an extra column added at the
123
- // svg root in MsaSvg. The viewport export takes the alignment canvas's own
124
- // size rather than the widget's -- the widget box also covers the header, the
125
- // resize handle and the scrollbars, and rendering that box exports rows and
126
- // columns the scrollbars hide on screen
127
- return opts.exportType === 'entire'
128
- ? {
129
- width: totalWidth + gutterWidth,
130
- msaAreaWidth: totalWidth,
131
- height: totalHeight + trackHeight + topHeight,
132
- contentHeight: totalHeight,
133
- trackHeight,
134
- topHeight,
135
- offsetX: 0,
136
- offsetY: 0,
137
- includeMinimap,
138
- legendWidth,
139
- }
140
- : {
141
- width: msaCanvasWidth + gutterWidth,
142
- msaAreaWidth: msaCanvasWidth,
143
- height: msaAreaHeight + topHeight + trackHeight,
144
- contentHeight: msaAreaHeight,
145
- trackHeight,
146
- topHeight,
147
- offsetX: -scrollX,
148
- offsetY: -scrollY,
149
- includeMinimap,
150
- legendWidth,
151
- }
152
- }
43
+ export type { ExportSvgOptions } from './exportLayout.ts'
153
44
 
154
45
  export async function renderToSvg(model: MsaViewModel, opts: ExportSvgOptions) {
155
46
  // dataInitialized stays false while the model holds a load error, so waiting
@@ -173,12 +64,13 @@ export async function renderToSvg(model: MsaViewModel, opts: ExportSvgOptions) {
173
64
  layers={layers}
174
65
  />,
175
66
  )
67
+ // innerHTML serializes as HTML, and an XML parser rejects its  
176
68
  return svgSafeColors(
177
69
  markup.replaceAll(/<g data-layer="([^"]+)"><\/g>/g, (match, id: string) => {
178
70
  const ctx = layers.get(id)
179
71
  return ctx ? `<g id="${id}">${ctx.getSvg().innerHTML}</g>` : match
180
72
  }),
181
- )
73
+ ).replaceAll('&nbsp;', '&#160;')
182
74
  }
183
75
 
184
76
  function MsaSvg({
@@ -196,17 +88,8 @@ function MsaSvg({
196
88
  }) {
197
89
  const { width, trackHeight, includeMinimap, legendWidth, topHeight } = layout
198
90
  const { effectiveTreeAreaWidth, rowPanelsWidth } = model
199
- const totalWidth = width + legendWidth
91
+ const { width: totalWidth, height } = exportPageSize(model, layout)
200
92
  const legendTop = topHeight
201
- // a short alignment with many legend entries has a key taller than its rows,
202
- // and the page has to hold the whole key
203
- const height =
204
- legendWidth > 0
205
- ? Math.max(
206
- layout.height,
207
- legendTop + legendHeight(legendRows(model.legends)),
208
- )
209
- : layout.height
210
93
  const props = { Context, model, theme, layout, layers }
211
94
 
212
95
  const body = (
@@ -227,6 +110,9 @@ function MsaSvg({
227
110
  xmlns="http://www.w3.org/2000/svg"
228
111
  xmlnsXlink="http://www.w3.org/1999/xlink"
229
112
  viewBox={`0 0 ${totalWidth} ${height}`}
113
+ // the canvas layers name sans-serif on each glyph; the legend, labels
114
+ // and headers inherit it here instead of an editor's default serif
115
+ fontFamily="sans-serif"
230
116
  >
231
117
  {/* every layer below draws in the theme's colors, so the page has to be
232
118
  the theme's background too -- a hardcoded white one turns a dark-theme
@@ -359,7 +245,6 @@ function RowPanelHeadersSVG({
359
245
  const center = effectiveTreeAreaWidth + panel.offsetX + panel.width / 2
360
246
  const x = across ? center : center + size / 3
361
247
  const room = across ? panel.width : y - LEGEND_PAD
362
- const maxChars = Math.floor(room / (size * CHAR_WIDTH))
363
248
  return size < 5 ? null : (
364
249
  <text
365
250
  key={panel.id}
@@ -371,9 +256,10 @@ function RowPanelHeadersSVG({
371
256
  fontSize={size}
372
257
  fill={theme.palette.text.primary}
373
258
  >
374
- {panel.header.length > maxChars
375
- ? `${panel.header.slice(0, Math.max(1, maxChars - 1))}…`
376
- : panel.header}
259
+ {
260
+ ellipsize(t => measureTextCanvas(t, size), panel.header, room)
261
+ .text
262
+ }
377
263
  </text>
378
264
  )
379
265
  })}
@@ -381,21 +267,11 @@ function RowPanelHeadersSVG({
381
267
  )
382
268
  }
383
269
 
384
- // A label past the room it has, cut to what fits with an ellipsis. The measure
385
- // is the one cladeGutterWidth sizes the gutter with, so a label the gutter
386
- // holds keeps every character.
387
- function clipCladeLabel(label: string, room: number, fontSize: number) {
388
- const width = measureTextCanvas(label, fontSize)
389
- if (width <= room) {
390
- return label
391
- }
392
- const fits = Math.floor((label.length * room) / width)
393
- return `${label.slice(0, Math.max(1, fits - 1))}…`
394
- }
395
-
396
270
  // Each bracket's label in the gutter beside its bar, where the live view puts
397
271
  // it (see CladeLabels). The canvas layer draws the bar; a label longer than the
398
272
  // gutter or the clade is clipped, since text that overruns runs off the figure.
273
+ // cladeGutterWidth sizes the gutter with the same measureTextCanvas, so a label
274
+ // the gutter holds keeps every character.
399
275
  function CladeLabelsSVG({
400
276
  model,
401
277
  theme,
@@ -405,43 +281,32 @@ function CladeLabelsSVG({
405
281
  theme: Theme
406
282
  offsetY: number
407
283
  }) {
408
- const {
409
- resolvedClades,
410
- cladeGutterWidth,
411
- rowHeight,
412
- effectiveTreeAreaWidth,
413
- fontSize,
414
- } = model
415
- if (cladeGutterWidth === 0) {
416
- return null
417
- }
418
- const gutterLeft = effectiveTreeAreaWidth - cladeGutterWidth
419
- return (
284
+ const { fontSize } = model
285
+ const boxes = cladeLabelBoxes(model)
286
+ return boxes.length === 0 ? null : (
420
287
  <g id="clade-labels">
421
- {resolvedClades.map((clade, index) => {
422
- const { label, markColor, rows } = clade
423
- if (!label || !cladeInGutter(clade)) {
424
- return null
425
- }
426
- const left = gutterLeft + cladeLabelOffset(clade)
427
- const top = rows[0] * rowHeight - offsetY
428
- const height = cladeHeight(clade, rowHeight)
429
- const horizontal = cladeLabelHorizontal(clade, rowHeight, fontSize)
430
- const room = horizontal
431
- ? effectiveTreeAreaWidth - left - bracketGap
432
- : height
433
- const x = horizontal ? left : left + fontSize
434
- const y = horizontal ? top + height / 2 + fontSize / 3 : top + height
288
+ {boxes.map(box => {
289
+ const top = box.top - offsetY
290
+ const x = box.horizontal ? box.left : box.left + fontSize
291
+ const y = box.horizontal
292
+ ? top + box.height / 2 + fontSize / 3
293
+ : top + box.height
435
294
  return (
436
295
  <text
437
- key={`${rows[0]}-${rows[1]}-${index}`}
296
+ key={box.key}
438
297
  x={x}
439
298
  y={y}
440
- transform={horizontal ? undefined : `rotate(-90 ${x} ${y})`}
299
+ transform={box.horizontal ? undefined : `rotate(-90 ${x} ${y})`}
441
300
  fontSize={fontSize}
442
- fill={markColor ?? theme.palette.text.primary}
301
+ fill={box.color ?? theme.palette.text.primary}
443
302
  >
444
- {clipCladeLabel(label, room, fontSize)}
303
+ {
304
+ ellipsize(
305
+ t => measureTextCanvas(t, fontSize),
306
+ box.label,
307
+ box.horizontal ? box.width : box.height,
308
+ ).text
309
+ }
445
310
  </text>
446
311
  )
447
312
  })}
@@ -464,9 +329,7 @@ function LegendSVG({
464
329
  const boxHeight = legendHeight(rows)
465
330
  // the column is capped at LEGEND_MAX_W, so a name too long for it has to be
466
331
  // clipped here -- text that overruns the reserved width runs off the figure
467
- const maxChars = Math.floor(
468
- (width - LEGEND_TEXT_X - LEGEND_PAD) / LEGEND_CHAR_W,
469
- )
332
+ const room = width - LEGEND_TEXT_X - LEGEND_PAD
470
333
  return (
471
334
  <g>
472
335
  <rect
@@ -499,9 +362,7 @@ function LegendSVG({
499
362
  fontWeight={row.color ? undefined : 'bold'}
500
363
  fill={theme.palette.text.primary}
501
364
  >
502
- {row.label.length > maxChars
503
- ? `${row.label.slice(0, Math.max(1, maxChars - 1))}…`
504
- : row.label}
365
+ {ellipsize(t => t.length * LEGEND_CHAR_W, row.label, room).text}
505
366
  </text>
506
367
  </g>
507
368
  )
@@ -622,9 +483,8 @@ function CoreRendering({ model, theme, layout, Context, layers }: LayerProps) {
622
483
  }
623
484
 
624
485
  /**
625
- * The alignment background as a single <image>, under the live canvas's
626
- * conditions: no domain overlay boxes, background coloring on, and a canvas
627
- * that reads back.
486
+ * The alignment background as a single <image>, wherever the live canvas
487
+ * draws the raster.
628
488
  *
629
489
  * The vector path's <rect> per cell exhausts the heap on a real alignment. The
630
490
  * image is one pixel per cell, and image-rendering keeps the upscaled cell
@@ -640,8 +500,8 @@ function rasterBackground({
640
500
  layout: Layout
641
501
  }) {
642
502
  const { contentHeight, offsetX, offsetY, msaAreaWidth } = layout
643
- const { colWidth, rowHeight, actuallyShowDomains, bgColor } = model
644
- if (actuallyShowDomains || !bgColor || !rasterSupported()) {
503
+ const { colWidth, rowHeight } = model
504
+ if (!rasterApplies(model)) {
645
505
  return undefined
646
506
  }
647
507
  const { xStart, xEnd } = visibleColRange({
@@ -728,7 +588,8 @@ function TrackLabelsSVG({
728
588
  const top = turnedOnTracks
729
589
  .slice(0, i)
730
590
  .reduce((acc, t) => acc + t.model.height, 0)
731
- return { id, name, size, baseline: top + height / 2 + size / 3 }
591
+ // pinned to the top of the track, as the live view's flex row pins it
592
+ return { id, name, size, baseline: top + size * 0.8 }
732
593
  })
733
594
  return (
734
595
  <>
package/src/types.ts CHANGED
@@ -64,7 +64,6 @@ export interface ArcTrackModel extends BasicTrackModel {
64
64
  }
65
65
 
66
66
  export interface BasicTrack {
67
- ReactComponent: React.FC<any>
68
67
  model: TextTrackModel & BarTrackModel & ArcTrackModel
69
68
  }
70
69
 
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '9.0.0'
1
+ export const version = '9.1.0'
@@ -0,0 +1,147 @@
1
+ import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
2
+
3
+ import type { MSAViewerProps } from './components/MSAViewer.tsx'
4
+ import type { ColorScheme, Highlight } from './types.ts'
5
+
6
+ export function colorSchemeSnapshot(scheme: ColorScheme | undefined) {
7
+ return typeof scheme === 'string'
8
+ ? { colorSchemeName: scheme }
9
+ : scheme
10
+ ? { customColorScheme: scheme.map }
11
+ : {}
12
+ }
13
+
14
+ /**
15
+ * The `highlightColumns` prop as `highlights`: one `{start, end}` per run of
16
+ * adjacent 1-based alignment columns
17
+ */
18
+ export function columnHighlights(columns: number[] = []) {
19
+ const sorted = [...new Set(columns)].sort((a, b) => a - b)
20
+ const runs: { start: number; end: number }[] = []
21
+ for (const column of sorted) {
22
+ const last = runs.at(-1)
23
+ if (last?.end === column - 1) {
24
+ last.end = column
25
+ } else {
26
+ runs.push({ start: column, end: column })
27
+ }
28
+ }
29
+ return runs
30
+ }
31
+
32
+ export function viewerHighlights({
33
+ highlights = [],
34
+ highlightColumns,
35
+ }: Pick<MSAViewerProps, 'highlights' | 'highlightColumns'>): Highlight[] {
36
+ return [...highlights, ...columnHighlights(highlightColumns)]
37
+ }
38
+
39
+ function urlLocation(uri: string | undefined) {
40
+ return uri ? { uri, locationType: 'UriLocation' as const } : undefined
41
+ }
42
+
43
+ /** each filehandle prop, or the URL prop standing in for it */
44
+ export function fileLocations({
45
+ msaFilehandle,
46
+ treeFilehandle,
47
+ gffFilehandle,
48
+ msaUrl,
49
+ treeUrl,
50
+ gffUrl,
51
+ }: MSAViewerProps) {
52
+ return {
53
+ msaFilehandle: msaFilehandle ?? urlLocation(msaUrl),
54
+ treeFilehandle: treeFilehandle ?? urlLocation(treeUrl),
55
+ gffFilehandle: gffFilehandle ?? urlLocation(gffUrl),
56
+ }
57
+ }
58
+
59
+ export function resolveTheme(theme: MSAViewerProps['theme']) {
60
+ return createJBrowseTheme(
61
+ theme === 'dark'
62
+ ? { palette: { mode: 'dark' } }
63
+ : typeof theme === 'object'
64
+ ? theme
65
+ : undefined,
66
+ )
67
+ }
68
+
69
+ /**
70
+ * The model snapshot `MSAViewer` creates from its props. The CLI's `--props`
71
+ * reads the same JSON the R and Python widgets send, so a figure exported
72
+ * headlessly draws what the widget drew.
73
+ */
74
+ export function viewerSnapshot(props: MSAViewerProps) {
75
+ const {
76
+ msa,
77
+ tree,
78
+ gff,
79
+ rowData,
80
+ colorScheme,
81
+ height,
82
+ colWidth,
83
+ rowHeight,
84
+ allowedGappyness,
85
+ highlightColumns,
86
+ highlights,
87
+ features,
88
+ selection,
89
+ clades,
90
+ relativeTo,
91
+ columnTracks,
92
+ residueMappings,
93
+ encodings,
94
+ rowPanels,
95
+ drawTree,
96
+ treeAreaWidth,
97
+ showBranchLen,
98
+ treeOrder,
99
+ treeRoot,
100
+ region,
101
+ residueEncoding,
102
+ } = props
103
+ const { msaFilehandle, treeFilehandle, gffFilehandle } = fileLocations(props)
104
+ return {
105
+ type: 'MsaView' as const,
106
+ ...(msa || tree || gff || rowData
107
+ ? {
108
+ data: {
109
+ msa: msa ?? '',
110
+ tree: tree ?? '',
111
+ gff,
112
+ // the row table is stored as this JSON string, which is what a
113
+ // shared URL and the size limit already cover
114
+ ...(rowData ? { treeMetadata: JSON.stringify(rowData) } : {}),
115
+ },
116
+ }
117
+ : {}),
118
+ ...(msaFilehandle ? { msaFilehandle } : {}),
119
+ ...(treeFilehandle ? { treeFilehandle } : {}),
120
+ ...(gffFilehandle ? { gffFilehandle } : {}),
121
+ ...colorSchemeSnapshot(colorScheme),
122
+ ...(height ? { height } : {}),
123
+ ...(colWidth ? { colWidth } : {}),
124
+ ...(rowHeight ? { rowHeight } : {}),
125
+ ...(allowedGappyness !== undefined ? { allowedGappyness } : {}),
126
+ ...(highlights || highlightColumns
127
+ ? { highlights: viewerHighlights({ highlights, highlightColumns }) }
128
+ : {}),
129
+ ...(features ? { features } : {}),
130
+ ...(selection ? { selection } : {}),
131
+ ...(clades ? { clades } : {}),
132
+ ...(relativeTo ? { relativeTo } : {}),
133
+ ...(columnTracks ? { columnTracks } : {}),
134
+ ...(residueMappings ? { residueMappings } : {}),
135
+ ...(encodings ? { encodings } : {}),
136
+ ...(rowPanels ? { rowPanels } : {}),
137
+ ...(drawTree !== undefined ? { drawTree } : {}),
138
+ ...(treeAreaWidth ? { treeAreaWidth } : {}),
139
+ ...(showBranchLen !== undefined ? { showBranchLen } : {}),
140
+ ...(treeOrder ? { treeOrder } : {}),
141
+ ...(treeRoot ? { treeRoot } : {}),
142
+ ...(region ? { region } : {}),
143
+ ...(residueEncoding !== undefined
144
+ ? { bgColor: residueEncoding === 'fill' }
145
+ : {}),
146
+ }
147
+ }
@@ -1,6 +0,0 @@
1
- import React from 'react';
2
- import type { MsaViewModel } from '../../model.ts';
3
- declare const ZoomMenu: ({ model }: {
4
- model: MsaViewModel;
5
- }) => React.JSX.Element;
6
- export default ZoomMenu;
@@ -1,37 +0,0 @@
1
- import React from 'react';
2
- import CascadingMenuButton from '@jbrowse/core/ui/CascadingMenuButton';
3
- import MoreVert from '@mui/icons-material/MoreVert';
4
- import RestartAlt from '@mui/icons-material/RestartAlt';
5
- import { observer } from 'mobx-react';
6
- const ZoomMenu = observer(function ({ model }) {
7
- return (React.createElement(CascadingMenuButton, { tooltip: "Fit / zoom options", menuItems: [
8
- {
9
- label: 'Fit both vertically/horizontally',
10
- onClick: () => {
11
- model.fit();
12
- },
13
- },
14
- {
15
- label: 'Fit vertically',
16
- onClick: () => {
17
- model.fitVertically();
18
- },
19
- },
20
- {
21
- label: 'Fit horizontally',
22
- onClick: () => {
23
- model.fitHorizontally();
24
- },
25
- },
26
- {
27
- label: 'Reset zoom to default',
28
- icon: RestartAlt,
29
- onClick: () => {
30
- model.resetZoom();
31
- },
32
- },
33
- ] },
34
- React.createElement(MoreVert, null)));
35
- });
36
- export default ZoomMenu;
37
- //# sourceMappingURL=ZoomMenu.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ZoomMenu.js","sourceRoot":"","sources":["../../../src/components/header/ZoomMenu.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,mBAAmB,MAAM,sCAAsC,CAAA;AACtE,OAAO,QAAQ,MAAM,8BAA8B,CAAA;AACnD,OAAO,UAAU,MAAM,gCAAgC,CAAA;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAIrC,MAAM,QAAQ,GAAG,QAAQ,CAAC,UAAU,EAAE,KAAK,EAA2B;IACpE,OAAO,CACL,oBAAC,mBAAmB,IAClB,OAAO,EAAC,oBAAoB,EAC5B,SAAS,EAAE;YACT;gBACE,KAAK,EAAE,kCAAkC;gBACzC,OAAO,EAAE,GAAG,EAAE;oBACZ,KAAK,CAAC,GAAG,EAAE,CAAA;gBACb,CAAC;aACF;YACD;gBACE,KAAK,EAAE,gBAAgB;gBACvB,OAAO,EAAE,GAAG,EAAE;oBACZ,KAAK,CAAC,aAAa,EAAE,CAAA;gBACvB,CAAC;aACF;YACD;gBACE,KAAK,EAAE,kBAAkB;gBACzB,OAAO,EAAE,GAAG,EAAE;oBACZ,KAAK,CAAC,eAAe,EAAE,CAAA;gBACzB,CAAC;aACF;YAED;gBACE,KAAK,EAAE,uBAAuB;gBAC9B,IAAI,EAAE,UAAU;gBAChB,OAAO,EAAE,GAAG,EAAE;oBACZ,KAAK,CAAC,SAAS,EAAE,CAAA;gBACnB,CAAC;aACF;SACF;QAED,oBAAC,QAAQ,OAAG,CACQ,CACvB,CAAA;AACH,CAAC,CAAC,CAAA;AAEF,eAAe,QAAQ,CAAA"}
@@ -1,48 +0,0 @@
1
- import React from 'react'
2
-
3
- import CascadingMenuButton from '@jbrowse/core/ui/CascadingMenuButton'
4
- import MoreVert from '@mui/icons-material/MoreVert'
5
- import RestartAlt from '@mui/icons-material/RestartAlt'
6
- import { observer } from 'mobx-react'
7
-
8
- import type { MsaViewModel } from '../../model.ts'
9
-
10
- const ZoomMenu = observer(function ({ model }: { model: MsaViewModel }) {
11
- return (
12
- <CascadingMenuButton
13
- tooltip="Fit / zoom options"
14
- menuItems={[
15
- {
16
- label: 'Fit both vertically/horizontally',
17
- onClick: () => {
18
- model.fit()
19
- },
20
- },
21
- {
22
- label: 'Fit vertically',
23
- onClick: () => {
24
- model.fitVertically()
25
- },
26
- },
27
- {
28
- label: 'Fit horizontally',
29
- onClick: () => {
30
- model.fitHorizontally()
31
- },
32
- },
33
-
34
- {
35
- label: 'Reset zoom to default',
36
- icon: RestartAlt,
37
- onClick: () => {
38
- model.resetZoom()
39
- },
40
- },
41
- ]}
42
- >
43
- <MoreVert />
44
- </CascadingMenuButton>
45
- )
46
- })
47
-
48
- export default ZoomMenu