react-msaview 8.4.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/bundle/index.js +503 -89
  2. package/bundle/index.js.map +4 -4
  3. package/dist/components/MSAView.js +2 -2
  4. package/dist/components/MSAView.js.map +1 -1
  5. package/dist/components/MSAViewer.d.ts +1 -3
  6. package/dist/components/MSAViewer.js +1 -2
  7. package/dist/components/MSAViewer.js.map +1 -1
  8. package/dist/components/ResizeHandles.js +1 -1
  9. package/dist/components/ResizeHandles.js.map +1 -1
  10. package/dist/components/Track.js +1 -1
  11. package/dist/components/Track.js.map +1 -1
  12. package/dist/components/dialogs/ExportSVGDialog.js +2 -2
  13. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  14. package/dist/components/header/GoToBox.d.ts +332 -0
  15. package/dist/components/header/GoToBox.js +14 -4
  16. package/dist/components/header/GoToBox.js.map +1 -1
  17. package/dist/components/header/settingsMenuItems.js +4 -1
  18. package/dist/components/header/settingsMenuItems.js.map +1 -1
  19. package/dist/components/tree/CladeLabels.js +3 -3
  20. package/dist/components/tree/CladeLabels.js.map +1 -1
  21. package/dist/components/tree/TreeCanvas.js +4 -4
  22. package/dist/components/tree/TreeCanvas.js.map +1 -1
  23. package/dist/components/tree/TreeCanvasBlock.js +4 -4
  24. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  25. package/dist/components/tree/TreeOverview.js +10 -5
  26. package/dist/components/tree/TreeOverview.js.map +1 -1
  27. package/dist/components/tree/TreePanel.js +6 -2
  28. package/dist/components/tree/TreePanel.js.map +1 -1
  29. package/dist/components/tree/TreeRuler.js +2 -2
  30. package/dist/components/tree/TreeRuler.js.map +1 -1
  31. package/dist/components/tree/cladeBrackets.d.ts +8 -10
  32. package/dist/components/tree/cladeBrackets.js +7 -6
  33. package/dist/components/tree/cladeBrackets.js.map +1 -1
  34. package/dist/components/tree/clickMap.d.ts +2 -0
  35. package/dist/components/tree/clickMap.js.map +1 -1
  36. package/dist/components/tree/ellipsize.d.ts +8 -0
  37. package/dist/components/tree/ellipsize.js +20 -0
  38. package/dist/components/tree/ellipsize.js.map +1 -0
  39. package/dist/components/tree/renderTreeCanvas.js +30 -26
  40. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  41. package/dist/components/tree/renderTreeMouseover.js +1 -1
  42. package/dist/components/tree/renderTreeMouseover.js.map +1 -1
  43. package/dist/constants.d.ts +6 -3
  44. package/dist/constants.js +16 -6
  45. package/dist/constants.js.map +1 -1
  46. package/dist/measureTextCanvas.js +2 -2
  47. package/dist/measureTextCanvas.js.map +1 -1
  48. package/dist/model/treeModel.d.ts +14 -20
  49. package/dist/model/treeModel.js +16 -23
  50. package/dist/model/treeModel.js.map +1 -1
  51. package/dist/model.d.ts +192 -213
  52. package/dist/model.js +112 -104
  53. package/dist/model.js.map +1 -1
  54. package/dist/renderToSvg.js +24 -22
  55. package/dist/renderToSvg.js.map +1 -1
  56. package/dist/svgTestUtil.d.ts +252 -356
  57. package/dist/tipLabels.d.ts +8 -0
  58. package/dist/tipLabels.js +46 -0
  59. package/dist/tipLabels.js.map +1 -0
  60. package/dist/version.d.ts +1 -1
  61. package/dist/version.js +1 -1
  62. package/package.json +6 -6
  63. package/src/components/MSAView.tsx +2 -2
  64. package/src/components/MSAViewer.tsx +1 -5
  65. package/src/components/ResizeHandles.tsx +1 -1
  66. package/src/components/Track.tsx +1 -1
  67. package/src/components/dialogs/ExportSVGDialog.tsx +2 -2
  68. package/src/components/header/GoToBox.tsx +18 -4
  69. package/src/components/header/settingsMenuItems.ts +8 -0
  70. package/src/components/tree/CladeLabels.tsx +3 -3
  71. package/src/components/tree/TreeCanvas.tsx +10 -4
  72. package/src/components/tree/TreeCanvasBlock.tsx +4 -4
  73. package/src/components/tree/TreeOverview.tsx +10 -5
  74. package/src/components/tree/TreePanel.tsx +8 -2
  75. package/src/components/tree/TreeRuler.tsx +7 -3
  76. package/src/components/tree/cladeBrackets.ts +10 -17
  77. package/src/components/tree/clickMap.ts +2 -0
  78. package/src/components/tree/ellipsize.ts +24 -0
  79. package/src/components/tree/renderTreeCanvas.ts +42 -30
  80. package/src/components/tree/renderTreeMouseover.ts +1 -1
  81. package/src/constants.ts +17 -6
  82. package/src/measureTextCanvas.ts +2 -2
  83. package/src/model/treeModel.ts +17 -27
  84. package/src/model.ts +133 -128
  85. package/src/renderToSvg.tsx +24 -22
  86. package/src/tipLabels.ts +51 -0
  87. package/src/version.ts +1 -1
