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
@@ -3,13 +3,7 @@ import React from 'react'
3
3
  import { useTheme } from '@mui/material'
4
4
  import { observer } from 'mobx-react'
5
5
 
6
- import {
7
- bracketGap,
8
- cladeHeight,
9
- cladeInGutter,
10
- cladeLabelHorizontal,
11
- cladeLabelOffset,
12
- } from './cladeBrackets.ts'
6
+ import { cladeLabelBoxes } from './cladeBrackets.ts'
13
7
 
14
8
  import type { MsaViewModel } from '../../model.ts'
15
9
 
@@ -20,56 +14,38 @@ import type { MsaViewModel } from '../../model.ts'
20
14
  * the same labels with a `rotate(-90)`.
21
15
  */
22
16
  const CladeLabels = observer(function ({ model }: { model: MsaViewModel }) {
23
- const {
24
- resolvedClades,
25
- cladeGutterWidth,
26
- rowHeight,
27
- treeAreaWidth,
28
- fontSize,
29
- } = model
30
17
  const theme = useTheme()
31
- if (cladeGutterWidth === 0) {
32
- return null
33
- }
34
- const gutterLeft = treeAreaWidth - cladeGutterWidth
18
+ const { fontSize } = model
35
19
  return (
36
20
  <>
37
- {resolvedClades.map((clade, index) => {
38
- const { label, markColor, rows } = clade
39
- if (!label || !cladeInGutter(clade)) {
40
- return null
41
- }
42
- const left = gutterLeft + cladeLabelOffset(clade)
43
- const horizontal = cladeLabelHorizontal(clade, rowHeight, fontSize)
44
- return (
45
- <div
46
- key={`${rows[0]}-${rows[1]}-${index}`}
47
- title={label}
48
- style={{
49
- position: 'absolute',
50
- left,
51
- top: rows[0] * rowHeight,
52
- width: treeAreaWidth - left - bracketGap,
53
- height: cladeHeight(clade, rowHeight),
54
- display: 'flex',
55
- alignItems: 'center',
56
- color: markColor ?? theme.palette.text.primary,
57
- fontSize,
58
- overflow: 'hidden',
59
- textOverflow: 'ellipsis',
60
- whiteSpace: 'nowrap',
61
- pointerEvents: 'none',
62
- // vertical-rl reads top to bottom, and the half turn makes it
63
- // read upwards, which is what rotate(-90) exports
64
- ...(horizontal
65
- ? {}
66
- : { writingMode: 'vertical-rl', transform: 'rotate(180deg)' }),
67
- }}
68
- >
69
- {label}
70
- </div>
71
- )
72
- })}
21
+ {cladeLabelBoxes(model).map(box => (
22
+ <div
23
+ key={box.key}
24
+ title={box.label}
25
+ style={{
26
+ position: 'absolute',
27
+ left: box.left,
28
+ top: box.top,
29
+ width: box.width,
30
+ height: box.height,
31
+ display: 'flex',
32
+ alignItems: 'center',
33
+ color: box.color ?? theme.palette.text.primary,
34
+ fontSize,
35
+ overflow: 'hidden',
36
+ textOverflow: 'ellipsis',
37
+ whiteSpace: 'nowrap',
38
+ pointerEvents: 'none',
39
+ // vertical-rl reads top to bottom, and the half turn makes it
40
+ // read upwards, which is what rotate(-90) exports
41
+ ...(box.horizontal
42
+ ? {}
43
+ : { writingMode: 'vertical-rl', transform: 'rotate(180deg)' }),
44
+ }}
45
+ >
46
+ {box.label}
47
+ </div>
48
+ ))}
73
49
  </>
74
50
  )
75
51
  })
@@ -13,7 +13,13 @@ import type { MsaViewModel } from '../../model.ts'
13
13
 
14
14
  const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
15
15
  const ref = useRef<HTMLDivElement>(null)
