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
package/src/model.ts CHANGED
@@ -56,15 +56,19 @@ import {
56
56
  defaultShowDomainLegend,
57
57
  defaultShowDomains,
58
58
  defaultSubFeatureRows,
59
- defaultTreeWidth,
60
- labelReferenceFontSize,
59
+ fontSizeFor,
60
+ labelFitFontSize,
61
61
  maxCellSize,
62
+ maxFitTreeWidth,
63
+ maxTreeAreaShare,
62
64
  maxInlineSnapshotBytes as defaultMaxInlineSnapshotBytes,
63
65
  maxNeighborJoiningRows,
64
66
  minColWidth,
65
67
  minLetterColWidth,
66
68
  minLetterRowHeight,
67
69
  minRowHeight,
70
+ minTreeWidth,
71
+ pxPerTreeDepth,
68
72
  rowPanelHeaderHeight,
69
73
  rowTintAlpha,
70
74
  scrollZoomAxes,
@@ -95,7 +99,6 @@ import {
95
99
  sort,
96
100
  sum as hierarchySum,
97
101
  } from './hierarchy.ts'
98
- import { measureTextCanvas } from './measureTextCanvas.ts'
99
102
  import { DataModelF } from './model/DataModel.ts'
100
103
  import { DialogQueueSessionMixin } from './model/DialogQueue.ts'
101
104
  import { MSAModelF } from './model/msaModel.ts'
@@ -114,6 +117,7 @@ import { resolveScale } from './scales.ts'
114
117
  import { buildSeqPosIndex } from './seqPosToGlobalCol.ts'
115
118
  import { maxBitsFor } from './sequenceLogo.ts'
116
119
  import { stripDefault } from './stripDefault.ts'
120
+ import { treeTips, widestLabel } from './tipLabels.ts'
117
121
  import {
118
122
  computeRowInsertions,
119
123
  dropBlanks,
@@ -528,7 +532,6 @@ export const preservedOnReset = new Set([
528
532
  'drawNodeLabels',
529
533
  'showTreeOverview',
530
534
  'overviewHeight',
531
- 'autoTreeAreaWidth',
532
535
  'turnedOffTracks',
533
536
  'trackHeights',
534
537
  'hideGaps',
@@ -1588,28 +1591,28 @@ function stateModelFactory({
1588
1591
  * #action
1589
1592
  */
1590
1593
  setMSAFilehandle(msaFilehandle?: FileLocationType) {
1591
- self.msaFilehandle = msaFilehandle
1594
+ self.msaFilehandle = cast(msaFilehandle)
1592
1595
  },
1593
1596
 
1594
1597
  /**
1595
1598
  * #action
1596
1599
  */
1597
1600
  setTreeFilehandle(treeFilehandle?: FileLocationType) {
1598
- self.treeFilehandle = treeFilehandle
1601
+ self.treeFilehandle = cast(treeFilehandle)
1599
1602
  },
1600
1603
 
1601
1604
  /**
1602
1605
  * #action
1603
1606
  */
1604
1607
  setTreeMetadataFilehandle(treeMetadataFilehandle?: FileLocationType) {
1605
- self.treeMetadataFilehandle = treeMetadataFilehandle
1608
+ self.treeMetadataFilehandle = cast(treeMetadataFilehandle)
1606
1609
  },
1607
1610
 
1608
1611
  /**
1609
1612
  * #action
1610
1613
  */
1611
1614
  setGFFFilehandle(gffFilehandle?: FileLocationType) {
1612
- self.gffFilehandle = gffFilehandle
1615
+ self.gffFilehandle = cast(gffFilehandle)
1613
1616
  },
1614
1617
 
1615
1618
  /**
@@ -1804,10 +1807,10 @@ function stateModelFactory({
1804
1807
  * the row table: extra fields per row, keyed by row name, which the
1805
1808
  * `encodings` channels read. It is stored as the JSON string
1806
1809
  * `data.treeMetadata`, the name that travels in existing links, so the
1807
- * inline size limit and `treeMetadataFilehandle` cover it. labelWidthMap
1808
- * reads it on every layout, so a malformed user-supplied file returns {}
1809
- * instead of throwing out of rendering, and every field is a string by
1810
- * the time a scale sorts it.
1810
+ * inline size limit and `treeMetadataFilehandle` cover it. The tree area
1811
+ * fit reads it on every layout, so a malformed user-supplied file
1812
+ * returns {} instead of throwing out of rendering, and every field is a
1813
+ * string by the time a scale sorts it.
1811
1814
  */
1812
1815
  get rowData(): Record<string, Record<string, string> | undefined> {
1813
1816
  const text = self.data.treeMetadata
@@ -1938,11 +1941,107 @@ function stateModelFactory({
1938
1941
  * where no clade draws a bar or a label.
1939
1942
  */
1940
1943
  get cladeGutterWidth() {
1941
- return cladeGutterWidth({
1942
- clades: this.resolvedClades,
1943
- rowHeight: self.rowHeight,
1944
- fontSize: this.fontSize,
1945
- })
1944
+ return cladeGutterWidth(self.clades)
1945
+ },
1946
+ /**
1947
+ * #getter
1948
+ * whether a tree draws beside the labels: false for a file with no
1949
+ * tree, and with drawTree off
1950
+ */
1951
+ get treeDrawn() {
1952
+ return self.drawTree && !this.noTree
1953
+ },
1954
+ /**
1955
+ * #getter
1956
+ * the leaf names of the whole tree and its depth in levels. The fit
1957
+ * reads these, so a collapse or a focus leaves the tree area as it is
1958
+ */
1959
+ get treeTips() {
1960
+ return treeTips(this.tree)
1961
+ },
1962
+ /**
1963
+ * #getter
1964
+ * the widest tip label at labelFitFontSize, measured whether or not a
1965
+ * zoom hides the labels
1966
+ */
1967
+ get maxTipLabelWidth() {
1968
+ return widestLabel(
1969
+ this.treeTips.names.map(name => this.rowDataOf(name)?.genome || name),
1970
+ labelFitFontSize,
1971
+ )
1972
+ },
1973
+ /**
1974
+ * #getter
1975
+ * the px the tip labels take across the tree area: the widest label at
1976
+ * labelFitFontSize and the gaps either side of it, or 0 with drawLabels
1977
+ * off
1978
+ */
1979
+ get labelColumnWidth() {
1980
+ return self.drawLabels
1981
+ ? this.maxTipLabelWidth + (this.treeDrawn ? 10 : 6)
1982
+ : 0
1983
+ },
1984
+ /**
1985
+ * #getter
1986
+ * the tree area width while treeAreaWidth is undefined: the label column
1987
+ * beside a tree that widens with its depth, up to maxTreeAreaShare of the
1988
+ * view, or the room the row panels leave where there is no alignment.
1989
+ * Where that binds, the tree keeps minTreeWidth and the renderer
1990
+ * ellipsizes the labels
1991
+ */
1992
+ get fittedTreeAreaWidth() {
1993
+ const chrome = self.marginLeft + this.cladeGutterWidth
1994
+ const tree = this.treeDrawn
1995
+ ? (self.treeWidth ??
1996
+ clamp(
1997
+ this.treeTips.depth * pxPerTreeDepth,
1998
+ minTreeWidth,
1999
+ maxFitTreeWidth,
2000
+ ))
2001
+ : 0
2002
+ const fit = chrome + tree + this.labelColumnWidth
2003
+ const room = self.viewInitialized
2004
+ ? this.numColumns > 0
2005
+ ? self.width * maxTreeAreaShare
2006
+ : self.width - this.rowPanelsWidth - self.resizeHandleWidth
2007
+ : Infinity
2008
+ const cap = Math.max(room, chrome + Math.min(tree, minTreeWidth))
2009
+ return Math.ceil(Math.min(fit, cap))
2010
+ },
2011
+ /**
2012
+ * #getter
2013
+ * the width of the area the tree and its labels draw in, px:
2014
+ * treeAreaWidth where set, the fit otherwise
2015
+ */
2016
+ get effectiveTreeAreaWidth() {
2017
+ return self.treeAreaWidth ?? this.fittedTreeAreaWidth
2018
+ },
2019
+ /**
2020
+ * #getter
2021
+ * the width of the tree, px: treeWidth where set, otherwise the tree
2022
+ * area less the label column, which a zoom hiding the labels gives to
2023
+ * the tree
2024
+ */
2025
+ get effectiveTreeWidth() {
2026
+ const labels = this.showTreeText ? this.labelColumnWidth : 0
2027
+ return (
2028
+ self.treeWidth ??
2029
+ Math.max(
2030
+ minTreeWidth,
2031
+ Math.floor(
2032
+ this.effectiveTreeAreaWidth -
2033
+ self.marginLeft -
2034
+ this.cladeGutterWidth -
2035
+ labels,
2036
+ ),
2037
+ )
2038
+ )
2039
+ },
2040
+ /**
2041
+ * #getter
2042
+ */
2043
+ get showTreeText() {
2044
+ return self.drawLabels && self.rowHeight >= minLetterRowHeight
1946
2045
  },
1947
2046
  /**
1948
2047
  * #getter
@@ -2125,7 +2224,7 @@ function stateModelFactory({
2125
2224
  get msaAreaWidth() {
2126
2225
  return (
2127
2226
  self.width -
2128
- self.treeAreaWidth -
2227
+ this.effectiveTreeAreaWidth -
2129
2228
  this.rowPanelsWidth -
2130
2229
  self.resizeHandleWidth
2131
2230
  )
@@ -2137,7 +2236,9 @@ function stateModelFactory({
2137
2236
  * margin and the bracket gutter
2138
2237
  */
2139
2238
  get treeAreaWidthMinusMargin() {
2140
- return self.treeAreaWidth - self.marginLeft - this.cladeGutterWidth
2239
+ return (
2240
+ this.effectiveTreeAreaWidth - self.marginLeft - this.cladeGutterWidth
2241
+ )
2141
2242
  },
2142
2243
  /**
2143
2244
  * #getter
@@ -2282,7 +2383,7 @@ function stateModelFactory({
2282
2383
  * #getter
2283
2384
  */
2284
2385
  get fontSize() {
2285
- return Math.min(Math.max(6, self.rowHeight - 3), 18)
2386
+ return fontSizeFor(self.rowHeight)
2286
2387
  },
2287
2388
  /**
2288
2389
  * #getter
@@ -2388,17 +2489,18 @@ function stateModelFactory({
2388
2489
  /**
2389
2490
  * #getter
2390
2491
  * `root` laid out on new nodes in row and branch-length units, read in
2391
- * pixels through `rowHeight` and `treeWidth`. A zoom leaves it and every
2392
- * node in it as they are, and an observer reading a node's `x` or `len`
2393
- * observes the size it scales by.
2492
+ * pixels through `rowHeight` and `effectiveTreeWidth`. A zoom leaves it
2493
+ * and every node in it as they are, and an observer reading a node's `x`
2494
+ * or `len` observes the size it scales by.
2394
2495
  */
2395
2496
  get treeLayout() {
2497
+ const treeWidth = () => this.effectiveTreeWidth
2396
2498
  return layoutTree(this.root, {
2397
2499
  get rowHeight() {
2398
2500
  return self.rowHeight
2399
2501
  },
2400
2502
  get treeWidth() {
2401
- return self.treeWidth
2503
+ return treeWidth()
2402
2504
  },
2403
2505
  })
2404
2506
  },
@@ -2436,11 +2538,11 @@ function stateModelFactory({
2436
2538
 
2437
2539
  /**
2438
2540
  * #getter
2439
- * x-position of the farthest tip in a phylogram, px: treeWidth, or 0 for
2440
- * a tree with no branch lengths (drawn as a cladogram)
2541
+ * x-position of the farthest tip in a phylogram, px: effectiveTreeWidth,
2542
+ * or 0 for a tree with no branch lengths (drawn as a cladogram)
2441
2543
  */
2442
2544
  get maxBranchLength() {
2443
- return this.rootToTipLength ? self.treeWidth : 0
2545
+ return this.rootToTipLength ? this.effectiveTreeWidth : 0
2444
2546
  },
2445
2547
 
2446
2548
  /**
@@ -2450,7 +2552,9 @@ function stateModelFactory({
2450
2552
  */
2451
2553
  get pxPerBranchLength() {
2452
2554
  const max = this.rootToTipLength
2453
- return this.showBranchLenEffective && max ? self.treeWidth / max : 0
2555
+ return this.showBranchLenEffective && max
2556
+ ? this.effectiveTreeWidth / max
2557
+ : 0
2454
2558
  },
2455
2559
 
2456
2560
  /**
@@ -2497,12 +2601,6 @@ function stateModelFactory({
2497
2601
  self.colWidth > self.rowHeight / 2
2498
2602
  )
2499
2603
  },
2500
- /**
2501
- * #getter
2502
- */
2503
- get showTreeText() {
2504
- return self.drawLabels && self.rowHeight >= minLetterRowHeight
2505
- },
2506
2604
  }))
2507
2605
  .views(self => {
2508
2606
  // a spec is a frozen value, so it keys its computed until setColumnTracks
@@ -2639,53 +2737,6 @@ function stateModelFactory({
2639
2737
  }
2640
2738
  })
2641
2739
  .views(self => ({
2642
- /**
2643
- * #getter
2644
- */
2645
- get labelWidthMap() {
2646
- const { showTreeText, leaves } = self
2647
- // gated on the renderer's label condition, so hidden labels reserve no
2648
- // gutter. Measured once at a reference size and scaled by
2649
- // labelWidthScale: re-measuring per vertical-zoom frame cost ~200ms on a
2650
- // 50k-leaf tree
2651
- return showTreeText
2652
- ? new Map(
2653
- leaves.map(node => {
2654
- const { name } = node.data
2655
- // `||`, matching renderTreeLabels: an empty genome falls back
2656
- // to the row name
2657
- const displayName = self.rowDataOf(name)?.genome || name
2658
- return [
2659
- name,
2660
- measureTextCanvas(displayName, labelReferenceFontSize),
2661
- ] as const
2662
- }),
2663
- )
2664
- : new Map<string, number>()
2665
- },
2666
-
2667
- /**
2668
- * #getter
2669
- * factor turning a labelWidthMap entry into its width at the current
2670
- * font size
2671
- */
2672
- get labelWidthScale() {
2673
- return self.fontSize / labelReferenceFontSize
2674
- },
2675
-
2676
- get labelsWidth() {
2677
- // a loop, not Math.max(...): spreading passes one argument per leaf,
2678
- // and the ~125k argument limit throws RangeError on the 230k-tip COVID
2679
- // tree
2680
- let max = 0
2681
- for (const width of this.labelWidthMap.values()) {
2682
- if (width > max) {
2683
- max = width
2684
- }
2685
- }
2686
- return max * this.labelWidthScale
2687
- },
2688
-
2689
2740
  /**
2690
2741
  * #getter
2691
2742
  */
@@ -3180,7 +3231,7 @@ function stateModelFactory({
3180
3231
  get treeScaleBar() {
3181
3232
  return scaleBarLength(
3182
3233
  self.pxPerBranchLength,
3183
- self.treeAreaWidth - self.marginLeft * 2,
3234
+ self.effectiveTreeAreaWidth - self.marginLeft * 2,
3184
3235
  )
3185
3236
  },
3186
3237
  /**
@@ -4942,52 +4993,6 @@ function stateModelFactory({
4942
4993
  self.columns
4943
4994
  }),
4944
4995
  )
4945
-
4946
- // with autoTreeAreaWidth and no tree drawn, size the tree area to the
4947
- // labels. Gated on noTree/!drawTree so it does not loop with the
4948
- // treeWidth autorun below
4949
- addDisposer(
4950
- self,
4951
- autorun(() => {
4952
- if (
4953
- self.autoTreeAreaWidth &&
4954
- (self.noTree || !self.drawTree) &&
4955
- self.labelsWidth
4956
- ) {
4957
- self.setTreeAreaWidth(
4958
- self.labelsWidth + self.marginLeft + 12 + self.cladeGutterWidth,
4959
- )
4960
- }
4961
- }),
4962
- )
4963
-
4964
- // treeWidth follows the tree area less the labels. A non-default
4965
- // snapshot treeWidth (jbrowse-plugin-msaview opens at 100 in a 200px
4966
- // area) is kept until the tree area changes. Not a getter, because
4967
- // labelsWidth is measured off leaves laid out against treeWidth.
4968
- let pinnedAreaWidth =
4969
- self.treeWidth === defaultTreeWidth ? undefined : self.treeAreaWidth
4970
- addDisposer(
4971
- self,
4972
- autorun(() => {
4973
- const areaWidth = self.treeAreaWidth
4974
- const labelsWidth = self.labelsWidth
4975
- if (pinnedAreaWidth === areaWidth) {
4976
- return
4977
- }
4978
- pinnedAreaWidth = undefined
4979
- self.setTreeWidth(
4980
- Math.max(
4981
- 50,
4982
- areaWidth -
4983
- labelsWidth -
4984
- 10 -
4985
- self.marginLeft -
4986
- self.cladeGutterWidth,
4987
- ),
4988
- )
4989
- }),
4990
- )
4991
4996
  },
4992
4997
  }))
4993
4998
  .postProcessSnapshot(({ data, columnTracks, ...rest }) => ({
@@ -90,7 +90,7 @@ function getLayout(model: MsaViewModel, opts: ExportSvgOptions): Layout {
90
90
  scrollY,
91
91
  totalWidth,
92
92
  totalHeight,
93
- treeAreaWidth,
93
+ effectiveTreeAreaWidth,
94
94
  rowPanelsWidth,
95
95
  rowPanelsHeaderHeight,
96
96
  totalTrackAreaHeight,
@@ -117,7 +117,7 @@ function getLayout(model: MsaViewModel, opts: ExportSvgOptions): Layout {
117
117
  rowPanelsHeaderHeight,
118
118
  treeOverviewHeight + (treeScaleBar ? treeScaleBarHeight : 0),
119
119
  )
120
- const gutterWidth = treeAreaWidth + rowPanelsWidth
120
+ const gutterWidth = effectiveTreeAreaWidth + rowPanelsWidth
121
121
 
122
122
  // width stays content-only; the legend occupies an extra column added at the
123
123
  // svg root in MsaSvg. The viewport export takes the alignment canvas's own
@@ -195,7 +195,7 @@ function MsaSvg({
195
195
  layers: LayerMap
196
196
  }) {
197
197
  const { width, trackHeight, includeMinimap, legendWidth, topHeight } = layout
198
- const { treeAreaWidth, rowPanelsWidth } = model
198
+ const { effectiveTreeAreaWidth, rowPanelsWidth } = model
199
199
  const totalWidth = width + legendWidth
200
200
  const legendTop = topHeight
201
201
  // a short alignment with many legend entries has a key taller than its rows,
@@ -241,7 +241,7 @@ function MsaSvg({
241
241
  {includeMinimap ? (
242
242
  <g
243
243
  id="minimap-panel"
244
- transform={`translate(${treeAreaWidth + rowPanelsWidth} 0)`}
244
+ transform={`translate(${effectiveTreeAreaWidth + rowPanelsWidth} 0)`}
245
245
  >
246
246
  <MinimapSVG model={model} theme={theme} />
247
247
  </g>
@@ -288,20 +288,20 @@ function TreeOverviewSVG({
288
288
  Context: typeof ContextType
289
289
  layers: LayerMap
290
290
  }) {
291
- const { treeAreaWidth, overviewHeight, id } = model
292
- const ctx = new Context(treeAreaWidth, overviewHeight)
291
+ const { effectiveTreeAreaWidth, overviewHeight, id } = model
292
+ const ctx = new Context(effectiveTreeAreaWidth, overviewHeight)
293
293
  renderTreeOverview({
294
294
  model,
295
295
  ctx,
296
296
  theme,
297
- width: treeAreaWidth,
297
+ width: effectiveTreeAreaWidth,
298
298
  height: overviewHeight,
299
299
  })
300
300
  return (
301
301
  <ClipGroup
302
302
  panelId="tree-overview"
303
303
  clipId={`tree-overview-${id}`}
304
- width={treeAreaWidth}
304
+ width={effectiveTreeAreaWidth}
305
305
  height={overviewHeight}
306
306
  ctx={ctx}
307
307
  layers={layers}
@@ -349,14 +349,14 @@ function RowPanelHeadersSVG({
349
349
  theme: Theme
350
350
  bandHeight: number
351
351
  }) {
352
- const { resolvedRowPanels, treeAreaWidth, fontSize } = model
352
+ const { resolvedRowPanels, effectiveTreeAreaWidth, fontSize } = model
353
353
  const y = bandHeight - 4
354
354
  return resolvedRowPanels.length === 0 ? null : (
355
355
  <g id="rowpanel-headers">
356
356
  {resolvedRowPanels.map(panel => {
357
357
  const across = headerRunsAcross(panel.width)
358
358
  const size = across ? fontSize : Math.min(fontSize, panel.width)
359
- const center = treeAreaWidth + panel.offsetX + panel.width / 2
359
+ const center = effectiveTreeAreaWidth + panel.offsetX + panel.width / 2
360
360
  const x = across ? center : center + size / 3
361
361
  const room = across ? panel.width : y - LEGEND_PAD
362
362
  const maxChars = Math.floor(room / (size * CHAR_WIDTH))
@@ -409,13 +409,13 @@ function CladeLabelsSVG({
409
409
  resolvedClades,
410
410
  cladeGutterWidth,
411
411
  rowHeight,
412
- treeAreaWidth,
412
+ effectiveTreeAreaWidth,
413
413
  fontSize,
414
414
  } = model
415
415
  if (cladeGutterWidth === 0) {
416
416
  return null
417
417
  }
418
- const gutterLeft = treeAreaWidth - cladeGutterWidth
418
+ const gutterLeft = effectiveTreeAreaWidth - cladeGutterWidth
419
419
  return (
420
420
  <g id="clade-labels">
421
421
  {resolvedClades.map((clade, index) => {
@@ -427,7 +427,9 @@ function CladeLabelsSVG({
427
427
  const top = rows[0] * rowHeight - offsetY
428
428
  const height = cladeHeight(clade, rowHeight)
429
429
  const horizontal = cladeLabelHorizontal(clade, rowHeight, fontSize)
430
- const room = horizontal ? treeAreaWidth - left - bracketGap : height
430
+ const room = horizontal
431
+ ? effectiveTreeAreaWidth - left - bracketGap
432
+ : height
431
433
  const x = horizontal ? left : left + fontSize
432
434
  const y = horizontal ? top + height / 2 + fontSize / 3 : top + height
433
435
  return (
@@ -520,9 +522,9 @@ interface LayerProps {
520
522
 
521
523
  function CoreRendering({ model, theme, layout, Context, layers }: LayerProps) {
522
524
  const { contentHeight, offsetX, offsetY, msaAreaWidth } = layout
523
- const { treeAreaWidth, rowPanelsWidth, id } = model
525
+ const { effectiveTreeAreaWidth, rowPanelsWidth, id } = model
524
526
 
525
- const treeCtx = new Context(treeAreaWidth, contentHeight)
527
+ const treeCtx = new Context(effectiveTreeAreaWidth, contentHeight)
526
528
  renderTreeCanvas({
527
529
  model,
528
530
  theme,
@@ -588,7 +590,7 @@ function CoreRendering({ model, theme, layout, Context, layers }: LayerProps) {
588
590
  <ClipGroup
589
591
  panelId="tree-panel"
590
592
  clipId={`tree-${id}`}
591
- width={treeAreaWidth}
593
+ width={effectiveTreeAreaWidth}
592
594
  height={contentHeight}
593
595
  ctx={treeCtx}
594
596
  layers={layers}
@@ -600,7 +602,7 @@ function CoreRendering({ model, theme, layout, Context, layers }: LayerProps) {
600
602
  clipId={`rowpanels-${id}`}
601
603
  width={rowPanelsWidth}
602
604
  height={contentHeight}
603
- transform={`translate(${treeAreaWidth} 0)`}
605
+ transform={`translate(${effectiveTreeAreaWidth} 0)`}
604
606
  ctx={rowPanelsCtx}
605
607
  layers={layers}
606
608
  />
@@ -610,7 +612,7 @@ function CoreRendering({ model, theme, layout, Context, layers }: LayerProps) {
610
612
  clipId={`msa-${id}`}
611
613
  width={msaAreaWidth}
612
614
  height={contentHeight}
613
- transform={`translate(${treeAreaWidth + rowPanelsWidth} 0)`}
615
+ transform={`translate(${effectiveTreeAreaWidth + rowPanelsWidth} 0)`}
614
616
  ctx={msaCtx}
615
617
  layers={layers}
616
618
  underlay={raster}
@@ -677,7 +679,7 @@ function rasterBackground({
677
679
 
678
680
  function TrackRendering({ model, theme, layout, Context, layers }: LayerProps) {
679
681
  const { trackHeight, offsetX, msaAreaWidth } = layout
680
- const { treeAreaWidth, rowPanelsWidth, id } = model
682
+ const { effectiveTreeAreaWidth, rowPanelsWidth, id } = model
681
683
 
682
684
  const ctx = new Context(msaAreaWidth, trackHeight)
683
685
  renderAllTracks({
@@ -692,7 +694,7 @@ function TrackRendering({ model, theme, layout, Context, layers }: LayerProps) {
692
694
  return (
693
695
  <>
694
696
  <TrackLabelsSVG model={model} theme={theme} />
695
- <g transform={`translate(${treeAreaWidth + rowPanelsWidth} 0)`}>
697
+ <g transform={`translate(${effectiveTreeAreaWidth + rowPanelsWidth} 0)`}>
696
698
  <ClipGroup
697
699
  panelId="tracks-panel"
698
700
  clipId={`tracks-${id}`}
@@ -716,7 +718,7 @@ function TrackLabelsSVG({
716
718
  model: MsaViewModel
717
719
  theme: Theme
718
720
  }) {
719
- const { turnedOnTracks, treeAreaWidth, fontSize, drawLabels } = model
721
+ const { turnedOnTracks, effectiveTreeAreaWidth, fontSize, drawLabels } = model
720
722
  if (!drawLabels) {
721
723
  return null
722
724
  }
@@ -734,7 +736,7 @@ function TrackLabelsSVG({
734
736
  return size < 5 ? null : (
735
737
  <text
736
738
  key={id}
737
- x={treeAreaWidth - 4}
739
+ x={effectiveTreeAreaWidth - 4}
738
740
  y={baseline}
739
741
  fontSize={size}
740
742
  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/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '8.4.0'
1
+ export const version = '9.0.0'