@@ -0,0 +1,8 @@
1
+ import type { NodeWithIds } from './types.ts';
2
+ /** the leaf names of a tree, and its depth in levels from the root */
3
+ export declare function treeTips(tree: NodeWithIds): {
4
+ names: string[];
5
+ depth: number;
6
+ };
7
+ /** the width of the widest label at `fontSize`, measuring the longest few */
8
+ export declare function widestLabel(labels: string[], fontSize: number): number;
@@ -0,0 +1,46 @@
1
+ import { measureTextCanvas } from './measureTextCanvas.js';
2
+ // A label's width follows its length closely enough that the widest one is
3
+ // among the longest few, so a 230k-tip tree measures this many strings.
4
+ const measuredLabels = 100;
5
+ /** the leaf names of a tree, and its depth in levels from the root */
6
+ export function treeTips(tree) {
7
+ const names = [];
8
+ let depth = 0;
9
+ const stack = [[tree, 0]];
10
+ for (let entry = stack.pop(); entry; entry = stack.pop()) {
11
+ const [node, level] = entry;
12
+ const { children } = node;
13
+ if (children.length > 0) {
14
+ for (const child of children) {
15
+ stack.push([child, level + 1]);
16
+ }
17
+ }
18
+ else {
19
+ names.push(node.name);
20
+ depth = Math.max(depth, level);
21
+ }
22
+ }
23
+ return { names, depth };
24
+ }
25
+ /** the width of the widest label at `fontSize`, measuring the longest few */
26
+ export function widestLabel(labels, fontSize) {
27
+ const counts = [];
28
+ for (const label of labels) {
29
+ counts[label.length] = (counts[label.length] ?? 0) + 1;
30
+ }
31
+ let shortest = counts.length;
32
+ for (let seen = 0; shortest > 0 && seen < measuredLabels;) {
33
+ shortest--;
34
+ seen += counts[shortest] ?? 0;
35
+ }
36
+ let widest = 0;
37
+ let ties = 0;
38
+ for (const label of labels) {
39
+ if (label.length > shortest ||
40
+ (label.length === shortest && ties++ < measuredLabels)) {
41
+ widest = Math.max(widest, measureTextCanvas(label, fontSize));
42
+ }
43
+ }
44
+ return widest;
45
+ }
46
+ //# sourceMappingURL=tipLabels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tipLabels.js","sourceRoot":"","sources":["../src/tipLabels.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAI1D,2EAA2E;AAC3E,wEAAwE;AACxE,MAAM,cAAc,GAAG,GAAG,CAAA;AAE1B,sEAAsE;AACtE,MAAM,UAAU,QAAQ,CAAC,IAAiB;IACxC,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,MAAM,KAAK,GAA4B,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;IAClD,KAAK,IAAI,KAAK,GAAG,KAAK,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,KAAK,GAAG,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC;QACzD,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,KAAK,CAAA;QAC3B,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAA;QACzB,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;gBAC7B,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAA;YAChC,CAAC;QACH,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACrB,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;QAChC,CAAC;IACH,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAA;AACzB,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,WAAW,CAAC,MAAgB,EAAE,QAAgB;IAC5D,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAA;IACxD,CAAC;IACD,IAAI,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAA;IAC5B,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,QAAQ,GAAG,CAAC,IAAI,IAAI,GAAG,cAAc,GAAG,CAAC;QAC1D,QAAQ,EAAE,CAAA;QACV,IAAI,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;IAC/B,CAAC;IACD,IAAI,MAAM,GAAG,CAAC,CAAA;IACd,IAAI,IAAI,GAAG,CAAC,CAAA;IACZ,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IACE,KAAK,CAAC,MAAM,GAAG,QAAQ;YACvB,CAAC,KAAK,CAAC,MAAM,KAAK,QAAQ,IAAI,IAAI,EAAE,GAAG,cAAc,CAAC,EACtD,CAAC;YACD,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAA;QAC/D,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC"}
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "8.4.0";
1
+ export declare const version = "9.0.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
- export const version = '8.4.0';
1
+ export const version = '9.0.0';
2
2
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-msaview",
3
3
  "author": "Colin",