16
- const { height, blocksY, treeAreaWidth, scrollY, highResScaleFactor } = model
16
+ const {
17
+ height,
18
+ blocksY,
19
+ effectiveTreeAreaWidth,
20
+ scrollY,
21
+ highResScaleFactor,
22
+ } = model
17
23
  const onScrollY = useCallback(
18
24
  (d: number) => {
19
25
  model.doScrollY(d)
@@ -22,7 +28,7 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
22
28
  )
23
29
  const { onMouseDown, onMouseUp } = useWheelScroll({ ref, onScrollY })
24
30
 
25
- const canvasWidth = treeAreaWidth * highResScaleFactor
31
+ const canvasWidth = effectiveTreeAreaWidth * highResScaleFactor
26
32
  const canvasHeight = height * highResScaleFactor
27
33
  const mouseoverRef = useCanvasAutorun({
28
34
  draw: ctx => {
@@ -43,7 +49,7 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
43
49
  style={{
44
50
  height,
45
51
  position: 'relative',
46
- width: treeAreaWidth,
52
+ width: effectiveTreeAreaWidth,
47
53
  }}
48
54
  >
49
55
  {/* one transform for the whole block set, matching MSACanvas: the tree
@@ -70,7 +76,7 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
70
76
  position: 'absolute',
71
77
  top: 0,
72
78
  left: 0,
73
- width: treeAreaWidth,
79
+ width: effectiveTreeAreaWidth,
74
80
  height,
75
81
  zIndex: 1000,
76
82
  pointerEvents: 'none',
@@ -37,9 +37,9 @@ const TreeCanvasBlock = observer(function ({
37
37
  )
38
38
  const hovered = anchor?.value
39
39
 
40
- const { treeAreaWidth, blockSize, highResScaleFactor } = model
41
- // TreePanel clips to treeAreaWidth, so a wider canvas is wasted backing store
42
- const width = treeAreaWidth
40
+ const { effectiveTreeAreaWidth, blockSize, highResScaleFactor } = model
41
+ // TreePanel clips to the tree area, so a wider canvas is wasted backing store
42
+ const width = effectiveTreeAreaWidth
43
43
  const height = blockSize
44
44
 
45
45
  const canvasWidth = width * highResScaleFactor
@@ -121,7 +121,7 @@ const TreeCanvasBlock = observer(function ({
121
121
 
122
122
  {anchor ? (
123
123
  <BaseTooltip clientPoint={anchor.clientPoint}>
124
- {anchor.value.name}
124
+ {anchor.value.label ?? anchor.value.name}
125
125
  </BaseTooltip>
126
126
  ) : null}
127
127
  </>
@@ -21,10 +21,10 @@ import type { MsaViewModel } from '../../model.ts'
21
21
  * moving across it costs are a blit and two boxes.
22
22
  */
23
23
  const TreeOverview = observer(function ({ model }: { model: MsaViewModel }) {
24
- const { treeAreaWidth, overviewHeight, highResScaleFactor } = model
24
+ const { effectiveTreeAreaWidth, overviewHeight, highResScaleFactor } = model
25
25
  const theme = useTheme()
26
26
  const [hover, setHover] = useState<{ id: string; rows: [number, number] }>()
27
- const width = Math.round(treeAreaWidth * highResScaleFactor)
27
+ const width = Math.round(effectiveTreeAreaWidth * highResScaleFactor)
28
28
  const height = Math.round(overviewHeight * highResScaleFactor)
29
29
 
30
30
  const ref = useCanvasAutorun({
@@ -34,12 +34,17 @@ const TreeOverview = observer(function ({ model }: { model: MsaViewModel }) {
34
34
  const image = treeOverviewImage({
35
35
  model,
36
36
  theme,
37
- width: treeAreaWidth,
37
+ width: effectiveTreeAreaWidth,
38
38
  height: overviewHeight,
39
39
  scale: highResScaleFactor,
40
40
  })
41
41
  ctx.scale(highResScaleFactor, highResScaleFactor)
42
- const box = { model, ctx, width: treeAreaWidth, height: overviewHeight }
42
+ const box = {
43
+ model,
44
+ ctx,
45
+ width: effectiveTreeAreaWidth,
46
+ height: overviewHeight,
47
+ }
43
48
  if (image) {
44
49
  ctx.drawImage(
45
50
  image,
@@ -86,7 +91,7 @@ const TreeOverview = observer(function ({ model }: { model: MsaViewModel }) {
86
91
  display: 'block',
87
92
  flexShrink: 0,
88
93
  cursor: 'pointer',
89
- width: treeAreaWidth,
94
+ width: effectiveTreeAreaWidth,
90
95
  height: overviewHeight,
91
96
  }}
92
97
  onMouseMove={event => {
@@ -7,9 +7,15 @@ import TreeCanvas from './TreeCanvas.tsx'
7
7
  import type { MsaViewModel } from '../../model.ts'
8
8
 
9
9
  const TreePanel = observer(function ({ model }: { model: MsaViewModel }) {
10
- const { treeAreaWidth } = model
10
+ const { effectiveTreeAreaWidth } = model
11
11
  return (
12
- <div style={{ overflow: 'hidden', flexShrink: 0, width: treeAreaWidth }}>
12
+ <div
13
+ style={{
14
+ overflow: 'hidden',
15
+ flexShrink: 0,
16
+ width: effectiveTreeAreaWidth,
17
+ }}
18
+ >
13
19
  <TreeCanvas model={model} />
14
20
  </div>
15
21
  )
@@ -13,13 +13,17 @@ import type { MsaViewModel } from '../../model.ts'
13
13
  // scale bar: without one the branch lengths are drawn to a scale nothing
14
14
  // states.
15
15
  const TreeRuler = observer(({ model }: { model: MsaViewModel }) => {
16
- const { treeAreaWidth, marginLeft, treeScaleBar: bar } = model
16
+ const { effectiveTreeAreaWidth, marginLeft, treeScaleBar: bar } = model
17
17
  const theme = useTheme()
18
18
 
19
19
  return (
20
- <div style={{ flexShrink: 0, width: treeAreaWidth }}>
20
+ <div style={{ flexShrink: 0, width: effectiveTreeAreaWidth }}>
21
21
  {bar ? (
22
- <svg width={treeAreaWidth} height={h} style={{ display: 'block' }}>
22
+ <svg
23
+ width={effectiveTreeAreaWidth}
24
+ height={h}
25
+ style={{ display: 'block' }}
26
+ >
23
27
  <ScaleBarSVG
24
28
  bar={bar}
25
29
  marginLeft={marginLeft}
@@ -1,6 +1,8 @@
1
+ import { labelFitFontSize } from '../../constants.ts'
1
2
  import { measureTextCanvas } from '../../measureTextCanvas.ts'
2
3
 
3
- import type { ResolvedClade } from '../../types.ts'
4
+ import type { MsaViewModel } from '../../model.ts'
5
+ import type { Clade, ResolvedClade } from '../../types.ts'
4
6
 
5
7
  // The bar of the bracket mark and the space around it, in px.
6
8
  export const bracketBarWidth = 3
@@ -11,14 +13,14 @@ export const bracketGap = 4
11
13
  export const maxCladeGutterWidth = 140
12
14
 
13
15
  /** whether a clade draws in the gutter: a bracket, or a highlight with a label */
14
- export function cladeInGutter(clade: ResolvedClade) {
16
+ export function cladeInGutter(clade: Pick<Clade, 'mark' | 'label'>) {
15
17
  return (
16
18
  clade.mark === 'bracket' || (clade.mark === 'highlight' && !!clade.label)
17
19
  )
18
20
  }
19
21
 
20
22
  /** the pixel height of the rows a clade covers */
21
- export function cladeHeight(clade: ResolvedClade, rowHeight: number) {
23
+ function cladeHeight(clade: ResolvedClade, rowHeight: number) {
22
24
  return (clade.rows[1] - clade.rows[0] + 1) * rowHeight
23
25
  }
24
26
 
@@ -26,7 +28,7 @@ export function cladeHeight(clade: ResolvedClade, rowHeight: number) {
26
28
  * Whether a label reads across the rows. It does where they are taller than the
27
29
  * font, and runs up the bar where they are not.
28
30
  */
29
- export function cladeLabelHorizontal(
31
+ function cladeLabelHorizontal(
30
32
  clade: ResolvedClade,
31
33
  rowHeight: number,
32
34
  fontSize: number,
@@ -35,7 +37,7 @@ export function cladeLabelHorizontal(
35
37
  }
36
38
 
37
39
  /** the px between the left edge of the gutter and the label's own column */
38
- export function cladeLabelOffset(clade: ResolvedClade) {
40
+ function cladeLabelOffset(clade: Pick<Clade, 'mark'>) {
39
41
  return clade.mark === 'bracket'
40
42
  ? bracketGap + bracketBarWidth + bracketGap
41
43
  : bracketGap
@@ -43,27 +45,19 @@ export function cladeLabelOffset(clade: ResolvedClade) {
43
45
 
44
46
  /**
45
47
  * The gutter the bracket mark needs at the right of the tree area: the bar plus
46
- * the widest label at the tree font, zero where no clade draws there, and at
47
- * most maxCladeGutterWidth.
48
+ * the widest label read across at labelFitFontSize, zero where no clade draws
49
+ * there, and at most maxCladeGutterWidth. The records size it, so a zoom, a
50
+ * collapse or a focus leaves it as it is, and a renderer clips a label that
51
+ * outgrows it.
48
52
  */
49
- export function cladeGutterWidth({
50
- clades,
51
- rowHeight,
52
- fontSize,
53
- }: {
54
- clades: ResolvedClade[]
55
- rowHeight: number
56
- fontSize: number
57
- }) {
53
+ export function cladeGutterWidth(clades: Pick<Clade, 'mark' | 'label'>[]) {
58
54
  let widest = 0
59
55
  for (const clade of clades) {
60
56
  if (!cladeInGutter(clade)) {
61
57
  continue
62
58
  }
63
59
  const text = clade.label
64
- ? cladeLabelHorizontal(clade, rowHeight, fontSize)
65
- ? measureTextCanvas(clade.label, fontSize)
66
- : fontSize
60
+ ? measureTextCanvas(clade.label, labelFitFontSize)
67
61
  : 0
68
62
  widest = Math.max(widest, cladeLabelOffset(clade) + text + bracketGap)
69
63
  }
@@ -71,3 +65,49 @@ export function cladeGutterWidth({
71
65
  // the measurement here sized the gutter for
72
66
  return Math.min(maxCladeGutterWidth, Math.ceil(widest))
73
67
  }
68
+
69
+ /**
70
+ * The box each gutter label takes, in unscrolled tree-area px: from the
71
+ * label's column to the right edge of the gutter, across the clade's rows. The
72
+ * live view sets DOM text in it, and the SVG export a `<text>`.
73
+ */
74
+ export function cladeLabelBoxes(
75
+ model: Pick<
76
+ MsaViewModel,
77
+ | 'resolvedClades'
78
+ | 'cladeGutterWidth'
79
+ | 'rowHeight'
80
+ | 'effectiveTreeAreaWidth'
81
+ | 'fontSize'
82
+ >,
83
+ ) {
84
+ const {
85
+ resolvedClades,
86
+ cladeGutterWidth,
87
+ rowHeight,
88
+ effectiveTreeAreaWidth,
89
+ fontSize,
90
+ } = model
91
+ const gutterLeft = effectiveTreeAreaWidth - cladeGutterWidth
92
+ return cladeGutterWidth === 0
93
+ ? []
94
+ : resolvedClades.flatMap((clade, index) => {
95
+ const { label, markColor, rows } = clade
96
+ if (!label || !cladeInGutter(clade)) {
97
+ return []
98
+ }
99
+ const left = gutterLeft + cladeLabelOffset(clade)
100
+ return [
101
+ {
102
+ key: `${rows[0]}-${rows[1]}-${index}`,
103
+ label,
104
+ color: markColor,
105
+ left,
106
+ top: rows[0] * rowHeight,
107
+ width: effectiveTreeAreaWidth - left - bracketGap,
108
+ height: cladeHeight(clade, rowHeight),
109
+ horizontal: cladeLabelHorizontal(clade, rowHeight, fontSize),
110
+ },
111
+ ]
112
+ })
113
+ }
@@ -2,6 +2,8 @@ import Flatbush from 'flatbush'
2
2
 
3
3
  export interface ClickEntry {
4
4
  name: string
5
+ /** the text a tip label draws, which may be cut short */
6
+ label?: string
5
7
  id: string
6
8
  branch?: boolean
7
9
  minX: number
@@ -0,0 +1,24 @@
1
+ const ellipsis = '…'
2
+
3
+ /**
4
+ * the longest prefix of `text`, plus an ellipsis, that measures at most
5
+ * `room` px
6
+ */
7
+ export function ellipsize(
8
+ measure: (text: string) => number,
9
+ text: string,
10
+ room: number,
11
+ ) {
12
+ const width = measure(text)
13
+ if (width <= room) {
14
+ return { text, width }
15
+ }
16
+ for (let n = Math.floor((text.length * room) / width); n > 0; n--) {
17
+ const truncated = text.slice(0, n).trimEnd() + ellipsis
18
+ const truncatedWidth = measure(truncated)
19
+ if (truncatedWidth <= room) {
20
+ return { text: truncated, width: truncatedWidth }
21
+ }
22
+ }
23
+ return { text: '', width: 0 }
24
+ }
@@ -8,6 +8,7 @@ import {
8
8
  } from '../msa/renderHighlights.ts'
9
9
  import { referenceColor } from '../overlayColors.ts'
10
10
  import { bracketBarWidth, bracketGap } from './cladeBrackets.ts'
11
+ import { ellipsize } from './ellipsize.ts'
11
12
 
12
13
  import type { HierarchyNode } from '../../hierarchy.ts'
13
14
  import type { MsaViewModel } from '../../model.ts'
@@ -35,7 +36,7 @@ function inYBlock(y: number, offsetY: number, by: number, pad: number) {
35
36
  /**
36
37
  * Every node whose subtree reaches into the block, skipping the subtrees that
37
38
  * do not. A subtree's leaves are one contiguous run of rows, recorded by
38
- * clusterLayout as xMin/xMax, so a block of a 200k-tip tree visits a few dozen
39
+ * layoutTree as xMin/xMax, so a block of a 200k-tip tree visits a few dozen
39
40
  * nodes.
40
41
  *
41
42
  * A link out of a visited node is drawn even when its child is pruned, so a
@@ -382,9 +383,8 @@ function renderTreeLabels({
382
383
  treeAreaWidthMinusMargin,
383
384
  marginLeft,
384
385
  noTree,
385
- labelWidthMap,
386
- labelWidthScale,
387
386
  } = model
387
+ const treeDrawn = drawTree && !noTree
388
388
  const by = blockSizeYOverride ?? blockSize
389
389
  // labels only exist for leaves, which are laid out top to bottom, so take the
390
390
  // same slice the MSA renderer uses instead of walking every tip per block
@@ -412,22 +412,22 @@ function renderTreeLabels({
412
412
  // fontSize/4 below the row center, the baseline the alignment letters
413
413
  // share
414
414
  const yp = y + fontSize / 4
415
- let xp = 0
416
- if (!noTree) {
417
- xp = getNodeX(node, showBranchLen, tipX, maxDepthToLeaf) ?? 0
418
- }
419
-
420
- const measured = labelWidthMap.get(name)
421
- const width =
422
- measured === undefined
423
- ? ctx.measureText(displayName).width
424
- : measured * labelWidthScale
415
+ const xp = treeDrawn
416
+ ? (getNodeX(node, showBranchLen, tipX, maxDepthToLeaf) ?? 0)
417
+ : 0
418
+ const fit = (room: number) =>
419
+ ellipsize(text => ctx.measureText(text).width, displayName, room)
420
+ // a label cut to nothing still gets a click box the font size wide
421
+ const boxWidth = (width: number) => Math.max(width, fontSize)
425
422
 
426
423
  ctx.fillStyle = tipLabelColors?.get(name) ?? theme.palette.text.primary
427
424
  if (labelsAlignRight) {
428
425
  const smallPadding = 2
429
426
  const offset = treeAreaWidthMinusMargin - smallPadding
430
- if (drawTree && !noTree) {
427
+ const { text, width } = fit(
428
+ offset - (treeDrawn ? xp + radius + 2 + smallPadding : 0),
429
+ )
430
+ if (treeDrawn) {
431
431
  // beginPath is load-bearing: without it every leader line re-strokes
432
432
  // all the ones before it, which is quadratic in the label count
433
433
  ctx.beginPath()
@@ -435,28 +435,31 @@ function renderTreeLabels({
435
435
  ctx.lineTo(offset - smallPadding - width, y)
436
436
  ctx.stroke()
437
437
  }
438
- ctx.fillText(displayName, offset, yp)
438
+ ctx.fillText(text, offset, yp)
439
439
  // everything here is drawn under translate(marginLeft), so the click box
440
440
  // has to be the text's own right edge in that same space -- taking
441
- // treeAreaWidth instead put the target smallPadding off the label
441
+ // the tree area width instead put the target smallPadding off the label
442
442
  const labelRight = offset + marginLeft
443
443
  clickMap?.insert({
444
- minX: labelRight - width,
444
+ minX: labelRight - boxWidth(width),
445
445
  maxX: labelRight,
446
446
  minY: yp - emHeight,
447
447
  maxY: yp,
448
448
  name,
449
+ label: displayName,
449
450
  id,
450
451
  })
451
452
  } else {
452
- const labelX = noTree ? 2 : xp + d
453
- ctx.fillText(displayName, labelX, yp)
453
+ const labelX = treeDrawn ? xp + d : 2
454
+ const { text, width } = fit(treeAreaWidthMinusMargin - labelX)
455
+ ctx.fillText(text, labelX, yp)
454
456
  clickMap?.insert({
455
457
  minX: labelX + marginLeft,
456
- maxX: labelX + width + marginLeft,
458
+ maxX: labelX + boxWidth(width) + marginLeft,
457
459
  minY: yp - emHeight,
458
460
  maxY: yp,
459
461
  name,
462
+ label: displayName,
460
463
  id,
461
464
  })
462
465
  }
@@ -502,12 +505,12 @@ function renderCladeHighlights({
502
505
  hierarchy,
503
506
  showBranchLenEffective: showBranchLen,
504
507
  rowHeight,
505
- treeAreaWidth,
508
+ effectiveTreeAreaWidth,
506
509
  marginLeft,
507
510
  blockSize,
508
511
  } = model
509
512
  const by = blockSizeYOverride ?? blockSize
510
- const right = treeAreaWidth - marginLeft
513
+ const right = effectiveTreeAreaWidth - marginLeft
511
514
  for (const { rows, color, mark } of resolvedClades) {
512
515
  if (mark !== 'highlight') {
513
516
  continue
@@ -548,7 +551,7 @@ function renderCladeBrackets({
548
551
  resolvedClades,
549
552
  cladeGutterWidth,
550
553
  rowHeight,
551
- treeAreaWidth,
554
+ effectiveTreeAreaWidth,
552
555
  marginLeft,
553
556
  blockSize,
554
557
  } = model
@@ -557,7 +560,7 @@ function renderCladeBrackets({
557
560
  }
558
561
  const by = blockSizeYOverride ?? blockSize
559
562
  const x =
560
- treeAreaWidth -
563
+ effectiveTreeAreaWidth -
561
564
  marginLeft -
562
565
  cladeGutterWidth +
563
566
  bracketGap +
@@ -600,7 +603,7 @@ function renderRowHighlights({
600
603
  rowTints,
601
604
  referenceRowIndex,
602
605
  rowHeight,
603
- treeAreaWidth,
606
+ effectiveTreeAreaWidth,
604
607
  marginLeft,
605
608
  blockSize,
606
609
  } = model
@@ -608,7 +611,12 @@ function renderRowHighlights({
608
611
  const pad = blockPad(model)
609
612
  const placed: LabelBox[] = []
610
613
  const fillRow = (index: number) => {
611
- ctx.fillRect(-marginLeft, index * rowHeight, treeAreaWidth, rowHeight)
614
+ ctx.fillRect(
615
+ -marginLeft,
616
+ index * rowHeight,
617
+ effectiveTreeAreaWidth,
618
+ rowHeight,
619
+ )
612
620
  }
613
621
  if (rowTints) {
614
622
  const top = Math.max(0, Math.floor((offsetY - pad) / rowHeight))
@@ -647,8 +655,8 @@ function renderRowHighlights({
647
655
  label,
648
656
  x: -marginLeft + 2,
649
657
  y: first * rowHeight,
650
- spanWidth: treeAreaWidth,
651
- bounds: { min: -marginLeft, max: treeAreaWidth - marginLeft },
658
+ spanWidth: effectiveTreeAreaWidth,
659
+ bounds: { min: -marginLeft, max: effectiveTreeAreaWidth - marginLeft },
652
660
  placed,
653
661
  })
654
662
  }
@@ -698,14 +706,7 @@ export function renderTreeCanvas({
698
706
 
699
707
  // memoized on the model and shared across the tree/bubble/label passes (and
700
708
  // across all tree blocks) rather than re-traversing the hierarchy in each
701
- const { maxBranchLength, maxDepthToLeaf, showBranchLenEffective, treeWidth } =
702
- model
703
- // pixel x of the tips. A phylogram scales branch lengths so the longest
704
- // root-to-tip path lands at maxBranchLength; a cladogram ignores lengths and
705
- // spreads topological depth across the whole tree area instead. Reusing
706
- // maxBranchLength for the cladogram would pin every node to x=0 for a tree
707
- // with no branch lengths -- which is exactly what forces cladogram mode.
708
- const tipX = showBranchLenEffective ? maxBranchLength : treeWidth
709
+ const { maxDepthToLeaf, effectiveTreeWidth: tipX } = model
709
710
  // built once for all three passes: each of them asks about a different node on
710
711
  // every block of every redraw, and `collapsed` is a list
711
712
  const collapsedSet = new Set(model.collapsed)
@@ -11,7 +11,7 @@ export function renderTreeMouseover({
11
11
  }) {
12
12
  const {
13
13
  rowHeight,
14
- treeAreaWidth: width,
14
+ effectiveTreeAreaWidth: width,
15
15
  height,
16
16
  scrollY,
17
17
  mouseRow,
@@ -1,4 +1,5 @@
1
1
  import { descendants } from '../../hierarchy.ts'
2
+ import { makeOffscreenCanvas } from '../msa/msaRaster.ts'
2
3
  import { getNodeX } from './renderTreeCanvas.ts'
3
4
 
4
5
  import type { HierarchyNode } from '../../hierarchy.ts'
@@ -62,10 +63,10 @@ export function drawTreeOverview({
62
63
  continue
63
64
  }
64
65
  const sx = nodeX(source)
65
- const sy = rowY(source.x!)
66
+ const sy = rowY(source.x)
66
67
  for (const target of source.children) {
67
68
  const tx = nodeX(target)
68
- const ty = rowY(target.x!)
69
+ const ty = rowY(target.x)
69
70
  if (Math.abs(tx - sx) < 1 && Math.abs(ty - sy) < 1) {
70
71
  continue
71
72
  }
@@ -158,17 +159,8 @@ function overviewKeys({
158
159
  ]
159
160
  }
160
161
 
161
- function makeCanvas(width: number, height: number) {
162
- const canvas =
163
- typeof OffscreenCanvas !== 'undefined'
164
- ? new OffscreenCanvas(width, height)
165
- : typeof document !== 'undefined'
166
- ? Object.assign(document.createElement('canvas'), { width, height })
167
- : undefined
168
- const ctx = canvas?.getContext('2d')
169
- return ctx && typeof (ctx as CanvasRenderingContext2D).stroke === 'function'
170
- ? { canvas: canvas as RasterCanvas, ctx: ctx as CanvasRenderingContext2D }
171
- : undefined
162
+ function canStroke(ctx: unknown): ctx is CanvasRenderingContext2D {
163
+ return typeof (ctx as CanvasRenderingContext2D | null)?.stroke === 'function'
172
164
  }
173
165
 
174
166
  /**
@@ -198,9 +190,10 @@ export function treeOverviewImage({
198
190
  if (prev && keys.every((key, i) => key === prev.keys[i])) {
199
191
  return prev.canvas
200
192
  }
201
- const made = makeCanvas(
193
+ const made = makeOffscreenCanvas(
202
194
  Math.max(1, Math.round(width * scale)),
203
195
  Math.max(1, Math.round(height * scale)),
196
+ canStroke,
204
197
  )
205
198
  if (made) {
206
199
  made.ctx.scale(scale, scale)