react-msaview 5.8.0 → 5.10.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 (80) hide show
  1. package/bundle/index.js +84 -84
  2. package/bundle/index.js.map +3 -3
  3. package/dist/components/ConservationTrack.js +21 -16
  4. package/dist/components/ConservationTrack.js.map +1 -1
  5. package/dist/components/MSAView.js +17 -23
  6. package/dist/components/MSAView.js.map +1 -1
  7. package/dist/components/TextTrack.js +26 -21
  8. package/dist/components/TextTrack.js.map +1 -1
  9. package/dist/components/header/settingsMenuItems.js +39 -72
  10. package/dist/components/header/settingsMenuItems.js.map +1 -1
  11. package/dist/components/msa/DomainLegend.js +4 -5
  12. package/dist/components/msa/DomainLegend.js.map +1 -1
  13. package/dist/components/msa/MSACanvasBlock.js +19 -16
  14. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  15. package/dist/components/msa/MSAMouseoverCanvas.js +13 -6
  16. package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
  17. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +9 -6
  18. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  19. package/dist/components/msa/renderMSAMouseover.js +31 -31
  20. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  21. package/dist/components/overlayColors.d.ts +2 -0
  22. package/dist/components/overlayColors.js +5 -0
  23. package/dist/components/overlayColors.js.map +1 -1
  24. package/dist/components/tree/TreeCanvas.js +29 -24
  25. package/dist/components/tree/TreeCanvas.js.map +1 -1
  26. package/dist/components/tree/TreeCanvasBlock.js +19 -12
  27. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  28. package/dist/components/tree/renderTreeCanvas.d.ts +0 -38
  29. package/dist/components/tree/renderTreeCanvas.js +17 -17
  30. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  31. package/dist/constants.d.ts +1 -0
  32. package/dist/constants.js +1 -0
  33. package/dist/constants.js.map +1 -1
  34. package/dist/model.d.ts +16 -1
  35. package/dist/model.js +99 -114
  36. package/dist/model.js.map +1 -1
  37. package/dist/neighborJoining.js +12 -19
  38. package/dist/neighborJoining.js.map +1 -1
  39. package/dist/parseAsn1.js +32 -43
  40. package/dist/parseAsn1.js.map +1 -1
  41. package/dist/renderToSvg.js +3 -0
  42. package/dist/renderToSvg.js.map +1 -1
  43. package/dist/seqPosToGlobalCol.d.ts +6 -19
  44. package/dist/seqPosToGlobalCol.js +6 -33
  45. package/dist/seqPosToGlobalCol.js.map +1 -1
  46. package/dist/useCanvasAutorun.d.ts +23 -1
  47. package/dist/useCanvasAutorun.js +20 -8
  48. package/dist/useCanvasAutorun.js.map +1 -1
  49. package/dist/version.d.ts +1 -1
  50. package/dist/version.js +1 -1
  51. package/dist/version.js.map +1 -1
  52. package/package.json +3 -3
  53. package/src/components/ConservationTrack.tsx +7 -5
  54. package/src/components/MSAView.tsx +24 -30
  55. package/src/components/TextTrack.tsx +7 -12
  56. package/src/components/header/settingsMenuItems.ts +40 -72
  57. package/src/components/msa/DomainLegend.tsx +4 -3
  58. package/src/components/msa/MSACanvasBlock.tsx +16 -21
  59. package/src/components/msa/MSAMouseoverCanvas.tsx +10 -6
  60. package/src/components/msa/renderBoxFeatureCanvasBlock.test.ts +51 -0
  61. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +13 -8
  62. package/src/components/msa/renderMSAMouseover.ts +37 -32
  63. package/src/components/overlayColors.ts +6 -0
  64. package/src/components/tree/TreeCanvas.tsx +6 -4
  65. package/src/components/tree/TreeCanvasBlock.tsx +11 -12
  66. package/src/components/tree/renderTreeCanvas.ts +20 -17
  67. package/src/constants.ts +1 -0
  68. package/src/model.ts +114 -130
  69. package/src/modelFilehandleLoaders.test.ts +137 -0
  70. package/src/neighborJoining.test.ts +12 -0
  71. package/src/neighborJoining.ts +12 -20
  72. package/src/parseAsn1.ts +32 -40
  73. package/src/{renderDomainLegendSvg.test.tsx → renderDomainsSvg.test.tsx} +44 -20
  74. package/src/renderToSvg.tsx +3 -0
  75. package/src/seqPosToGlobalCol.test.ts +39 -51
  76. package/src/seqPosToGlobalCol.ts +6 -41
  77. package/src/stripDefaultSnapshot.test.ts +11 -0
  78. package/src/useCanvasAutorun.test.tsx +98 -0
  79. package/src/useCanvasAutorun.ts +30 -11
  80. package/src/version.ts +1 -1
