react-msaview 5.5.0 → 5.6.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 (139) hide show
  1. package/bundle/index.js +173 -162
  2. package/bundle/index.js.map +4 -4
  3. package/dist/components/ConservationTrack.js +20 -26
  4. package/dist/components/ConservationTrack.js.map +1 -1
  5. package/dist/components/MSAViewer.d.ts +7 -1
  6. package/dist/components/MSAViewer.js +4 -1
  7. package/dist/components/MSAViewer.js.map +1 -1
  8. package/dist/components/SequenceTextArea.js +1 -1
  9. package/dist/components/SequenceTextArea.js.map +1 -1
  10. package/dist/components/TextTrack.js +20 -27
  11. package/dist/components/TextTrack.js.map +1 -1
  12. package/dist/components/Track.js +1 -1
  13. package/dist/components/Track.js.map +1 -1
  14. package/dist/components/dialogs/TrackInfoDialog.js +1 -1
  15. package/dist/components/dialogs/TrackInfoDialog.js.map +1 -1
  16. package/dist/components/header/ColorSchemeMenu.js +1 -1
  17. package/dist/components/header/ColorSchemeMenu.js.map +1 -1
  18. package/dist/components/header/FileMenu.js +1 -16
  19. package/dist/components/header/FileMenu.js.map +1 -1
  20. package/dist/components/header/Header.js +6 -5
  21. package/dist/components/header/Header.js.map +1 -1
  22. package/dist/components/header/HeaderInfoArea.js +1 -1
  23. package/dist/components/header/HeaderInfoArea.js.map +1 -1
  24. package/dist/components/header/HeaderStatusArea.js +1 -1
  25. package/dist/components/header/HeaderStatusArea.js.map +1 -1
  26. package/dist/components/header/MSASettingsMenu.js +1 -1
  27. package/dist/components/header/MSASettingsMenu.js.map +1 -1
  28. package/dist/components/header/TreeSettingsMenu.js +1 -1
  29. package/dist/components/header/TreeSettingsMenu.js.map +1 -1
  30. package/dist/components/header/ZoomControls.js +14 -12
  31. package/dist/components/header/ZoomControls.js.map +1 -1
  32. package/dist/components/header/ZoomMenu.js +1 -10
  33. package/dist/components/header/ZoomMenu.js.map +1 -1
  34. package/dist/components/header/settingsMenuItems.js +9 -1
  35. package/dist/components/header/settingsMenuItems.js.map +1 -1
  36. package/dist/components/{header/HeaderMenu.d.ts → msa/ColumnStats.d.ts} +3 -3
  37. package/dist/components/msa/ColumnStats.js +42 -0
  38. package/dist/components/msa/ColumnStats.js.map +1 -0
  39. package/dist/components/msa/DomainLegend.js +1 -1
  40. package/dist/components/msa/DomainLegend.js.map +1 -1
  41. package/dist/components/msa/MSACanvasBlock.js +39 -39
  42. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  43. package/dist/components/msa/MSAMouseoverCanvas.js +6 -12
  44. package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
  45. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +3 -1
  46. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  47. package/dist/components/msa/renderMSAMouseover.js +1 -2
  48. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  49. package/dist/components/overlayColors.d.ts +3 -0
  50. package/dist/components/overlayColors.js +11 -0
  51. package/dist/components/overlayColors.js.map +1 -0
  52. package/dist/components/tracks/renderTracksSvg.d.ts +6 -3
  53. package/dist/components/tracks/renderTracksSvg.js +17 -8
  54. package/dist/components/tracks/renderTracksSvg.js.map +1 -1
  55. package/dist/components/tree/TreeCanvas.js +31 -38
  56. package/dist/components/tree/TreeCanvas.js.map +1 -1
  57. package/dist/components/tree/TreeCanvasBlock.js +12 -47
  58. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  59. package/dist/components/tree/clickMap.d.ts +24 -0
  60. package/dist/components/tree/clickMap.js +33 -0
  61. package/dist/components/tree/clickMap.js.map +1 -0
  62. package/dist/components/tree/renderTreeCanvas.d.ts +1 -21
  63. package/dist/components/tree/renderTreeCanvas.js +8 -7
  64. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  65. package/dist/constants.d.ts +1 -0
  66. package/dist/constants.js +1 -0
  67. package/dist/constants.js.map +1 -1
  68. package/dist/fetchUtils.d.ts +2 -1
  69. package/dist/fetchUtils.js +3 -2
  70. package/dist/fetchUtils.js.map +1 -1
  71. package/dist/index.d.ts +1 -1
  72. package/dist/model/msaModel.d.ts +25 -2
  73. package/dist/model/msaModel.js +15 -3
  74. package/dist/model/msaModel.js.map +1 -1
  75. package/dist/model/treeModel.d.ts +46 -5
  76. package/dist/model/treeModel.js +20 -7
  77. package/dist/model/treeModel.js.map +1 -1
  78. package/dist/model.d.ts +545 -132
  79. package/dist/model.js +101 -75
  80. package/dist/model.js.map +1 -1
  81. package/dist/propertyConservation.d.ts +7 -0
  82. package/dist/propertyConservation.js +66 -0
  83. package/dist/propertyConservation.js.map +1 -0
  84. package/dist/types.d.ts +4 -1
  85. package/dist/useCanvasAutorun.d.ts +2 -0
  86. package/dist/useCanvasAutorun.js +23 -0
  87. package/dist/useCanvasAutorun.js.map +1 -0
  88. package/dist/version.d.ts +1 -1
  89. package/dist/version.js +1 -1
  90. package/package.json +4 -6
  91. package/src/colorSchemes.docs.test.ts +55 -0
  92. package/src/columnStats.test.ts +94 -0
  93. package/src/components/ConservationTrack.tsx +17 -15
  94. package/src/components/MSAViewer.tsx +12 -0
  95. package/src/components/SequenceTextArea.tsx +1 -1
  96. package/src/components/TextTrack.tsx +7 -11
  97. package/src/components/Track.tsx +1 -1
  98. package/src/components/dialogs/TrackInfoDialog.tsx +1 -1
  99. package/src/components/header/ColorSchemeMenu.tsx +1 -0
  100. package/src/components/header/FileMenu.tsx +1 -15
  101. package/src/components/header/Header.tsx +11 -8
  102. package/src/components/header/HeaderInfoArea.tsx +1 -1
  103. package/src/components/header/HeaderStatusArea.tsx +1 -1
  104. package/src/components/header/MSASettingsMenu.tsx +1 -0
  105. package/src/components/header/TreeSettingsMenu.tsx +1 -0
  106. package/src/components/header/ZoomControls.tsx +24 -17
  107. package/src/components/header/ZoomMenu.tsx +1 -10
  108. package/src/components/header/settingsMenuItems.ts +9 -1
  109. package/src/components/msa/ColumnStats.tsx +54 -0
  110. package/src/components/msa/DomainLegend.tsx +5 -1
  111. package/src/components/msa/MSACanvasBlock.tsx +29 -26
  112. package/src/components/msa/MSAMouseoverCanvas.tsx +10 -13
  113. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +3 -1
  114. package/src/components/msa/renderMSAMouseover.ts +2 -2
  115. package/src/components/overlayColors.ts +12 -0
  116. package/src/components/tracks/renderTracksSvg.ts +22 -8
  117. package/src/components/tree/TreeCanvas.tsx +47 -54
  118. package/src/components/tree/TreeCanvasBlock.tsx +10 -68
  119. package/src/components/tree/clickMap.ts +55 -0
  120. package/src/components/tree/renderTreeCanvas.ts +10 -34
  121. package/src/constants.ts +1 -0
  122. package/src/fetchUtils.ts +6 -3
  123. package/src/index.ts +1 -1
  124. package/src/model/msaModel.ts +21 -3
  125. package/src/model/treeModel.ts +21 -7
  126. package/src/model.ts +116 -117
  127. package/src/propertyConservation.test.ts +47 -0
  128. package/src/propertyConservation.ts +73 -0
  129. package/src/stripDefaultSnapshot.test.ts +54 -0
  130. package/src/types.ts +7 -1
  131. package/src/useCanvasAutorun.ts +28 -0
  132. package/src/version.ts +1 -1
  133. package/dist/components/dialogs/SettingsDialog.d.ts +0 -7
  134. package/dist/components/dialogs/SettingsDialog.js +0 -100
  135. package/dist/components/dialogs/SettingsDialog.js.map +0 -1
  136. package/dist/components/header/HeaderMenu.js +0 -27
  137. package/dist/components/header/HeaderMenu.js.map +0 -1
  138. package/src/components/dialogs/SettingsDialog.tsx +0 -234
  139. package/src/components/header/HeaderMenu.tsx +0 -38
