react-msaview 8.4.1 → 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 (193) 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/MSAView.js +2 -2
  12. package/dist/components/MSAView.js.map +1 -1
  13. package/dist/components/MSAViewer.d.ts +1892 -7
  14. package/dist/components/MSAViewer.js +144 -202
  15. package/dist/components/MSAViewer.js.map +1 -1
  16. package/dist/components/ResizeHandles.js +4 -3
  17. package/dist/components/ResizeHandles.js.map +1 -1
  18. package/dist/components/Track.js +3 -2
  19. package/dist/components/Track.js.map +1 -1
  20. package/dist/components/controls.d.ts +11 -0
  21. package/dist/components/controls.js +48 -0
  22. package/dist/components/controls.js.map +1 -0
  23. package/dist/components/dialogs/AnnotationFileDialog.js +5 -16
  24. package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -1
  25. package/dist/components/dialogs/ExportSVGDialog.js +5 -7
  26. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  27. package/dist/components/dialogs/HelpDialog.d.ts +6 -0
  28. package/dist/components/dialogs/HelpDialog.js +30 -0
  29. package/dist/components/dialogs/HelpDialog.js.map +1 -0
  30. package/dist/components/header/FileMenu.js +33 -7
  31. package/dist/components/header/FileMenu.js.map +1 -1
  32. package/dist/components/header/Header.js +4 -6
  33. package/dist/components/header/Header.js.map +1 -1
  34. package/dist/components/header/TreeSettingsMenu.d.ts +1 -1
  35. package/dist/components/header/TreeSettingsMenu.js +1 -32
  36. package/dist/components/header/TreeSettingsMenu.js.map +1 -1
  37. package/dist/components/header/ZoomControls.js +28 -44
  38. package/dist/components/header/ZoomControls.js.map +1 -1
  39. package/dist/components/header/settingsMenuItems.d.ts +1 -0
  40. package/dist/components/header/settingsMenuItems.js +48 -1
  41. package/dist/components/header/settingsMenuItems.js.map +1 -1
  42. package/dist/components/import/data/seq2.d.ts +0 -1
  43. package/dist/components/import/data/seq2.js +0 -17
  44. package/dist/components/import/data/seq2.js.map +1 -1
  45. package/dist/components/msa/MSACanvas.js +2 -1
  46. package/dist/components/msa/MSACanvas.js.map +1 -1
  47. package/dist/components/msa/MSACanvasBlock.js +2 -5
  48. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  49. package/dist/components/msa/drawFeatureSpans.d.ts +50 -11
  50. package/dist/components/msa/drawFeatureSpans.js +99 -56
  51. package/dist/components/msa/drawFeatureSpans.js.map +1 -1
  52. package/dist/components/msa/msaRaster.d.ts +14 -1
  53. package/dist/components/msa/msaRaster.js +19 -9
  54. package/dist/components/msa/msaRaster.js.map +1 -1
  55. package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +9 -0
  56. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +19 -8
  57. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  58. package/dist/components/msa/renderHighlights.d.ts +2 -2
  59. package/dist/components/msa/renderHighlights.js +2 -3
  60. package/dist/components/msa/renderHighlights.js.map +1 -1
  61. package/dist/components/msa/renderMSAMouseover.js +2 -1
  62. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  63. package/dist/components/renderCtx.d.ts +1 -1
  64. package/dist/components/rowpanels/renderFeaturePanel.d.ts +5 -1
  65. package/dist/components/rowpanels/renderFeaturePanel.js +12 -5
  66. package/dist/components/rowpanels/renderFeaturePanel.js.map +1 -1
  67. package/dist/components/tracks/drawTracks.d.ts +1 -53
  68. package/dist/components/tracks/drawTracks.js +5 -5
  69. package/dist/components/tracks/drawTracks.js.map +1 -1
  70. package/dist/components/tree/CladeLabels.d.ts +1 -1
  71. package/dist/components/tree/CladeLabels.js +22 -34
  72. package/dist/components/tree/CladeLabels.js.map +1 -1
  73. package/dist/components/tree/TreeCanvas.js +4 -4
  74. package/dist/components/tree/TreeCanvas.js.map +1 -1
  75. package/dist/components/tree/TreeCanvasBlock.js +4 -4
  76. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  77. package/dist/components/tree/TreeOverview.js +10 -5
  78. package/dist/components/tree/TreeOverview.js.map +1 -1
  79. package/dist/components/tree/TreePanel.js +6 -2
  80. package/dist/components/tree/TreePanel.js.map +1 -1
  81. package/dist/components/tree/TreeRuler.js +2 -2
  82. package/dist/components/tree/TreeRuler.js.map +1 -1
  83. package/dist/components/tree/cladeBrackets.d.ts +22 -17
  84. package/dist/components/tree/cladeBrackets.js +40 -9
  85. package/dist/components/tree/cladeBrackets.js.map +1 -1
  86. package/dist/components/tree/clickMap.d.ts +2 -0
  87. package/dist/components/tree/clickMap.js.map +1 -1
  88. package/dist/components/tree/ellipsize.d.ts +8 -0
  89. package/dist/components/tree/ellipsize.js +20 -0
  90. package/dist/components/tree/ellipsize.js.map +1 -0
  91. package/dist/components/tree/renderTreeCanvas.js +30 -32
  92. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  93. package/dist/components/tree/renderTreeMouseover.js +1 -1
  94. package/dist/components/tree/renderTreeMouseover.js.map +1 -1
  95. package/dist/components/tree/renderTreeOverview.js +4 -11
  96. package/dist/components/tree/renderTreeOverview.js.map +1 -1
  97. package/dist/constants.d.ts +6 -3
  98. package/dist/constants.js +16 -6
  99. package/dist/constants.js.map +1 -1
  100. package/dist/element.js +3 -6
  101. package/dist/element.js.map +1 -1
  102. package/dist/exportLayout.d.ts +39 -0
  103. package/dist/exportLayout.js +78 -0
  104. package/dist/exportLayout.js.map +1 -0
  105. package/dist/headlessRenderEnv.d.ts +12 -39
  106. package/dist/headlessRenderEnv.js +18 -12
  107. package/dist/headlessRenderEnv.js.map +1 -1
  108. package/dist/hierarchy.d.ts +5 -8
  109. package/dist/hierarchy.js +5 -79
  110. package/dist/hierarchy.js.map +1 -1
  111. package/dist/index.d.ts +3 -1
  112. package/dist/index.js +4 -1
  113. package/dist/index.js.map +1 -1
  114. package/dist/measureTextCanvas.d.ts +7 -0
  115. package/dist/measureTextCanvas.js +10 -8
  116. package/dist/measureTextCanvas.js.map +1 -1
  117. package/dist/model/treeModel.d.ts +14 -25
  118. package/dist/model/treeModel.js +16 -30
  119. package/dist/model/treeModel.js.map +1 -1
  120. package/dist/model.d.ts +108 -79
  121. package/dist/model.js +258 -209
  122. package/dist/model.js.map +1 -1
  123. package/dist/renderToSvg.d.ts +2 -7
  124. package/dist/renderToSvg.js +49 -135
  125. package/dist/renderToSvg.js.map +1 -1
  126. package/dist/svgTestUtil.d.ts +48 -48
  127. package/dist/tipLabels.d.ts +8 -0
  128. package/dist/tipLabels.js +46 -0
  129. package/dist/tipLabels.js.map +1 -0
  130. package/dist/types.d.ts +0 -1
  131. package/dist/version.d.ts +1 -1
  132. package/dist/version.js +1 -1
  133. package/dist/viewerSnapshot.d.ts +169 -0
  134. package/dist/viewerSnapshot.js +100 -0
  135. package/dist/viewerSnapshot.js.map +1 -0
  136. package/package.json +3 -3
  137. package/src/colorSchemes.ts +1 -4
  138. package/src/columnCounts.ts +1 -1
  139. package/src/components/Loading.tsx +19 -12
  140. package/src/components/MSAView.tsx +2 -2
  141. package/src/components/MSAViewer.tsx +276 -256
  142. package/src/components/ResizeHandles.tsx +10 -3
  143. package/src/components/Track.tsx +3 -2
  144. package/src/components/controls.ts +61 -0
  145. package/src/components/dialogs/AnnotationFileDialog.tsx +5 -21
  146. package/src/components/dialogs/ExportSVGDialog.tsx +8 -19
  147. package/src/components/dialogs/HelpDialog.tsx +79 -0
  148. package/src/components/header/FileMenu.tsx +33 -7
  149. package/src/components/header/Header.tsx +4 -6
  150. package/src/components/header/TreeSettingsMenu.tsx +2 -32
  151. package/src/components/header/ZoomControls.tsx +30 -66
  152. package/src/components/header/settingsMenuItems.ts +67 -0
  153. package/src/components/import/data/seq2.ts +0 -18
  154. package/src/components/msa/MSACanvas.tsx +2 -1
  155. package/src/components/msa/MSACanvasBlock.tsx +2 -6
  156. package/src/components/msa/drawFeatureSpans.ts +160 -79
  157. package/src/components/msa/msaRaster.ts +27 -9
  158. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +39 -24
  159. package/src/components/msa/renderHighlights.ts +2 -3
  160. package/src/components/msa/renderMSAMouseover.ts +5 -1
  161. package/src/components/renderCtx.ts +0 -1
  162. package/src/components/rowpanels/renderFeaturePanel.ts +26 -7
  163. package/src/components/tracks/drawTracks.ts +5 -5
  164. package/src/components/tree/CladeLabels.tsx +30 -54
  165. package/src/components/tree/TreeCanvas.tsx +10 -4
  166. package/src/components/tree/TreeCanvasBlock.tsx +4 -4
  167. package/src/components/tree/TreeOverview.tsx +10 -5
  168. package/src/components/tree/TreePanel.tsx +8 -2
  169. package/src/components/tree/TreeRuler.tsx +7 -3
  170. package/src/components/tree/cladeBrackets.ts +59 -19
  171. package/src/components/tree/clickMap.ts +2 -0
  172. package/src/components/tree/ellipsize.ts +24 -0
  173. package/src/components/tree/renderTreeCanvas.ts +37 -36
  174. package/src/components/tree/renderTreeMouseover.ts +1 -1
  175. package/src/components/tree/renderTreeOverview.ts +7 -14
  176. package/src/constants.ts +17 -6
  177. package/src/element.ts +3 -7
  178. package/src/exportLayout.ts +134 -0
  179. package/src/headlessRenderEnv.ts +24 -11
  180. package/src/hierarchy.ts +5 -88
  181. package/src/index.ts +5 -5
  182. package/src/measureTextCanvas.ts +10 -8
  183. package/src/model/treeModel.ts +17 -34
  184. package/src/model.ts +296 -243
  185. package/src/renderToSvg.tsx +72 -209
  186. package/src/tipLabels.ts +51 -0
  187. package/src/types.ts +0 -1
  188. package/src/version.ts +1 -1
  189. package/src/viewerSnapshot.ts +147 -0
  190. package/dist/components/header/ZoomMenu.d.ts +0 -6
  191. package/dist/components/header/ZoomMenu.js +0 -37
  192. package/dist/components/header/ZoomMenu.js.map +0 -1
  193. 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