@@ -1,41 +1,34 @@
1
1
  import type { MsaViewModel } from '../../model.ts'
2
2
  import type { MenuItem } from '@jbrowse/core/ui/Menu'
3
3
 
4
+ function toggle(label: string, checked: boolean, set: (arg: boolean) => void) {
5
+ return {
6
+ label,
7
+ type: 'checkbox' as const,
8
+ checked,
9
+ onClick: () => {
10
+ set(!checked)
11
+ },
12
+ }
13
+ }
14
+
4
15
  export function msaSettingsMenuItems(model: MsaViewModel): MenuItem[] {
5
16
  const { drawMsaLetters, hideGaps, bgColor, showColumnStats } = model
6
17
  return [
7
- {
8
- label: 'Show column statistics on hover',
9
- type: 'checkbox',
10
- checked: showColumnStats,
11
- onClick: () => {
12
- model.setShowColumnStats(!showColumnStats)
13
- },
14
- },
15
- {
16
- label: 'Draw letters',
17
- type: 'checkbox',
18
- checked: drawMsaLetters,
19
- onClick: () => {
20
- model.setDrawMsaLetters(!drawMsaLetters)
21
- },
22
- },
23
- {
24
- label: 'Color letters instead of background of tiles',
25
- type: 'checkbox',
26
- checked: !bgColor,
27
- onClick: () => {
28
- model.setBgColor(!bgColor)
29
- },
30
- },
31
- {
32
- label: 'Enable hiding gappy columns?',
33
- type: 'checkbox',
34
- checked: hideGaps,
35
- onClick: () => {
36
- model.setHideGaps(!hideGaps)
37
- },
38
- },
18
+ toggle('Show column statistics on hover', showColumnStats, arg => {
19
+ model.setShowColumnStats(arg)
20
+ }),
21
+ toggle('Draw letters', drawMsaLetters, arg => {
22
+ model.setDrawMsaLetters(arg)
23
+ }),
24
+ // the checkbox reads as the inverse of the property it sets: bgColor draws
25
+ // the tile, and turning it off is what leaves the letter itself colored
26
+ toggle('Color letters instead of background of tiles', !bgColor, arg => {
27
+ model.setBgColor(!arg)
28
+ }),
29
+ toggle('Enable hiding gappy columns?', hideGaps, arg => {
30
+ model.setHideGaps(arg)
31
+ }),
39
32
  ]
40
33
  }
41
34
 
@@ -48,45 +41,20 @@ export function treeSettingsMenuItems(model: MsaViewModel): MenuItem[] {
48
41
  drawLabels,
49
42
  } = model