4
- "version": "8.4.0",
4
+ "version": "9.0.0",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "repository": {
@@ -37,11 +37,11 @@
37
37
  "react-dom": ">=16.8.0"
38
38
  },
39
39
  "devDependencies": {
40
- "@jbrowse/core": "file:vendor-jbrowse/jbrowse-core.tgz",
41
- "@jbrowse/mobx-state-tree": "^6.5.1",
40
+ "@jbrowse/core": "^5.0.0-beta.9",
41
+ "@jbrowse/mobx-state-tree": "^6.6.1",
42
42
  "@mui/icons-material": "^9.4.0",
43
43
  "@mui/material": "^9.4.0",
44
- "@types/node": "^26.5.1",
44
+ "@types/node": "^26.6.2",
45
45
  "esbuild": "^0.28.2",
46
46
  "mobx": "^7.0.3",
47
47
  "mobx-react": "^10.0.2",
@@ -50,10 +50,10 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@gmod/newick": "^1.0.3",
53
- "@jbrowse/svgcanvas": "8.4.0",
53
+ "@jbrowse/svgcanvas": "9.0.0",
54
54
  "colord": "^2.10.0",
55
55
  "flatbush": "^4.6.2",
56
- "msa-parsers": "8.4.0"
56
+ "msa-parsers": "9.0.0"
57
57
  },