@@ -1,11 +1,12 @@
1
- import React, { useEffect, useRef, useState } from 'react'
1
+ import React, { useState } from 'react'
2
2
 
3
3
  import { BaseTooltip } from '@jbrowse/core/ui'
4
- import { autorun } from 'mobx'
5
4
  import { observer } from 'mobx-react'
6
5
 
6
+ import ColumnStats from './ColumnStats.tsx'
7
7
  import { renderBoxFeatureCanvasBlock } from './renderBoxFeatureCanvasBlock.ts'
8
8
  import { renderMSABlock } from './renderMSABlock.ts'
9
+ import { useCanvasAutorun } from '../../useCanvasAutorun.ts'
9
10
  import { useColorContrast } from '../../useColorContrast.ts'
10
11
 
11
12
  import type { MsaViewModel } from '../../model.ts'
@@ -47,13 +48,8 @@ const MSACanvasBlock = observer(function ({
47
48
  } = model
48
49
  const { theme, contrastScheme } = useColorContrast(colorScheme)
49
50
 
50
- const ref = useRef<HTMLCanvasElement>(null)
51
- useEffect(() => {
52
- const ctx = ref.current?.getContext('2d')
53
- if (!ctx) {
54
- return
55
- }
56
- return autorun(() => {
51
+ const ref = useCanvasAutorun(
52
+ ctx => {
57
53
  ctx.resetTransform()
58
54
  ctx.clearRect(
59
55
  0,
@@ -77,19 +73,20 @@ const MSACanvasBlock = observer(function ({
77
73
  contrastScheme,
78
74
  model,
79
75
  })
80
- })
81
- }, [
82
- model,
83
- offsetX,
84
- offsetY,
85
- theme,
86
- blockSize,
87
- highResScaleFactor,
88
- contrastScheme,
89
- ])
76
+ },
77
+ [
78
+ model,
79
+ offsetX,
80
+ offsetY,
81
+ theme,
82
+ blockSize,
83
+ highResScaleFactor,
84
+ contrastScheme,
85
+ ],
86
+ )
90
87
 
91
88
  const [mousePosition, setMousePosition] = useState<{ x: number; y: number }>()
92
- const { hoveredInsertion, mouseOverDomains } = model
89
+ const { hoveredInsertion, mouseOverDomains, showColumnStats } = model
93
90
 
94
91
  return (
95
92
  <>
@@ -117,12 +114,15 @@ const MSACanvasBlock = observer(function ({
117
114
  model.setMousePos(undefined, undefined)
118
115
  }
119
116
  // only track client coords when there's something to show in the
120
- // tooltip (insertion or a domain under the cursor); avoids a
121
- // re-render on every move over plain alignment cells
117
+ // tooltip: an insertion, a domain under the cursor, or column stats
118
+ // (when that option is enabled). avoids a re-render on every move
119
+ // when nothing would be shown
120
+ const hasTooltip =
121
+ model.hoveredInsertion ||
122
+ model.mouseOverDomains.length > 0 ||
123
+ (model.showColumnStats && !!model.mouseOverColumnStats)
122
124
  setMousePosition(
123
- model.hoveredInsertion || model.mouseOverDomains.length > 0
124
- ? { x: event.clientX, y: event.clientY }
125
- : undefined,
125
+ hasTooltip ? { x: event.clientX, y: event.clientY } : undefined,
126
126
  )
127
127
  }
128
128
  }}
@@ -168,7 +168,9 @@ const MSACanvasBlock = observer(function ({
168
168
  : hoveredInsertion.letters}
169
169
  </BaseTooltip>
170
170
  ) : null}
171
- {!hoveredInsertion && mouseOverDomains.length > 0 && mousePosition ? (
171
+ {!hoveredInsertion &&
172
+ mousePosition &&
173
+ (mouseOverDomains.length > 0 || showColumnStats) ? (
172
174
  <BaseTooltip
173
175
  clientPoint={{ x: mousePosition.x, y: mousePosition.y + 15 }}
174
176
  >
@@ -183,6 +185,7 @@ const MSACanvasBlock = observer(function ({
183
185
  ) : null}
184
186
  </div>
185
187
  ))}
188
+ {showColumnStats ? <ColumnStats model={model} /> : null}
186
189
  </BaseTooltip>
187
190
  ) : null}
188
191
  </>
@@ -1,10 +1,10 @@
1
- import React, { useEffect, useRef } from 'react'
1
+ import React from 'react'
2
2
 
3
3
  import { isAlive } from '@jbrowse/mobx-state-tree'
4
- import { autorun } from 'mobx'
5
4
  import { observer } from 'mobx-react'
6
5
 
7
6
  import { renderMouseover } from './renderMSAMouseover.ts'
7
+ import { useCanvasAutorun } from '../../useCanvasAutorun.ts'
8
8
 
9
9
  import type { MsaViewModel } from '../../model.ts'
10
10
 
@@ -13,20 +13,17 @@ const MSAMouseoverCanvas = observer(function ({
13
13
  }: {
14
14
  model: MsaViewModel
15
15
  }) {
16
- const ref = useRef<HTMLCanvasElement>(null)
17
16
  const { height, msaAreaWidth, verticalScrollbarWidth, highResScaleFactor } =
18
17
  model
19
18
  const width = msaAreaWidth - verticalScrollbarWidth
20
- useEffect(() => {
21
- const ctx = ref.current?.getContext('2d')
22
- return ctx
23
- ? autorun(() => {
24
- if (isAlive(model)) {
25
- renderMouseover({ ctx, model })
26
- }
27
- })
28
- : undefined
29
- }, [model])
19
+ const ref = useCanvasAutorun(
20
+ ctx => {
21
+ if (isAlive(model)) {
22
+ renderMouseover({ ctx, model })
23
+ }
24
+ },
25
+ [model],
26
+ )
30
27
 
31
28
  return (
32
29
  <canvas
@@ -88,7 +88,9 @@ function drawTiles({
88
88
  ctx.strokeRect(x, t, lw, h)
89
89
  // segment (exon) number drawn once per band, on the top visible row,
90
90
  // so it reads as a column header for the whole band
91
- const label = drawSegmentLabels ? segmentLabels.get(accession) : undefined
91
+ const label = drawSegmentLabels
92
+ ? segmentLabels.get(accession)
93
+ : undefined
92
94
  if (label !== undefined && i === 0) {
93
95
  const fontSize = Math.min(h - 2, 11)
94
96
  ctx.font = `${fontSize}px sans-serif`
@@ -1,3 +1,5 @@
1
+ import { multiRowHoverColor, referenceColor } from '../overlayColors.ts'
2
+
1
3
  import type { MsaViewModel } from '../../model.ts'
2
4
 
3
5
  const hoverColor = 'rgba(0,0,0,0.15)'
@@ -7,8 +9,6 @@ const highlightColor = 'rgba(128,128,0,0.2)'
7
9
  // faint hover-style wash alone is invisible against clustalx coloring)
8
10
  const highlightColumnsFill = 'rgba(255,140,0,0.28)'
9
11
  const highlightColumnsBorder = 'rgba(210,90,0,0.95)'
10
- const referenceColor = 'rgba(0,128,255,0.3)' // Blue highlight for reference row
11
- const multiRowHoverColor = 'rgba(255,165,0,0.15)' // Orange highlight for multi-row tree hover
12
12
 
13
13
  // Collapse sorted column indices into contiguous [start,end] runs so a run of
14
14
  // highlighted columns draws as one bordered band rather than per-column slices
@@ -0,0 +1,12 @@
1
+ // Shared highlight colors for the MSA/tree hover and selection overlay canvases.
2
+ // Kept in one place so the reference-row color (drawn identically over both the
3
+ // tree and the alignment) can't drift between the two surfaces.
4
+
5
+ // reference (relativeTo) row, drawn over both the tree and the alignment
6
+ export const referenceColor = 'rgba(0,128,255,0.3)'
7
+
8
+ // tree-node descendant rows highlighted on hover. The alpha differs by surface
9
+ // intentionally: the tree background is plain so it can take a stronger wash,
10
+ // while the alignment cells are already colored so a lighter wash reads better.
11
+ export const treeHoverColor = 'rgba(255,165,0,0.2)'
12
+ export const multiRowHoverColor = 'rgba(255,165,0,0.15)'
@@ -7,14 +7,16 @@ import type { RenderCtx } from '../renderCtx.ts'
7
7
 
8
8
  export function drawConservationBars({
9
9
  ctx,
10
- conservation,
10
+ values,
11
+ color,
11
12
  colWidth,
12
13
  trackHeight,
13
14
  offsetX,
14
15
  blockSize,
15
16
  }: {
16
17
  ctx: RenderCtx
17
- conservation: number[]
18
+ values: number[]
19
+ color: string
18
20
  colWidth: number
19
21
  trackHeight: number
20
22
  offsetX: number
@@ -26,15 +28,23 @@ export function drawConservationBars({
26
28
  colWidth,
27
29
  })
28
30
 
29
- ctx.fillStyle = 'gray'
30
- for (let i = xStart; i < xEnd && i < conservation.length; i++) {
31
- const value = conservation[i]!
31
+ ctx.fillStyle = color
32
+ for (let i = xStart; i < xEnd && i < values.length; i++) {
33
+ const value = values[i]!
32
34
  const barHeight = value * trackHeight
33
35
  const x = i * colWidth
34
36
  ctx.fillRect(x, trackHeight - barHeight, colWidth, barHeight)
35
37
  }
36
38
  }
37
39
 
40
+ // the bar-track values live on model getters (kept off the plain track object so
41
+ // the live canvas autorun stays reactive); select the right one by track id
42
+ export function barTrackValues(model: MsaViewModel, trackId: string) {
43
+ return trackId === 'property-conservation'
44
+ ? model.propertyConservation
45
+ : model.conservation
46
+ }
47
+
38
48
  export function drawTextTrackContent({
39
49
  ctx,
40
50
  data,
@@ -81,6 +91,7 @@ export function drawTextTrackContent({
81
91
  export function renderConservationTrack({
82
92
  model,
83
93
  ctx,
94
+ track,
84
95
  offsetX,
85
96
  offsetY,
86
97
  trackHeight,
@@ -89,13 +100,14 @@ export function renderConservationTrack({
89
100
  }: {
90
101
  model: MsaViewModel
91
102
  ctx: RenderCtx
103
+ track: BasicTrack
92
104
  offsetX: number
93
105
  offsetY: number
94
106
  trackHeight: number
95
107
  blockSizeXOverride?: number
96
108
  highResScaleFactorOverride?: number
97
109
  }) {
98
- const { blockSize, colWidth, highResScaleFactor, conservation } = model
110
+ const { blockSize, colWidth, highResScaleFactor } = model
99
111
  const bx = blockSizeXOverride ?? blockSize
100
112
  const k = highResScaleFactorOverride ?? highResScaleFactor
101
113
 
@@ -105,7 +117,8 @@ export function renderConservationTrack({
105
117
 
106
118
  drawConservationBars({
107
119
  ctx,
108
- conservation,
120
+ values: barTrackValues(model, track.model.id),
121
+ color: track.model.barColor ?? 'gray',
109
122
  colWidth,
110
123
  trackHeight,
111
124
  offsetX,
@@ -191,10 +204,11 @@ export function renderAllTracks({
191
204
  for (const track of turnedOnTracks) {
192
205
  const trackHeight = track.model.height
193
206
 
194
- if (track.model.id === 'conservation') {
207
+ if (track.model.barColor !== undefined) {
195
208
  renderConservationTrack({
196
209
  model,
197
210
  ctx,
211
+ track,
198
212
  offsetX,
199
213
  offsetY: currentY,
200
214
  trackHeight,
@@ -1,21 +1,18 @@
1
- import React, { useCallback, useEffect, useRef } from 'react'
1
+ import React, { useCallback, useRef } from 'react'
2
2
 
3
3
  import { isAlive } from '@jbrowse/mobx-state-tree'
4
- import { autorun } from 'mobx'
5
4
  import { observer } from 'mobx-react'
6
5
 
7
6
  import TreeCanvasBlock from './TreeCanvasBlock.tsx'
8
7
  import { padding } from './renderTreeCanvas.ts'
8
+ import { useCanvasAutorun } from '../../useCanvasAutorun.ts'
9
9
  import { useWheelScroll } from '../../useWheelScroll.ts'
10
+ import { referenceColor, treeHoverColor } from '../overlayColors.ts'
10
11
 
11
12
  import type { MsaViewModel } from '../../model.ts'
12
13
 
13
- const referenceColor = 'rgba(0,128,255,0.3)'
14
- const treeHoverColor = 'rgba(255,165,0,0.2)'
15
-
16
14
  const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
17
15
  const ref = useRef<HTMLDivElement>(null)
18
- const mouseoverRef = useRef<HTMLCanvasElement>(null)
19
16
  const { treeWidth, height, blocksY, treeAreaWidth } = model
20
17
  const onScrollY = useCallback(
21
18
  (d: number) => {
@@ -25,58 +22,54 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
25
22
  )
26
23
  const { onMouseDown, onMouseUp } = useWheelScroll({ ref, onScrollY })
27
24
 
28
- useEffect(() => {
29
- const ctx = mouseoverRef.current?.getContext('2d')
30
- return ctx
31
- ? autorun(() => {
32
- if (isAlive(model)) {
33
- const {
34
- relativeTo,
35
- leaves,
36
- rowHeight,
37
- hoveredTreeNode,
38
- treeAreaWidth: w,
39
- height: h,
40
- scrollY: sy,
41
- mouseOverRowName,
42
- } = model
43
- ctx.resetTransform()
44
- ctx.clearRect(0, 0, w, h)
45
-
46
- const leafByName = new Map(
47
- leaves.map(leaf => [leaf.data.name, leaf]),
48
- )
49
- const fillRow = (name: string) => {
50
- const leaf = leafByName.get(name)
51
- if (leaf) {
52
- ctx.fillRect(0, leaf.x! + sy - rowHeight / 2, w, rowHeight)
53
- }
54
- }
25
+ const mouseoverRef = useCanvasAutorun(
26
+ ctx => {
27
+ if (isAlive(model)) {
28
+ const {
29
+ relativeTo,
30
+ leaves,
31
+ rowHeight,
32
+ hoveredTreeNode,
33
+ treeAreaWidth: w,
34
+ height: h,
35
+ scrollY: sy,
36
+ mouseOverRowName,
37
+ } = model
38
+ ctx.resetTransform()
39
+ ctx.clearRect(0, 0, w, h)
55
40
 
56
- if (relativeTo) {
57
- ctx.fillStyle = referenceColor
58
- fillRow(relativeTo)
59
- }
41
+ const leafByName = new Map(leaves.map(leaf => [leaf.data.name, leaf]))
42
+ const fillRow = (name: string) => {
43
+ const leaf = leafByName.get(name)
44
+ if (leaf) {
45
+ ctx.fillRect(0, leaf.x! + sy - rowHeight / 2, w, rowHeight)
46
+ }
47
+ }
60
48
 
61
- if (hoveredTreeNode) {
62
- ctx.fillStyle = treeHoverColor
63
- for (const descendantName of hoveredTreeNode.descendantNames) {
64
- fillRow(descendantName)
65
- }
66
- }
49
+ if (relativeTo) {
50
+ ctx.fillStyle = referenceColor
51
+ fillRow(relativeTo)
52
+ }
67
53
 
68
- if (
69
- mouseOverRowName &&
70
- mouseOverRowName !== relativeTo &&
71
- !hoveredTreeNode?.descendantNames.includes(mouseOverRowName)
72
- ) {
73
- ctx.fillStyle = treeHoverColor
74
- fillRow(mouseOverRowName)
75
- }
54
+ if (hoveredTreeNode) {
55
+ ctx.fillStyle = treeHoverColor
56
+ for (const descendantName of hoveredTreeNode.descendantNames) {
57
+ fillRow(descendantName)
76
58
  }
77
- })
78
- : undefined
79
- }, [model])
59
+ }
60
+
61
+ if (
62
+ mouseOverRowName &&
63
+ mouseOverRowName !== relativeTo &&
64
+ !hoveredTreeNode?.descendantNames.includes(mouseOverRowName)
65
+ ) {
66
+ ctx.fillStyle = treeHoverColor
67
+ fillRow(mouseOverRowName)
68
+ }
69
+ }
70
+ },
71
+ [model],
72
+ )
80
73
 
81
74
  return (
82
75
  <div
@@ -1,15 +1,16 @@
1
1
  import React, { useEffect, useRef, useState } from 'react'
2
2
 
3
+ import { makeStyles } from '@jbrowse/core/util/tss-react'
3
4
  import { useTheme } from '@mui/material'
4
- import Flatbush from 'flatbush'
5
- import { autorun } from 'mobx'
6
5
  import { observer } from 'mobx-react'
7
- import { makeStyles } from 'tss-react/mui'
8
6
 
9
7
  import TreeBranchMenu from './TreeBranchMenu.tsx'
10
8
  import TreeNodeMenu from './TreeNodeMenu.tsx'
9
+ import { ClickMapIndex } from './clickMap.ts'
11
10
  import { padding, renderTreeCanvas } from './renderTreeCanvas.ts'
11
+ import { useCanvasAutorun } from '../../useCanvasAutorun.ts'
12
12
 
13
+ import type { ClickEntry } from './clickMap.ts'
13
14
  import type { MsaViewModel } from '../../model.ts'
14
15
 
15
16
  const useStyles = makeStyles()(theme => ({
@@ -36,55 +37,6 @@ interface TooltipData {
36
37
  y: number
37
38
  }
38
39
 
39
- interface ClickEntry {
40
- name: string
41
- id: string
42
- branch?: boolean
43
- minX: number
44
- maxX: number
45
- minY: number
46
- maxY: number
47
- }
48
-
49
- class ClickMapIndex {
50
- private flatbush: Flatbush | null = null
51
- private entries: ClickEntry[] = []
52
-
53
- clear() {
54
- this.flatbush = null
55
- this.entries = []
56
- }
57
-
58
- insert(entry: ClickEntry) {
59
- this.entries.push(entry)
60
- }
61
-
62
- finish() {
63
- if (this.entries.length === 0) {
64
- this.flatbush = null
65
- } else {
66
- this.flatbush = new Flatbush(this.entries.length)
67
- for (const entry of this.entries) {
68
- this.flatbush.add(entry.minX, entry.minY, entry.maxX, entry.maxY)
69
- }
70
- this.flatbush.finish()
71
- }
72
- }
73
-
74
- search(box: {
75
- minX: number
76
- maxX: number
77
- minY: number
78
- maxY: number
79
- }): ClickEntry[] {
80
- return (
81
- this.flatbush
82
- ?.search(box.minX, box.minY, box.maxX, box.maxY)
83
- .map(i => this.entries[i]!) ?? []
84
- )
85
- }
86
- }
87
-
88
40
  const TreeCanvasBlock = observer(function ({
89
41
  model,
90
42
  offsetY,
@@ -94,7 +46,6 @@ const TreeCanvasBlock = observer(function ({
94
46
  }) {
95
47
  const { classes } = useStyles()
96
48
  const theme = useTheme()
97
- const ref = useRef<HTMLCanvasElement>(null)
98
49
  const clickMap = useRef(new ClickMapIndex())
99
50
  const mouseoverRef = useRef<HTMLCanvasElement>(null)
100
51
  const [branchMenu, setBranchMenu] = useState<TooltipData>()
@@ -113,20 +64,10 @@ const TreeCanvasBlock = observer(function ({
113
64
  const w2 = width * highResScaleFactor
114
65
  const h2 = height * highResScaleFactor
115
66
 
116
- useEffect(() => {
117
- const ctx = ref.current?.getContext('2d')
118
- if (!ctx) {
119
- return
120
- }
121
-
122
- return autorun(() => {
67
+ const ref = useCanvasAutorun(
68
+ ctx => {
123
69
  ctx.resetTransform()
124
- ctx.clearRect(
125
- 0,
126
- 0,
127
- (treeAreaWidth + padding) * highResScaleFactor,
128
- blockSize * highResScaleFactor,
129
- )
70
+ ctx.clearRect(0, 0, w2, h2)
130
71
  renderTreeCanvas({
131
72
  ctx,
132
73
  model,
@@ -134,8 +75,9 @@ const TreeCanvasBlock = observer(function ({
134
75
  clickMap: clickMap.current,
135
76
  theme,
136
77
  })
137
- })
138
- }, [model, blockSize, highResScaleFactor, treeAreaWidth, offsetY, theme])
78
+ },
79
+ [model, blockSize, highResScaleFactor, treeAreaWidth, offsetY, theme],
80
+ )
139
81
 
140
82
  useEffect(() => {
141
83
  const ctx = mouseoverRef.current?.getContext('2d')
@@ -0,0 +1,55 @@
1
+ import Flatbush from 'flatbush'
2
+
3
+ export interface ClickEntry {
4
+ name: string
5
+ id: string
6
+ branch?: boolean
7
+ minX: number
8
+ maxX: number
9
+ minY: number
10
+ maxY: number
11
+ }
12
+
13
+ interface Box {
14
+ minX: number
15
+ maxX: number
16
+ minY: number
17
+ maxY: number
18
+ }
19
+
20
+ // Spatial index of clickable tree elements (leaf labels, node bubbles,
21
+ // collapsed-clade triangles). Entries are collected during a render pass, then
22
+ // finish() builds the Flatbush index queried by hit-testing.
23
+ export class ClickMapIndex {
24
+ private flatbush: Flatbush | null = null
25
+ private entries: ClickEntry[] = []
26
+
27
+ clear() {
28
+ this.flatbush = null
29
+ this.entries = []
30
+ }
31
+
32
+ insert(entry: ClickEntry) {
33
+ this.entries.push(entry)
34
+ }
35
+
36
+ finish() {
37
+ if (this.entries.length === 0) {
38
+ this.flatbush = null
39
+ } else {
40
+ this.flatbush = new Flatbush(this.entries.length)
41
+ for (const entry of this.entries) {
42
+ this.flatbush.add(entry.minX, entry.minY, entry.maxX, entry.maxY)
43
+ }
44
+ this.flatbush.finish()
45
+ }
46
+ }
47
+
48
+ search(box: Box): ClickEntry[] {
49
+ return (
50
+ this.flatbush
51
+ ?.search(box.minX, box.minY, box.maxX, box.maxY)
52
+ .map(i => this.entries[i]!) ?? []
53
+ )
54
+ }
55
+ }
@@ -5,6 +5,7 @@ import {
5
5
  } from '../../hierarchy.ts'
6
6
  import { setFontSize } from '../../setFontSize.ts'
7
7
 
8
+ import type { ClickMapIndex } from './clickMap.ts'
8
9
  import type { HierarchyNode } from '../../hierarchy.ts'
9
10
  import type { MsaViewModel } from '../../model.ts'
10
11
  import type { RenderCtx } from '../renderCtx.ts'
@@ -16,6 +17,12 @@ const extendBounds = 5
16
17
  const radius = 2.5
17
18
  const d = radius * 2
18
19
 
20
+ // whether a node's row-center y falls within the block being drawn, padded by
21
+ // extendBounds so bubbles/labels/triangles straddling the edge still render
22
+ function inYBlock(y: number, offsetY: number, by: number) {
23
+ return y > offsetY - extendBounds && y < offsetY + by + extendBounds
24
+ }
25
+
19
26
  // Calculate node x-coordinate for both phylogram (with branch lengths) and cladogram (topology only) modes
20
27
  // For cladograms: x = (maxDepthToLeaf - nodeDepthToLeaf) / maxDepthToLeaf * maxWidth
21
28
  // This positions: leaves at maxWidth (rightmost), root at 0 (leftmost), internal nodes proportionally in between
@@ -36,28 +43,6 @@ function getNodeX(
36
43
  return ((maxDepthToLeaf - depthToLeaf) / maxDepthToLeaf) * maxBranchLen
37
44
  }
38
45
 
39
- interface ClickEntry {
40
- name: string
41
- id: string
42
- branch?: boolean
43
- minX: number
44
- maxX: number
45
- minY: number
46
- maxY: number
47
- }
48
-
49
- interface ClickMapIndex {
50
- clear(): void
51
- insert(entry: ClickEntry): void
52
- finish(): void
53
- search(box: {
54
- minX: number
55
- maxX: number
56
- minY: number
57
- maxY: number
58
- }): ClickEntry[]
59
- }
60
-
61
46
  export function renderTree({
62
47
  offsetY,
63
48
  ctx,
@@ -137,8 +122,7 @@ export function renderCollapsedTriangles({
137
122
  if (collapsed.includes(id)) {
138
123
  const apexX = getNodeX(node, showBranchLen, maxBranchLen, maxDepthToLeaf)
139
124
  const y = node.x!
140
- const inBlock =
141
- y > offsetY - extendBounds && y < offsetY + by + extendBounds
125
+ const inBlock = inYBlock(y, offsetY, by)
142
126
  // in cladogram mode the hidden tips align at the right edge, otherwise use
143
127
  // the branch-length extent recorded in the model's hierarchy getter
144
128
  const baseX = showBranchLen
@@ -214,11 +198,7 @@ export function renderNodeBubbles({
214
198
  const { data } = node
215
199
  const y = node.x!
216
200
  const { id, name } = data
217
- if (
218
- node.height >= 1 &&
219
- y > offsetY - extendBounds &&
220
- y < offsetY + by + extendBounds
221
- ) {
201
+ if (node.height >= 1 && inYBlock(y, offsetY, by)) {
222
202
  const isCollapsed = collapsed.includes(id)
223
203
  ctx.strokeStyle = 'black'
224
204
  ctx.fillStyle = isCollapsed ? 'black' : 'white'
@@ -296,11 +276,7 @@ export function renderTreeLabels({
296
276
  // a collapsed clade is drawn as a triangle + tip count; suppress its leaf
297
277
  // label when the "name" is just the auto-generated internal-node id
298
278
  const isAnonymousCollapsed = collapsed.includes(id) && name === id
299
- if (
300
- !isAnonymousCollapsed &&
301
- y > offsetY - extendBounds &&
302
- y < offsetY + by + extendBounds
303
- ) {
279
+ if (!isAnonymousCollapsed && inYBlock(y, offsetY, by)) {
304
280
  // note: +rowHeight/4 matches with -rowHeight/4 in msa
305
281
  const yp = y + fontSize / 4
306
282
  let xp = 0
package/src/constants.ts CHANGED
@@ -40,6 +40,7 @@ export const maxCellSize = 80
40
40
  // MSA model defaults
41
41
  export const defaultBgColor = true
42
42
  export const defaultColorSchemeName = 'maeditor'
43
+ export const defaultShowColumnStats = true
43
44
 
44
45
  // Tree model defaults
45
46
  export const defaultDrawLabels = true