50
43
  return [
51
- {
52
- label: 'Show branch length',
53
- type: 'checkbox',
54
- checked: showBranchLen,
55
- onClick: () => {
56
- model.setShowBranchLen(!showBranchLen)
57
- },
58
- },
59
- {
60
- label: 'Show tree',
61
- type: 'checkbox',
62
- checked: drawTree,
63
- onClick: () => {
64
- model.setDrawTree(!drawTree)
65
- },
66
- },
67
- {
68
- label: 'Draw clickable bubbles on tree branches',
69
- type: 'checkbox',
70
- checked: drawNodeBubbles,
71
- onClick: () => {
72
- model.setDrawNodeBubbles(!drawNodeBubbles)
73
- },
74
- },
75
- {
76
- label: 'Tree labels align right',
77
- type: 'checkbox',
78
- checked: labelsAlignRight,
79
- onClick: () => {
80
- model.setLabelsAlignRight(!labelsAlignRight)
81
- },
82
- },
83
- {
84
- label: 'Draw labels',
85
- type: 'checkbox',
86
- checked: drawLabels,
87
- onClick: () => {
88
- model.setDrawLabels(!drawLabels)
89
- },
90
- },
44
+ toggle('Show branch length', showBranchLen, arg => {
45
+ model.setShowBranchLen(arg)
46
+ }),
47
+ toggle('Show tree', drawTree, arg => {
48
+ model.setDrawTree(arg)
49
+ }),
50
+ toggle('Draw clickable bubbles on tree branches', drawNodeBubbles, arg => {
51
+ model.setDrawNodeBubbles(arg)
52
+ }),
53
+ toggle('Tree labels align right', labelsAlignRight, arg => {
54
+ model.setLabelsAlignRight(arg)
55
+ }),
56
+ toggle('Draw labels', drawLabels, arg => {
57
+ model.setDrawLabels(arg)
58
+ }),
91
59
  ]
92
60
  }
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react'
1
+ import React from 'react'
2
2
 
3
3
  import ExpandLess from '@mui/icons-material/ExpandLess'
4
4
  import ExpandMore from '@mui/icons-material/ExpandMore'
@@ -8,11 +8,11 @@ import { observer } from 'mobx-react'
8
8
  import type { MsaViewModel } from '../../model.ts'
9
9
 