58
58
  "scripts": {
59
59
  "clean": "rimraf dist bundle",
@@ -48,7 +48,7 @@ const TrackColumnIndicator = observer(function ({
48
48
  mouseClickCol,
49
49
  colWidth,
50
50
  scrollX,
51
- treeAreaWidth,
51
+ effectiveTreeAreaWidth,
52
52
  rowPanelsWidth,
53
53
  resizeHandleWidth,
54
54
  totalTrackAreaHeight,
@@ -72,7 +72,7 @@ const TrackColumnIndicator = observer(function ({
72
72
  <div
73
73
  style={{
74
74
  position: 'absolute',
75
- left: treeAreaWidth + rowPanelsWidth + resizeHandleWidth,
75
+ left: effectiveTreeAreaWidth + rowPanelsWidth + resizeHandleWidth,
76
76
  top: 0,
77
77
  width: msaCanvasWidth,
78
78
  height: totalTrackAreaHeight,
@@ -105,10 +105,8 @@ export interface MSAViewerProps {
105
105
  rowPanels?: RowPanelSpec[]
106
106
  /** draw the phylogenetic tree (default true); false leaves a labels-only gutter */
107
107
  drawTree?: boolean
108
- /** fixed width (px) of the tree/label area */
108
+ /** fixed width (px) of the tree/label area; unset fits it to the labels and the tree */
109
109
  treeAreaWidth?: number
110
- /** auto-size the tree/label area to the labels (used when drawTree is false) */
111
- autoTreeAreaWidth?: boolean
112
110
  /** draw branch lengths (default true); false draws a cladogram */
113
111
  showBranchLen?: boolean
114
112
  /**
@@ -254,7 +252,6 @@ function Viewer({
254
252
  drawTree,
255
253
  residueEncoding,
256
254
  treeAreaWidth,
257
- autoTreeAreaWidth,
258
255
  showBranchLen,
259
256
  treeOrder,
260
257
  treeRoot,
@@ -303,7 +300,6 @@ function Viewer({
303
300
  ...(rowPanels ? { rowPanels } : {}),
304
301
  ...(drawTree !== undefined ? { drawTree } : {}),
305
302
  ...(treeAreaWidth ? { treeAreaWidth } : {}),
306
- ...(autoTreeAreaWidth ? { autoTreeAreaWidth } : {}),
307
303
  ...(showBranchLen !== undefined ? { showBranchLen } : {}),
308
304
  ...(treeOrder ? { treeOrder } : {}),
309
305
  ...(treeRoot ? { treeRoot } : {}),
@@ -22,7 +22,7 @@ export const VerticalResizeHandle = observer(function ({
22
22
  <DragHandle
23
23
  axis="x"
24
24
  variant="resizer"
25
- getStart={() => model.treeAreaWidth}
25
+ getStart={() => model.effectiveTreeAreaWidth}
26
26
  onDrag={onDrag}
27
27
  style={{ width: model.resizeHandleWidth }}
28
28
  />
@@ -44,7 +44,7 @@ const TrackLabel = observer(function TrackLabel({
44
44
  const closeMenu = () => {
45
45
  setAnchorEl(undefined)
46
46
  }
47
- const { drawLabels, fontSize, treeAreaWidth: width } = model
47
+ const { drawLabels, fontSize, effectiveTreeAreaWidth: width } = model
48
48
  const {
49
49
  model: { name, height },
50
50
  } = track
@@ -49,7 +49,7 @@ const ExportSVGDialog = observer(function ({
49
49
  const {
50
50
  totalWidth,
51
51
  totalHeight,
52
- treeAreaWidth,
52
+ effectiveTreeAreaWidth,
53
53
  rowPanelsWidth,
54
54
  turnedOnTracks,
55
55
  totalTrackAreaHeight,
@@ -59,7 +59,7 @@ const ExportSVGDialog = observer(function ({
59
59
  treeOverviewHeight,
60
60
  } = model
61
61
  const hasTracks = turnedOnTracks.length > 0
62
- const entireWidth = totalWidth + treeAreaWidth + rowPanelsWidth
62
+ const entireWidth = totalWidth + effectiveTreeAreaWidth + rowPanelsWidth
63
63
  const entireHeight =
64
64
  totalHeight +
65
65
  (includeTracks ? totalTrackAreaHeight : 0) +
@@ -6,9 +6,26 @@ import { observer } from 'mobx-react'
6
6
  import { findableRowNames, goTo, rowNameSuggestions } from './goTo.ts'
7
7
 
8
8
  import type { MsaViewModel } from '../../model.ts'
9
+ import type { AutocompleteRenderInputParams } from '@mui/material'
9
10
 
10
11
  const label = 'Go to row or column'
11
12
 
13
+ // MUI 9 hands renderInput `slotProps`; MUI 7, which every released JBrowse host
14
+ // serves, hands it `inputProps` and no `slotProps` at all
15
+ export function labelledInput(params: AutocompleteRenderInputParams) {
16
+ const { slotProps, inputProps } = params as Partial<
17
+ AutocompleteRenderInputParams & { inputProps: object }
18
+ >
19
+ return slotProps
20
+ ? {
21
+ slotProps: {
22
+ ...slotProps,
23
+ htmlInput: { ...slotProps.htmlInput, 'aria-label': label },
24
+ },
25
+ }
26
+ : { inputProps: { ...inputProps, 'aria-label': label } }
27
+ }
28
+
12
29
  const GoToBox = observer(function ({ model }: { model: MsaViewModel }) {
13
30
  const [missed, setMissed] = useState(false)
14
31
  const names = findableRowNames(model)
@@ -37,10 +54,7 @@ const GoToBox = observer(function ({ model }: { model: MsaViewModel }) {
37
54
  error={missed}
38
55
  placeholder={label}
39
56
  title="A row name, a column number, or name:residue"
40
- slotProps={{
41
- ...params.slotProps,
42
- htmlInput: { ...params.slotProps.htmlInput, 'aria-label': label },
43
- }}
57
+ {...labelledInput(params)}
44
58
  />
45
59
  )}
46
60
  />
@@ -76,6 +76,7 @@ export function treeSettingsMenuItems(model: MsaViewModel): MenuItem[] {
76
76
  drawNodeBubbles,
77
77
  drawNodeLabels,
78
78
  drawLabels,
79
+ treeAreaWidth,
79
80
  showTreeOverview,
80
81
  treeOrder,
81
82
  treeRoot,
@@ -145,5 +146,12 @@ export function treeSettingsMenuItems(model: MsaViewModel): MenuItem[] {
145
146
  toggle('Draw labels', drawLabels, arg => {
146
147
  model.setDrawLabels(arg)
147
148
  }),
149
+ toggle(
150
+ 'Fit tree area to labels and tree',
151
+ treeAreaWidth === undefined,
152
+ arg => {
153
+ model.setTreeAreaWidth(arg ? undefined : model.effectiveTreeAreaWidth)
154
+ },
155
+ ),
148
156
  ]
149
157
  }
@@ -24,14 +24,14 @@ const CladeLabels = observer(function ({ model }: { model: MsaViewModel }) {
24
24
  resolvedClades,
25
25
  cladeGutterWidth,
26
26
  rowHeight,
27
- treeAreaWidth,
27
+ effectiveTreeAreaWidth,
28
28
  fontSize,
29
29
  } = model
30
30
  const theme = useTheme()
31
31
  if (cladeGutterWidth === 0) {
32
32
  return null
33
33
  }
34
- const gutterLeft = treeAreaWidth - cladeGutterWidth
34
+ const gutterLeft = effectiveTreeAreaWidth - cladeGutterWidth
35
35
  return (
36
36
  <>
37
37
  {resolvedClades.map((clade, index) => {
@@ -49,7 +49,7 @@ const CladeLabels = observer(function ({ model }: { model: MsaViewModel }) {
49
49
  position: 'absolute',
50
50
  left,
51
51
  top: rows[0] * rowHeight,
52
- width: treeAreaWidth - left - bracketGap,
52
+ width: effectiveTreeAreaWidth - left - bracketGap,
53
53
  height: cladeHeight(clade, rowHeight),
54
54
  display: 'flex',
55
55
  alignItems: 'center',
@@ -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,7 @@
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 { Clade, ResolvedClade } from '../../types.ts'
4
5
 
5
6
  // The bar of the bracket mark and the space around it, in px.
6
7
  export const bracketBarWidth = 3
@@ -11,7 +12,7 @@ export const bracketGap = 4
11
12
  export const maxCladeGutterWidth = 140
12
13
 
13
14
  /** whether a clade draws in the gutter: a bracket, or a highlight with a label */
14
- export function cladeInGutter(clade: ResolvedClade) {
15
+ export function cladeInGutter(clade: Pick<Clade, 'mark' | 'label'>) {
15
16
  return (
16
17
  clade.mark === 'bracket' || (clade.mark === 'highlight' && !!clade.label)
17
18
  )
@@ -35,7 +36,7 @@ export function cladeLabelHorizontal(
35
36
  }
36
37
 
37
38
  /** the px between the left edge of the gutter and the label's own column */
38
- export function cladeLabelOffset(clade: ResolvedClade) {
39
+ export function cladeLabelOffset(clade: Pick<Clade, 'mark'>) {
39
40
  return clade.mark === 'bracket'
40
41
  ? bracketGap + bracketBarWidth + bracketGap
41
42
  : bracketGap
@@ -43,27 +44,19 @@ export function cladeLabelOffset(clade: ResolvedClade) {
43
44
 
44
45
  /**
45
46
  * 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.
47
+ * the widest label read across at labelFitFontSize, zero where no clade draws
48
+ * there, and at most maxCladeGutterWidth. The records size it, so a zoom, a
49
+ * collapse or a focus leaves it as it is, and a renderer clips a label that
50
+ * outgrows it.
48
51
  */
49
- export function cladeGutterWidth({
50
- clades,
51
- rowHeight,
52
- fontSize,
53
- }: {
54
- clades: ResolvedClade[]
55
- rowHeight: number
56
- fontSize: number
57
- }) {
52
+ export function cladeGutterWidth(clades: Pick<Clade, 'mark' | 'label'>[]) {
58
53
  let widest = 0
59
54
  for (const clade of clades) {
60
55
  if (!cladeInGutter(clade)) {
61
56
  continue
62
57
  }
63
58
  const text = clade.label
64
- ? cladeLabelHorizontal(clade, rowHeight, fontSize)
65
- ? measureTextCanvas(clade.label, fontSize)
66
- : fontSize
59
+ ? measureTextCanvas(clade.label, labelFitFontSize)
67
60
  : 0
68
61
  widest = Math.max(widest, cladeLabelOffset(clade) + text + bracketGap)
69
62
  }
@@ -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
+ }