- treeAreaWidth,
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 = treeAreaWidth + 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({
@@ -195,18 +87,9 @@ function MsaSvg({
195
87
  layers: LayerMap
196
88
  }) {
197
89
  const { width, trackHeight, includeMinimap, legendWidth, topHeight } = layout
198
- const { treeAreaWidth, rowPanelsWidth } = model
199
- const totalWidth = width + legendWidth
90
+ const { effectiveTreeAreaWidth, rowPanelsWidth } = model
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
@@ -241,7 +127,7 @@ function MsaSvg({
241
127
  {includeMinimap ? (
242
128
  <g
243
129
  id="minimap-panel"
244
- transform={`translate(${treeAreaWidth + rowPanelsWidth} 0)`}
130
+ transform={`translate(${effectiveTreeAreaWidth + rowPanelsWidth} 0)`}
245
131
  >
246
132
  <MinimapSVG model={model} theme={theme} />
247
133
  </g>
@@ -288,20 +174,20 @@ function TreeOverviewSVG({
288
174
  Context: typeof ContextType
289
175
  layers: LayerMap
290
176
  }) {
291
- const { treeAreaWidth, overviewHeight, id } = model
292
- const ctx = new Context(treeAreaWidth, overviewHeight)
177
+ const { effectiveTreeAreaWidth, overviewHeight, id } = model
178
+ const ctx = new Context(effectiveTreeAreaWidth, overviewHeight)
293
179
  renderTreeOverview({
294
180
  model,
295
181
  ctx,
296
182
  theme,
297
- width: treeAreaWidth,
183
+ width: effectiveTreeAreaWidth,
298
184
  height: overviewHeight,
299
185
  })
300
186
  return (
301
187
  <ClipGroup
302
188
  panelId="tree-overview"
303
189
  clipId={`tree-overview-${id}`}
304
- width={treeAreaWidth}
190
+ width={effectiveTreeAreaWidth}
305
191
  height={overviewHeight}
306
192
  ctx={ctx}
307
193
  layers={layers}
@@ -349,17 +235,16 @@ function RowPanelHeadersSVG({
349
235
  theme: Theme
350
236
  bandHeight: number
351
237
  }) {
352
- const { resolvedRowPanels, treeAreaWidth, fontSize } = model
238
+ const { resolvedRowPanels, effectiveTreeAreaWidth, fontSize } = model
353
239
  const y = bandHeight - 4
354
240
  return resolvedRowPanels.length === 0 ? null : (
355
241
  <g id="rowpanel-headers">
356
242
  {resolvedRowPanels.map(panel => {
357
243
  const across = headerRunsAcross(panel.width)
358
244
  const size = across ? fontSize : Math.min(fontSize, panel.width)
359
- const center = treeAreaWidth + panel.offsetX + panel.width / 2
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,41 +281,32 @@ function CladeLabelsSVG({
405
281
  theme: Theme
406
282
  offsetY: number
407
283
  }) {
408
- const {
409
- resolvedClades,
410
- cladeGutterWidth,
411
- rowHeight,
412
- treeAreaWidth,
413
- fontSize,
414
- } = model
415
- if (cladeGutterWidth === 0) {
416
- return null
417
- }
418
- const gutterLeft = treeAreaWidth - 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 ? treeAreaWidth - left - bracketGap : height
431
- const x = horizontal ? left : left + fontSize
432
- 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
433
294
  return (
434
295
  <text
435
- key={`${rows[0]}-${rows[1]}-${index}`}
296
+ key={box.key}
436
297
  x={x}
437
298
  y={y}
438
- transform={horizontal ? undefined : `rotate(-90 ${x} ${y})`}
299
+ transform={box.horizontal ? undefined : `rotate(-90 ${x} ${y})`}
439
300
  fontSize={fontSize}
440
- fill={markColor ?? theme.palette.text.primary}
301
+ fill={box.color ?? theme.palette.text.primary}
441
302
  >
442
- {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
+ }
443
310
  </text>
444
311
  )
445
312
  })}
@@ -462,9 +329,7 @@ function LegendSVG({
462
329
  const boxHeight = legendHeight(rows)
463
330
  // the column is capped at LEGEND_MAX_W, so a name too long for it has to be
464
331
  // clipped here -- text that overruns the reserved width runs off the figure
465
- const maxChars = Math.floor(
466
- (width - LEGEND_TEXT_X - LEGEND_PAD) / LEGEND_CHAR_W,
467
- )
332
+ const room = width - LEGEND_TEXT_X - LEGEND_PAD
468
333
  return (
469
334
  <g>
470
335
  <rect
@@ -497,9 +362,7 @@ function LegendSVG({
497
362
  fontWeight={row.color ? undefined : 'bold'}
498
363
  fill={theme.palette.text.primary}
499
364
  >
500
- {row.label.length > maxChars
501
- ? `${row.label.slice(0, Math.max(1, maxChars - 1))}…`
502
- : row.label}
365
+ {ellipsize(t => t.length * LEGEND_CHAR_W, row.label, room).text}
503
366
  </text>
504
367
  </g>
505
368
  )
@@ -520,9 +383,9 @@ interface LayerProps {
520
383
 
521
384
  function CoreRendering({ model, theme, layout, Context, layers }: LayerProps) {
522
385
  const { contentHeight, offsetX, offsetY, msaAreaWidth } = layout
523
- const { treeAreaWidth, rowPanelsWidth, id } = model
386
+ const { effectiveTreeAreaWidth, rowPanelsWidth, id } = model
524
387
 
525
- const treeCtx = new Context(treeAreaWidth, contentHeight)
388
+ const treeCtx = new Context(effectiveTreeAreaWidth, contentHeight)
526
389
  renderTreeCanvas({
527
390
  model,
528
391
  theme,
@@ -588,7 +451,7 @@ function CoreRendering({ model, theme, layout, Context, layers }: LayerProps) {
588
451
  <ClipGroup
589
452
  panelId="tree-panel"
590
453
  clipId={`tree-${id}`}
591
- width={treeAreaWidth}
454
+ width={effectiveTreeAreaWidth}
592
455
  height={contentHeight}
593
456
  ctx={treeCtx}
594
457
  layers={layers}
@@ -600,7 +463,7 @@ function CoreRendering({ model, theme, layout, Context, layers }: LayerProps) {
600
463
  clipId={`rowpanels-${id}`}
601
464
  width={rowPanelsWidth}
602
465
  height={contentHeight}
603
- transform={`translate(${treeAreaWidth} 0)`}
466
+ transform={`translate(${effectiveTreeAreaWidth} 0)`}
604
467
  ctx={rowPanelsCtx}
605
468
  layers={layers}
606
469
  />
@@ -610,7 +473,7 @@ function CoreRendering({ model, theme, layout, Context, layers }: LayerProps) {
610
473
  clipId={`msa-${id}`}
611
474
  width={msaAreaWidth}
612
475
  height={contentHeight}
613
- transform={`translate(${treeAreaWidth + rowPanelsWidth} 0)`}
476
+ transform={`translate(${effectiveTreeAreaWidth + rowPanelsWidth} 0)`}
614
477
  ctx={msaCtx}
615
478
  layers={layers}
616
479
  underlay={raster}
@@ -620,9 +483,8 @@ function CoreRendering({ model, theme, layout, Context, layers }: LayerProps) {
620
483
  }
621
484
 
622
485
  /**
623
- * The alignment background as a single <image>, under the live canvas's
624
- * conditions: no domain overlay boxes, background coloring on, and a canvas
625
- * that reads back.
486
+ * The alignment background as a single <image>, wherever the live canvas
487
+ * draws the raster.
626
488
  *
627
489
  * The vector path's <rect> per cell exhausts the heap on a real alignment. The
628
490
  * image is one pixel per cell, and image-rendering keeps the upscaled cell
@@ -638,8 +500,8 @@ function rasterBackground({
638
500
  layout: Layout
639
501
  }) {
640
502
  const { contentHeight, offsetX, offsetY, msaAreaWidth } = layout
641
- const { colWidth, rowHeight, actuallyShowDomains, bgColor } = model
642
- if (actuallyShowDomains || !bgColor || !rasterSupported()) {
503
+ const { colWidth, rowHeight } = model
504
+ if (!rasterApplies(model)) {
643
505
  return undefined
644
506
  }
645
507
  const { xStart, xEnd } = visibleColRange({
@@ -677,7 +539,7 @@ function rasterBackground({
677
539
 
678
540
  function TrackRendering({ model, theme, layout, Context, layers }: LayerProps) {
679
541
  const { trackHeight, offsetX, msaAreaWidth } = layout
680
- const { treeAreaWidth, rowPanelsWidth, id } = model
542
+ const { effectiveTreeAreaWidth, rowPanelsWidth, id } = model
681
543
 
682
544
  const ctx = new Context(msaAreaWidth, trackHeight)
683
545
  renderAllTracks({
@@ -692,7 +554,7 @@ function TrackRendering({ model, theme, layout, Context, layers }: LayerProps) {
692
554
  return (
693
555
  <>
694
556
  <TrackLabelsSVG model={model} theme={theme} />
695
- <g transform={`translate(${treeAreaWidth + rowPanelsWidth} 0)`}>
557
+ <g transform={`translate(${effectiveTreeAreaWidth + rowPanelsWidth} 0)`}>
696
558
  <ClipGroup
697
559
  panelId="tracks-panel"
698
560
  clipId={`tracks-${id}`}
@@ -716,7 +578,7 @@ function TrackLabelsSVG({
716
578
  model: MsaViewModel
717
579
  theme: Theme
718
580
  }) {
719
- const { turnedOnTracks, treeAreaWidth, fontSize, drawLabels } = model
581
+ const { turnedOnTracks, effectiveTreeAreaWidth, fontSize, drawLabels } = model
720
582
  if (!drawLabels) {
721
583
  return null
722
584
  }
@@ -726,7 +588,8 @@ function TrackLabelsSVG({
726
588
  const top = turnedOnTracks
727
589
  .slice(0, i)
728
590
  .reduce((acc, t) => acc + t.model.height, 0)
729
- 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 }
730
593
  })
731
594
  return (
732
595
  <>
@@ -734,7 +597,7 @@ function TrackLabelsSVG({
734
597
  return size < 5 ? null : (
735
598
  <text
736
599
  key={id}
737
- x={treeAreaWidth - 4}
600
+ x={effectiveTreeAreaWidth - 4}
738
601
  y={baseline}
739
602
  fontSize={size}
740
603
  textAnchor="end"
@@ -0,0 +1,51 @@
1
+ import { measureTextCanvas } from './measureTextCanvas.ts'
2
+
3
+ import type { NodeWithIds } from './types.ts'
4
+
5
+ // A label's width follows its length closely enough that the widest one is
6
+ // among the longest few, so a 230k-tip tree measures this many strings.
7
+ const measuredLabels = 100
8
+
9
+ /** the leaf names of a tree, and its depth in levels from the root */
10
+ export function treeTips(tree: NodeWithIds) {
11
+ const names: string[] = []
12
+ let depth = 0
13
+ const stack: [NodeWithIds, number][] = [[tree, 0]]
14
+ for (let entry = stack.pop(); entry; entry = stack.pop()) {
15
+ const [node, level] = entry
16
+ const { children } = node
17
+ if (children.length > 0) {
18
+ for (const child of children) {
19
+ stack.push([child, level + 1])
20
+ }
21
+ } else {
22
+ names.push(node.name)
23
+ depth = Math.max(depth, level)
24
+ }
25
+ }
26
+ return { names, depth }
27
+ }
28
+
29
+ /** the width of the widest label at `fontSize`, measuring the longest few */
30
+ export function widestLabel(labels: string[], fontSize: number) {
31
+ const counts: number[] = []
32
+ for (const label of labels) {
33
+ counts[label.length] = (counts[label.length] ?? 0) + 1
34
+ }
35
+ let shortest = counts.length
36
+ for (let seen = 0; shortest > 0 && seen < measuredLabels;) {
37
+ shortest--
38
+ seen += counts[shortest] ?? 0
39
+ }
40
+ let widest = 0
41
+ let ties = 0
42
+ for (const label of labels) {
43
+ if (
44
+ label.length > shortest ||
45
+ (label.length === shortest && ties++ < measuredLabels)
46
+ ) {
47
+ widest = Math.max(widest, measureTextCanvas(label, fontSize))
48
+ }
49
+ }
50
+ return widest
51
+ }
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 = '8.4.1'
1
+ export const version = '9.1.0'