10
10
  const DomainLegend = observer(function ({ model }: { model: MsaViewModel }) {
11
- const [expanded, setExpanded] = useState(true)
12
11
  const {
13
12
  actuallyShowDomains,
14
13
  visibleDomainTypes: visible,
15
14
  fillPalette,
15
+ showDomainLegend: expanded,
16
16
  } = model
17
17
  const ExpandIcon = expanded ? ExpandLess : ExpandMore
18
18
 
@@ -41,8 +41,9 @@ const DomainLegend = observer(function ({ model }: { model: MsaViewModel }) {
41
41
  >
42
42
  <IconButton
43
43
  size="small"
44
+ title={expanded ? 'Collapse domain key' : 'Expand domain key'}
44
45
  onClick={() => {
45
- setExpanded(!expanded)
46
+ model.setShowDomainLegend(!expanded)
46
47
  }}
47
48
  >
48
49
  <ExpandIcon fontSize="small" />
@@ -31,24 +31,17 @@ const MSACanvasBlock = observer(function ({
31
31
  },
32
32
  )
33
33
 
34
- const ref = useCanvasAutorun(
35
- ctx => {
36
- const { blockSize, highResScaleFactor, actuallyShowDomains } = model
34
+ const canvasSize = blockSize * highResScaleFactor
35
+ const ref = useCanvasAutorun({
36
+ draw: ctx => {
37
37
  ctx.resetTransform()
38
- ctx.clearRect(
39
- 0,
40
- 0,
41
- blockSize * highResScaleFactor,
42
- blockSize * highResScaleFactor,
43
- )
44
- if (actuallyShowDomains) {
45
- renderBoxFeatureCanvasBlock({
46
- ctx,
47
- offsetX,
48
- offsetY,
49
- model,
50
- })
51
- }
38
+ ctx.clearRect(0, 0, canvasSize, canvasSize)
39
+ renderBoxFeatureCanvasBlock({
40
+ ctx,
41
+ offsetX,
42
+ offsetY,
43
+ model,
44
+ })
52
45
  renderMSABlock({
53
46
  ctx,
54
47
  theme,
@@ -58,8 +51,10 @@ const MSACanvasBlock = observer(function ({
58
51
  model,
59
52
  })
60
53
  },
61
- [model, offsetX, offsetY, theme, contrastScheme],
62
- )
54
+ width: canvasSize,
55
+ height: canvasSize,
56
+ deps: [model, offsetX, offsetY, theme, contrastScheme],
57
+ })
63
58
 
64
59
  const { hoveredInsertion, mouseOverDomains, showColumnStats } = model
65
60
 
@@ -80,8 +75,8 @@ const MSACanvasBlock = observer(function ({
80
75
  onMouseLeave={() => {
81
76
  onMouseLeave()
82
77
  }}
83
- width={blockSize * highResScaleFactor}
84
- height={blockSize * highResScaleFactor}
78
+ width={canvasSize}
79
+ height={canvasSize}
85
80
  style={{
86
81
  position: 'absolute',
87
82
  top: scrollY + offsetY,
@@ -16,21 +16,25 @@ const MSAMouseoverCanvas = observer(function ({
16
16
  const { height, msaAreaWidth, verticalScrollbarWidth, highResScaleFactor } =
17
17
  model
18
18
  const width = msaAreaWidth - verticalScrollbarWidth
19
- const ref = useCanvasAutorun(
20
- ctx => {
19
+ const canvasWidth = width * highResScaleFactor
20
+ const canvasHeight = height * highResScaleFactor
21
+ const ref = useCanvasAutorun({
22
+ draw: ctx => {
21
23
  if (isAlive(model)) {
22
24
  renderMouseover({ ctx, model })
23
25
  }
24
26
  },
25
- [model],
26
- )
27
+ width: canvasWidth,
28
+ height: canvasHeight,
29
+ deps: [model],
30
+ })
27
31
 
28
32
  return (
29
33
  <canvas
30
34
  ref={ref}
31
35
  id="mouseover"
32
- width={width * highResScaleFactor}
33
- height={height * highResScaleFactor}
36
+ width={canvasWidth}
37
+ height={canvasHeight}
34
38
  style={{
35
39
  position: 'absolute',
36
40
  top: 0,
@@ -0,0 +1,51 @@
1
+ import { expect, test } from 'vitest'
2
+
3
+ import stateModelFactory from '../../model.ts'
4
+ import { renderBoxFeatureCanvasBlock } from './renderBoxFeatureCanvasBlock.ts'
5
+
6
+ import type { RenderCtx } from '../renderCtx.ts'
7
+
8
+ const row = 'ACDEFGHIKLMNPQRSTVWY'.repeat(3)
9
+
10
+ // only `human` carries the gene model; `mouse` is there to push it off the top
11
+ // of the block
12
+ const msa = `>mouse\n${row}\n>human\n${row}`
13
+
14
+ const gff = `##gff-version 3
15
+ human src exon 1 20 . . . ID=human.exon1;Name=exon-1
16
+ human src exon 21 40 . . . ID=human.exon2;Name=exon-2
17
+ human src exon 41 60 . . . ID=human.exon3;Name=exon-3`
18
+
19
+ function drawnLabels() {
20
+ const model = stateModelFactory().create({
21
+ type: 'MsaView',
22
+ data: { msa },
23
+ })
24
+ model.setWidth(1000)
25
+ model.setRowHeight(20)
26
+ // segment numbers are drawn in place of residue letters, never on top of them
27
+ model.setDrawMsaLetters(false)
28
+ model.applyGFFText(gff)
29
+ expect(model.rowNames).toEqual(['mouse', 'human'])
30
+
31
+ const labels: string[] = []
32
+ const ctx = {
33
+ font: '12px sans-serif',
34
+ resetTransform() {},
35
+ scale() {},
36
+ translate() {},
37
+ fillRect() {},
38
+ strokeRect() {},
39
+ measureText: (text: string) => ({ width: text.length * 6 }),
40
+ fillText(text: string) {
41
+ labels.push(text)
42
+ },
43
+ } as unknown as RenderCtx
44
+
45
+ renderBoxFeatureCanvasBlock({ model, ctx, offsetX: 0, offsetY: 0 })
46
+ return labels
47
+ }
48
+
49
+ test('segment numbers are drawn once each, even when row 0 has no gene model', () => {
50
+ expect(drawnLabels()).toEqual(['1', '2', '3'])
51
+ })
@@ -77,8 +77,13 @@ function drawTiles({
77
77
  const xMin = offsetX - cull
78
78
  const xMax = offsetX + blockWidth + cull
79
79
 
80
- for (let i = 0, l1 = visibleLeaves.length; i < l1; i++) {
81
- const node = visibleLeaves[i]!
80
+ // a segment (exon) number labels its band once per block, on the topmost
81
+ // visible row carrying that segment, so it reads as a column header for the
82
+ // whole band. Keyed by accession rather than drawn on row 0, because the rows
83
+ // carrying the gene model are often not the first ones in the alignment
84
+ const labelled = drawSegmentLabels ? new Set<string>() : undefined
85
+
86
+ for (const node of visibleLeaves) {
82
87
  const y = node.x!
83
88
  const bands = domainBands.get(node.data.name)
84
89
 
@@ -94,15 +99,15 @@ function drawTiles({
94
99
  if (strand === undefined) {
95
100
  ctx.fillRect(x, t, lw, h)
96
101
  ctx.strokeRect(x, t, lw, h)
97
- // segment (exon) number drawn once per band, on the top visible row,
98
- // so it reads as a column header for the whole band
99
- const label = drawSegmentLabels
100
- ? segmentLabels.get(accession)
101
- : undefined
102
- if (label !== undefined && i === 0) {
102
+ const label =
103
+ labelled && !labelled.has(accession)
104
+ ? segmentLabels.get(accession)
105
+ : undefined
106
+ if (label !== undefined) {
103
107
  const fontSize = Math.min(h - 2, 11)
104
108
  ctx.font = `${fontSize}px sans-serif`
105
109
  if (ctx.measureText(label).width + 2 <= lw) {
110
+ labelled!.add(accession)
106
111
  ctx.fillStyle = '#222'
107
112
  ctx.textAlign = 'center'
108
113
  ctx.textBaseline = 'middle'
@@ -1,9 +1,12 @@
1
- import { multiRowHoverColor, referenceColor } from '../overlayColors.ts'
1
+ import {
2
+ clickColor,
3
+ hoverColor,
4
+ multiRowHoverColor,
5
+ referenceColor,
6
+ } from '../overlayColors.ts'
2
7
 
3
8
  import type { MsaViewModel } from '../../model.ts'
4
9
 
5
- const hoverColor = 'rgba(0,0,0,0.15)'
6
- const highlightColor = 'rgba(128,128,0,0.2)'
7
10
  // the persistent highlightColumns overlay: a stronger fill plus a solid border
8
11
  // so a domain/motif band reads clearly over the colored alignment cells (the
9
12
  // faint hover-style wash alone is invisible against clustalx coloring)
@@ -39,48 +42,50 @@ export function renderMouseover({
39
42
  ctx.clearRect(0, 0, width * highResScaleFactor, height * highResScaleFactor)
40
43
  ctx.scale(highResScaleFactor, highResScaleFactor)
41
44
 
42
- // Highlight reference row (relativeTo) persistently
45
+ // rows and columns sit at a fixed pitch, so an index is all it takes to place
46
+ // a full-width or full-height band (matching fillRow in TreeCanvas)
47
+ const rowBand = (index: number) => {
48
+ ctx.fillRect(0, index * rowHeight + scrollY, width, rowHeight)
49
+ }
50
+ const colBand = (index: number) => {
51
+ ctx.fillRect(index * colWidth + scrollX, 0, colWidth, height)
52
+ }
53
+
54
+ // the reference row (relativeTo) stays lit, as does every tip under a hovered
55
+ // tree node
43
56
  if (referenceRowIndex !== undefined) {
44
57
  ctx.fillStyle = referenceColor
45
- ctx.fillRect(0, referenceRowIndex * rowHeight + scrollY, width, rowHeight)
58
+ rowBand(referenceRowIndex)
46
59
  }
47
-
48
- // Highlight multiple rows when hovering over tree nodes with children
49
- if (hoveredRowIndices.length > 0) {
50
- ctx.fillStyle = multiRowHoverColor
51
- for (const rowIndex of hoveredRowIndices) {
52
- ctx.fillRect(0, rowIndex * rowHeight + scrollY, width, rowHeight)
53
- }
60
+ ctx.fillStyle = multiRowHoverColor
61
+ for (const rowIndex of hoveredRowIndices) {
62
+ rowBand(rowIndex)
54
63
  }
55
64
 
56
- // Highlight multiple columns draw each contiguous run as a filled, bordered
57
- // band so a domain/motif highlight is clearly visible over the alignment
58
- if (highlightedColumnRuns.length > 0) {
59
- ctx.lineWidth = 2
60
- for (const { start, end } of highlightedColumnRuns) {
61
- const x = start * colWidth + scrollX
62
- const w = (end - start + 1) * colWidth
63
- ctx.fillStyle = highlightColumnsFill
64
- ctx.fillRect(x, 0, w, height)
65
- ctx.strokeStyle = highlightColumnsBorder
66
- ctx.strokeRect(x, 0, w, height)
67
- }
65
+ // each contiguous run of highlighted columns draws as one filled, bordered
66
+ // band, so a domain/motif highlight stays visible over the alignment
67
+ ctx.lineWidth = 2
68
+ for (const { start, end } of highlightedColumnRuns) {
69
+ const x = start * colWidth + scrollX
70
+ const w = (end - start + 1) * colWidth
71
+ ctx.fillStyle = highlightColumnsFill
72
+ ctx.fillRect(x, 0, w, height)
73
+ ctx.strokeStyle = highlightColumnsBorder
74
+ ctx.strokeRect(x, 0, w, height)
68
75
  }
69
76
 
77
+ ctx.fillStyle = hoverColor
70
78
  if (mouseCol !== undefined) {
71
- ctx.fillStyle = hoverColor
72
- ctx.fillRect(mouseCol * colWidth + scrollX, 0, colWidth, height)
79
+ colBand(mouseCol)
73
80
  }
74
81
  if (mouseRow !== undefined) {
75
- ctx.fillStyle = hoverColor
76
- ctx.fillRect(0, mouseRow * rowHeight + scrollY, width, rowHeight)
82
+ rowBand(mouseRow)
77
83
  }
84
+ ctx.fillStyle = clickColor
78
85
  if (mouseClickCol !== undefined) {
79
- ctx.fillStyle = highlightColor
80
- ctx.fillRect(mouseClickCol * colWidth + scrollX, 0, colWidth, height)
86
+ colBand(mouseClickCol)
81
87
  }
82
88
  if (mouseClickRow !== undefined) {
83
- ctx.fillStyle = highlightColor
84
- ctx.fillRect(0, mouseClickRow * rowHeight + scrollY, width, rowHeight)
89
+ rowBand(mouseClickRow)
85
90
  }
86
91
  }
@@ -10,3 +10,9 @@ export const referenceColor = 'rgba(0,128,255,0.3)'
10
10
  // while the alignment cells are already colored so a lighter wash reads better.
11
11
  export const treeHoverColor = 'rgba(255,165,0,0.2)'
12
12
  export const multiRowHoverColor = 'rgba(255,165,0,0.15)'
13
+
14
+ // the hovered column/row, and the one pinned by a click. Drawn on the alignment
15
+ // overlay canvas and again as plain divs over the track area, which have to
16
+ // match or the crosshair appears to change color as it crosses the tracks.
17
+ export const hoverColor = 'rgba(0,0,0,0.15)'
18
+ export const clickColor = 'rgba(128,128,0,0.2)'
@@ -22,8 +22,8 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
22
22
  )
23
23
  const { onMouseDown, onMouseUp } = useWheelScroll({ ref, onScrollY })
24
24
 
25
- const mouseoverRef = useCanvasAutorun(
26
- ctx => {
25
+ const mouseoverRef = useCanvasAutorun({
26
+ draw: ctx => {
27
27
  if (isAlive(model)) {
28
28
  const {
29
29
  rowHeight,
@@ -61,8 +61,10 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
61
61
  }
62
62
  }
63
63
  },
64
- [model],
65
- )
64
+ width: treeAreaWidth,
65
+ height,
66
+ deps: [model],
67
+ })
66
68
 
67
69
  return (
68
70
  <div
@@ -60,15 +60,12 @@ const TreeCanvasBlock = observer(function ({
60
60
  const width = treeAreaWidth + padding
61
61
  const height = blockSize
62
62
 
63
- const ref = useCanvasAutorun(
64
- ctx => {
63
+ const canvasWidth = width * highResScaleFactor
64
+ const canvasHeight = height * highResScaleFactor
65
+ const ref = useCanvasAutorun({
66
+ draw: ctx => {
65
67
  ctx.resetTransform()
66
- ctx.clearRect(
67
- 0,
68
- 0,
69
- width * highResScaleFactor,
70
- height * highResScaleFactor,
71
- )
68
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight)
72
69
  renderTreeCanvas({
73
70
  ctx,
74
71
  model,
@@ -77,8 +74,10 @@ const TreeCanvasBlock = observer(function ({
77
74
  theme,
78
75
  })
79
76
  },
80
- [model, clickMap, offsetY, theme],
81
- )
77
+ width: canvasWidth,
78
+ height: canvasHeight,
79
+ deps: [model, clickMap, offsetY, theme],
80
+ })
82
81
 
83
82
  const style = {
84
83
  width,
@@ -112,8 +111,8 @@ const TreeCanvasBlock = observer(function ({
112
111
 
113
112
  <canvas
114
113
  ref={ref}
115
- width={width * highResScaleFactor}
116
- height={height * highResScaleFactor}
114
+ width={canvasWidth}
115
+ height={canvasHeight}
117
116
  style={{ ...style, cursor: hovered ? 'pointer' : 'default' }}
118
117
  onMouseMove={event => {
119
118
  onMouseMove(event)
@@ -45,7 +45,7 @@ export function getNodeX(
45
45
  return ((maxDepthToLeaf - depthToLeaf) / maxDepthToLeaf) * tipX
46
46
  }
47
47
 
48
- export function renderTree({
48
+ function renderTree({
49
49
  offsetY,
50
50
  ctx,
51
51
  model,
@@ -89,7 +89,7 @@ export function renderTree({
89
89
  })
90
90
  }
91
91
 
92
- export function renderCollapsedTriangles({
92
+ function renderCollapsedTriangles({
93
93
  ctx,
94
94
  clickMap,
95
95
  offsetY,
@@ -98,6 +98,7 @@ export function renderCollapsedTriangles({
98
98
  tipX,
99
99
  maxDepthToLeaf,
100
100
  blockSizeYOverride,
101
+ collapsedSet,
101
102
  }: {
102
103
  ctx: RenderCtx
103
104
  clickMap?: ClickMapIndex
@@ -107,11 +108,11 @@ export function renderCollapsedTriangles({
107
108
  tipX: number
108
109
  maxDepthToLeaf: number
109
110
  blockSizeYOverride?: number
111
+ collapsedSet: Set<string>
110
112
  }) {
111
113
  const {
112
114
  hierarchy,
113
115
  showBranchLenEffective: showBranchLen,
114
- collapsed,
115
116
  blockSize,
116
117
  rowHeight,
117
118
  fontSize,
@@ -119,10 +120,9 @@ export function renderCollapsedTriangles({
119
120
  } = model
120
121
  // nothing collapsed is the common case, and the traversal below visits every
121
122
  // node in the tree on every block of every redraw
122
- if (collapsed.length === 0) {
123
+ if (collapsedSet.size === 0) {
123
124
  return
124
125
  }
125
- const collapsedSet = new Set(collapsed)
126
126
  const by = blockSizeYOverride ?? blockSize
127
127
  const halfHeight = Math.max(2, rowHeight * 0.42)
128
128
  forEachDescendant(hierarchy, node => {
@@ -171,7 +171,7 @@ export function renderCollapsedTriangles({
171
171
  })
172
172
  }
173
173
 
174
- export function renderNodeBubbles({
174
+ function renderNodeBubbles({
175
175
  ctx,
176
176
  clickMap,
177
177
  offsetY,
@@ -179,6 +179,7 @@ export function renderNodeBubbles({
179
179
  tipX,
180
180
  maxDepthToLeaf,
181
181
  blockSizeYOverride,
182
+ collapsedSet,
182
183
  }: {
183
184
  ctx: RenderCtx
184
185
  clickMap?: ClickMapIndex
@@ -187,16 +188,15 @@ export function renderNodeBubbles({
187
188
  tipX: number
188
189
  maxDepthToLeaf: number
189
190
  blockSizeYOverride?: number
191
+ collapsedSet: Set<string>
190
192
  }) {
191
193
  const {
192
194
  hierarchy,
193
195
  showBranchLenEffective: showBranchLen,
194
- collapsed,
195
196
  blockSize,
196
197
  marginLeft: ml,
197
198
  } = model
198
199
  const by = blockSizeYOverride ?? blockSize
199
- const collapsedSet = new Set(collapsed)
200
200
  forEachDescendant(hierarchy, node => {
201
201
  const x = getNodeX(node, showBranchLen, tipX, maxDepthToLeaf)
202
202
  if (x === undefined) {
@@ -231,7 +231,7 @@ export function renderNodeBubbles({
231
231
  })
232
232
  }
233
233
 
234
- export function renderTreeLabels({
234
+ function renderTreeLabels({
235
235
  theme,
236
236
  model,
237
237
  offsetY,
@@ -240,6 +240,7 @@ export function renderTreeLabels({
240
240
  tipX,
241
241
  maxDepthToLeaf,
242
242
  blockSizeYOverride,
243
+ collapsedSet,
243
244
  }: {
244
245
  model: MsaViewModel
245
246
  offsetY: number
@@ -249,6 +250,7 @@ export function renderTreeLabels({
249
250
  tipX: number
250
251
  maxDepthToLeaf: number
251
252
  blockSizeYOverride?: number
253
+ collapsedSet: Set<string>
252
254
  }) {
253
255
  const {
254
256
  fontSize,
@@ -262,16 +264,11 @@ export function renderTreeLabels({
262
264
  marginLeft,
263
265
  noTree,
264
266
  labelWidthMap,
265
- collapsed,
266
267
  } = model
268
+ const by = blockSizeYOverride ?? blockSize
267
269
  // labels only exist for leaves, which are laid out top to bottom, so take the
268
270
  // same slice the MSA renderer uses instead of walking every tip per block
269
- const visibleLeaves = getVisibleLeaves({
270
- model,
271
- offsetY,
272
- blockSizeY: blockSizeYOverride ?? model.blockSize,
273
- })
274
- const by = blockSizeYOverride ?? blockSize
271
+ const visibleLeaves = getVisibleLeaves({ model, offsetY, blockSizeY: by })
275
272
  const emHeight = ctx.measureText('M').width
276
273
  if (labelsAlignRight) {
277
274
  ctx.textAlign = 'right'
@@ -289,7 +286,7 @@ export function renderTreeLabels({
289
286
  const displayName = treeMetadata[name]?.genome || name
290
287
  // a collapsed clade is drawn as a triangle + tip count; suppress its leaf
291
288
  // label when the "name" is just the auto-generated internal-node id
292
- const isAnonymousCollapsed = collapsed.includes(id) && name === id
289
+ const isAnonymousCollapsed = collapsedSet.has(id) && name === id
293
290
  if (!isAnonymousCollapsed && inYBlock(y, offsetY, by)) {
294
291
  // note: +rowHeight/4 matches with -rowHeight/4 in msa
295
292
  const yp = y + fontSize / 4
@@ -389,6 +386,9 @@ export function renderTreeCanvas({
389
386
  // maxBranchLength for the cladogram would pin every node to x=0 for a tree
390
387
  // with no branch lengths -- which is exactly what forces cladogram mode.
391
388
  const tipX = showBranchLenEffective ? maxBranchLength : treeWidth
389
+ // built once for all three passes: each of them asks about a different node on
390
+ // every block of every redraw, and `collapsed` is a list
391
+ const collapsedSet = new Set(model.collapsed)
392
392
 
393
393
  if (!noTree && drawTree) {
394
394
  renderTree({
@@ -410,6 +410,7 @@ export function renderTreeCanvas({
410
410
  tipX,
411
411
  maxDepthToLeaf,
412
412
  blockSizeYOverride,
413
+ collapsedSet,
413
414
  })
414
415
 
415
416
  if (drawNodeBubbles) {
@@ -421,6 +422,7 @@ export function renderTreeCanvas({
421
422
  tipX,
422
423
  maxDepthToLeaf,
423
424
  blockSizeYOverride,
425
+ collapsedSet,
424
426
  })
425
427
  }
426
428
  }
@@ -435,6 +437,7 @@ export function renderTreeCanvas({
435
437
  tipX,
436
438
  maxDepthToLeaf,
437
439
  blockSizeYOverride,
440
+ collapsedSet,
438
441
  })
439
442
  }
440
443