react-msaview 4.4.4 → 4.4.5

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 (181) hide show
  1. package/bundle/index.js +111 -111
  2. package/bundle/index.js.LICENSE.txt +24 -6
  3. package/bundle/index.js.map +1 -1
  4. package/dist/components/dialogs/InterProScanDialog.js +1 -1
  5. package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
  6. package/dist/components/dialogs/SettingsDialog.js +1 -1
  7. package/dist/components/dialogs/SettingsDialog.js.map +1 -1
  8. package/dist/components/msa/MSACanvasBlock.js +8 -1
  9. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  10. package/dist/components/msa/renderMSABlock.js +22 -4
  11. package/dist/components/msa/renderMSABlock.js.map +1 -1
  12. package/dist/components/msa/renderMSAMouseover.js +21 -1
  13. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  14. package/dist/components/tree/TreeCanvas.js +61 -2
  15. package/dist/components/tree/TreeCanvas.js.map +1 -1
  16. package/dist/components/tree/TreeCanvasBlock.js +56 -5
  17. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  18. package/dist/components/tree/TreeNodeMenu.js +6 -2
  19. package/dist/components/tree/TreeNodeMenu.js.map +1 -1
  20. package/dist/components/tree/renderTreeCanvas.d.ts +14 -4
  21. package/dist/components/tree/renderTreeCanvas.js +3 -0
  22. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  23. package/dist/components/util.js +1 -1
  24. package/dist/components/util.js.map +1 -1
  25. package/dist/layout.d.ts +4 -1
  26. package/dist/layout.js +30 -8
  27. package/dist/layout.js.map +1 -1
  28. package/dist/model/treeModel.js +2 -2
  29. package/dist/model/treeModel.js.map +1 -1
  30. package/dist/model.d.ts +14 -79
  31. package/dist/model.js +94 -4
  32. package/dist/model.js.map +1 -1
  33. package/dist/version.d.ts +1 -1
  34. package/dist/version.js +1 -1
  35. package/package.json +3 -2
  36. package/src/components/dialogs/InterProScanDialog.tsx +1 -1
  37. package/src/components/dialogs/SettingsDialog.tsx +1 -1
  38. package/src/components/msa/MSACanvasBlock.tsx +8 -1
  39. package/src/components/msa/renderMSABlock.ts +32 -2
  40. package/src/components/msa/renderMSAMouseover.ts +26 -0
  41. package/src/components/tree/TreeCanvas.tsx +76 -1
  42. package/src/components/tree/TreeCanvasBlock.tsx +69 -5
  43. package/src/components/tree/TreeNodeMenu.tsx +12 -1
  44. package/src/components/tree/renderTreeCanvas.ts +20 -4
  45. package/src/components/util.ts +1 -1
  46. package/src/layout.ts +48 -8
  47. package/src/model/treeModel.ts +2 -2
  48. package/src/model.ts +108 -5
  49. package/src/version.ts +1 -1
  50. package/dist/DataModel.d.ts +0 -34
  51. package/dist/DataModel.js +0 -46
  52. package/dist/DataModel.js.map +0 -1
  53. package/dist/DialogQueue.d.ts +0 -25
  54. package/dist/DialogQueue.js +0 -44
  55. package/dist/DialogQueue.js.map +0 -1
  56. package/dist/SelectedStructuresMixin.d.ts +0 -46
  57. package/dist/SelectedStructuresMixin.js +0 -52
  58. package/dist/SelectedStructuresMixin.js.map +0 -1
  59. package/dist/StructureModel.d.ts +0 -9
  60. package/dist/StructureModel.js +0 -11
  61. package/dist/StructureModel.js.map +0 -1
  62. package/dist/UniprotTrack.d.ts +0 -27
  63. package/dist/UniprotTrack.js +0 -53
  64. package/dist/UniprotTrack.js.map +0 -1
  65. package/dist/components/BoxTrack.d.ts +0 -7
  66. package/dist/components/BoxTrack.js +0 -15
  67. package/dist/components/BoxTrack.js.map +0 -1
  68. package/dist/components/BoxTrackBlock.d.ts +0 -8
  69. package/dist/components/BoxTrackBlock.js +0 -136
  70. package/dist/components/BoxTrackBlock.js.map +0 -1
  71. package/dist/components/ExportSVGDialog.d.ts +0 -6
  72. package/dist/components/ExportSVGDialog.js +0 -39
  73. package/dist/components/ExportSVGDialog.js.map +0 -1
  74. package/dist/components/Header.d.ts +0 -6
  75. package/dist/components/Header.js +0 -62
  76. package/dist/components/Header.js.map +0 -1
  77. package/dist/components/HeaderInfoArea.d.ts +0 -6
  78. package/dist/components/HeaderInfoArea.js +0 -12
  79. package/dist/components/HeaderInfoArea.js.map +0 -1
  80. package/dist/components/ImportForm/ImportFormExamples.d.ts +0 -6
  81. package/dist/components/ImportForm/ImportFormExamples.js +0 -50
  82. package/dist/components/ImportForm/ImportFormExamples.js.map +0 -1
  83. package/dist/components/ImportForm/data/seq2.d.ts +0 -3
  84. package/dist/components/ImportForm/data/seq2.js +0 -33
  85. package/dist/components/ImportForm/data/seq2.js.map +0 -1
  86. package/dist/components/ImportForm/index.d.ts +0 -6
  87. package/dist/components/ImportForm/index.js +0 -31
  88. package/dist/components/ImportForm/index.js.map +0 -1
  89. package/dist/components/ImportForm/util.d.ts +0 -3
  90. package/dist/components/ImportForm/util.js +0 -16
  91. package/dist/components/ImportForm/util.js.map +0 -1
  92. package/dist/components/MSACanvas.d.ts +0 -6
  93. package/dist/components/MSACanvas.js +0 -141
  94. package/dist/components/MSACanvas.js.map +0 -1
  95. package/dist/components/MSAPanel/Loading.d.ts +0 -2
  96. package/dist/components/MSAPanel/Loading.js +0 -12
  97. package/dist/components/MSAPanel/Loading.js.map +0 -1
  98. package/dist/components/MSAPanel/MSABlock.d.ts +0 -8
  99. package/dist/components/MSAPanel/MSABlock.js +0 -62
  100. package/dist/components/MSAPanel/MSABlock.js.map +0 -1
  101. package/dist/components/MSAPanel/MSACanvas.d.ts +0 -7
  102. package/dist/components/MSAPanel/MSACanvas.js +0 -69
  103. package/dist/components/MSAPanel/MSACanvas.js.map +0 -1
  104. package/dist/components/MSAPanel/MSAMouseoverCanvas.d.ts +0 -6
  105. package/dist/components/MSAPanel/MSAMouseoverCanvas.js +0 -27
  106. package/dist/components/MSAPanel/MSAMouseoverCanvas.js.map +0 -1
  107. package/dist/components/MSAPanel/index.d.ts +0 -5
  108. package/dist/components/MSAPanel/index.js +0 -9
  109. package/dist/components/MSAPanel/index.js.map +0 -1
  110. package/dist/components/MSAPanel/renderMSABlock.d.ts +0 -9
  111. package/dist/components/MSAPanel/renderMSABlock.js +0 -89
  112. package/dist/components/MSAPanel/renderMSABlock.js.map +0 -1
  113. package/dist/components/MSAPanel/renderMSABlock_BACKUP_139826.d.ts +0 -9
  114. package/dist/components/MSAPanel/renderMSABlock_BACKUP_139826.js +0 -89
  115. package/dist/components/MSAPanel/renderMSABlock_BACKUP_139826.js.map +0 -1
  116. package/dist/components/MSAPanel/renderMSABlock_BASE_139826.d.ts +0 -13
  117. package/dist/components/MSAPanel/renderMSABlock_BASE_139826.js +0 -82
  118. package/dist/components/MSAPanel/renderMSABlock_BASE_139826.js.map +0 -1
  119. package/dist/components/MSAPanel/renderMSABlock_LOCAL_139826.d.ts +0 -9
  120. package/dist/components/MSAPanel/renderMSABlock_LOCAL_139826.js +0 -89
  121. package/dist/components/MSAPanel/renderMSABlock_LOCAL_139826.js.map +0 -1
  122. package/dist/components/MSAPanel/renderMSABlock_REMOTE_139826.d.ts +0 -0
  123. package/dist/components/MSAPanel/renderMSABlock_REMOTE_139826.js +0 -2
  124. package/dist/components/MSAPanel/renderMSABlock_REMOTE_139826.js.map +0 -1
  125. package/dist/components/MSAPanel/renderMSAMouseover.d.ts +0 -5
  126. package/dist/components/MSAPanel/renderMSAMouseover.js +0 -30
  127. package/dist/components/MSAPanel/renderMSAMouseover.js.map +0 -1
  128. package/dist/components/Minimap.d.ts +0 -6
  129. package/dist/components/Minimap.js +0 -72
  130. package/dist/components/Minimap.js.map +0 -1
  131. package/dist/components/MinimapSVG.d.ts +0 -6
  132. package/dist/components/MinimapSVG.js +0 -25
  133. package/dist/components/MinimapSVG.js.map +0 -1
  134. package/dist/components/MultiAlignmentSelector.d.ts +0 -6
  135. package/dist/components/MultiAlignmentSelector.js +0 -13
  136. package/dist/components/MultiAlignmentSelector.js.map +0 -1
  137. package/dist/components/TreePanel/TreeBranchMenu.d.ts +0 -14
  138. package/dist/components/TreePanel/TreeBranchMenu.js +0 -26
  139. package/dist/components/TreePanel/TreeBranchMenu.js.map +0 -1
  140. package/dist/components/TreePanel/TreeCanvas.d.ts +0 -6
  141. package/dist/components/TreePanel/TreeCanvas.js +0 -100
  142. package/dist/components/TreePanel/TreeCanvas.js.map +0 -1
  143. package/dist/components/TreePanel/TreeCanvasBlock.d.ts +0 -7
  144. package/dist/components/TreePanel/TreeCanvasBlock.js +0 -118
  145. package/dist/components/TreePanel/TreeCanvasBlock.js.map +0 -1
  146. package/dist/components/TreePanel/TreeNodeMenu.d.ts +0 -13
  147. package/dist/components/TreePanel/TreeNodeMenu.js +0 -74
  148. package/dist/components/TreePanel/TreeNodeMenu.js.map +0 -1
  149. package/dist/components/TreePanel/TreeRuler.d.ts +0 -6
  150. package/dist/components/TreePanel/TreeRuler.js +0 -8
  151. package/dist/components/TreePanel/TreeRuler.js.map +0 -1
  152. package/dist/components/TreePanel/dialogs/TreeNodeInfoDialog.d.ts +0 -9
  153. package/dist/components/TreePanel/dialogs/TreeNodeInfoDialog.js +0 -16
  154. package/dist/components/TreePanel/dialogs/TreeNodeInfoDialog.js.map +0 -1
  155. package/dist/components/TreePanel/index.d.ts +0 -6
  156. package/dist/components/TreePanel/index.js +0 -10
  157. package/dist/components/TreePanel/index.js.map +0 -1
  158. package/dist/components/TreePanel/renderTreeCanvas.d.ts +0 -46
  159. package/dist/components/TreePanel/renderTreeCanvas.js +0 -180
  160. package/dist/components/TreePanel/renderTreeCanvas.js.map +0 -1
  161. package/dist/components/VerticalGuide.d.ts +0 -7
  162. package/dist/components/VerticalGuide.js +0 -30
  163. package/dist/components/VerticalGuide.js.map +0 -1
  164. package/dist/components/ZoomControls.d.ts +0 -6
  165. package/dist/components/ZoomControls.js +0 -59
  166. package/dist/components/ZoomControls.js.map +0 -1
  167. package/dist/components/msa/MSACanvas_BACKUP_139826.d.ts +0 -7
  168. package/dist/components/msa/MSACanvas_BACKUP_139826.js +0 -68
  169. package/dist/components/msa/MSACanvas_BACKUP_139826.js.map +0 -1
  170. package/dist/components/msa/MSACanvas_BASE_139826.d.ts +0 -6
  171. package/dist/components/msa/MSACanvas_BASE_139826.js +0 -107
  172. package/dist/components/msa/MSACanvas_BASE_139826.js.map +0 -1
  173. package/dist/components/msa/MSACanvas_LOCAL_139826.d.ts +0 -7
  174. package/dist/components/msa/MSACanvas_LOCAL_139826.js +0 -69
  175. package/dist/components/msa/MSACanvas_LOCAL_139826.js.map +0 -1
  176. package/dist/components/msa/MSACanvas_REMOTE_139826.d.ts +0 -6
  177. package/dist/components/msa/MSACanvas_REMOTE_139826.js +0 -106
  178. package/dist/components/msa/MSACanvas_REMOTE_139826.js.map +0 -1
  179. package/dist/sansserif.d.ts +0 -3
  180. package/dist/sansserif.js +0 -1583
  181. package/dist/sansserif.js.map +0 -1
@@ -101,8 +101,14 @@ function drawTiles({
101
101
  columns,
102
102
  colWidth,
103
103
  rowHeight,
104
+ relativeTo,
104
105
  } = model
105
106
 
107
+ // Get reference sequence if relativeTo is set
108
+ const referenceSeq = relativeTo
109
+ ? columns[relativeTo]?.slice(xStart, xEnd)
110
+ : null
111
+
106
112
  for (let i = 0, l1 = visibleLeaves.length; i < l1; i++) {
107
113
  const node = visibleLeaves[i]!
108
114
  const {
@@ -113,6 +119,11 @@ function drawTiles({
113
119
  if (str) {
114
120
  for (let i = 0, l2 = str.length; i < l2; i++) {
115
121
  const letter = str[i]!
122
+
123
+ // Use a muted background for positions that match reference
124
+ const isMatchingReference =
125
+ referenceSeq && name !== relativeTo && letter === referenceSeq[i]
126
+
116
127
  const r1 = colorSchemeName === 'clustalx_protein_dynamic'
117
128
  const r2 = colorSchemeName === 'percent_identity_dynamic'
118
129
  const color = r1
@@ -137,7 +148,11 @@ function drawTiles({
137
148
  )
138
149
  : colorScheme[letter.toUpperCase()]
139
150
  if (bgColor || r1 || r2) {
140
- ctx.fillStyle = color || theme.palette.background.default
151
+ // Use a very light background for matching positions in relative mode
152
+ const finalColor = isMatchingReference
153
+ ? theme.palette.action.hover
154
+ : color || theme.palette.background.default
155
+ ctx.fillStyle = finalColor
141
156
  ctx.fillRect(
142
157
  i * colWidth + offsetX - (offsetX % colWidth),
143
158
  y - rowHeight,
@@ -177,7 +192,14 @@ function drawText({
177
192
  colWidth,
178
193
  contrastLettering,
179
194
  rowHeight,
195
+ relativeTo,
180
196
  } = model
197
+
198
+ // Get reference sequence if relativeTo is set
199
+ const referenceSeq = relativeTo
200
+ ? columns[relativeTo]?.slice(xStart, xEnd)
201
+ : null
202
+
181
203
  if (showMsaLetters) {
182
204
  for (let i = 0, l1 = visibleLeaves.length; i < l1; i++) {
183
205
  const node = visibleLeaves[i]!
@@ -189,6 +211,14 @@ function drawText({
189
211
  if (str) {
190
212
  for (let i = 0, l2 = str.length; i < l2; i++) {
191
213
  const letter = str[i]!
214
+
215
+ // Check if this position matches the reference
216
+ const isMatchingReference =
217
+ referenceSeq && name !== relativeTo && letter === referenceSeq[i]
218
+
219
+ // Show dot for matching positions, original letter for differences
220
+ const displayLetter = isMatchingReference ? '.' : letter
221
+
192
222
  const color = colorScheme[letter.toUpperCase()]
193
223
  const contrast = contrastLettering
194
224
  ? contrastScheme[letter.toUpperCase()] || 'black'
@@ -201,7 +231,7 @@ function drawText({
201
231
  : bgColor
202
232
  ? contrast
203
233
  : color || 'black'
204
- ctx.fillText(letter, x + colWidth / 2, y - rowHeight / 4)
234
+ ctx.fillText(displayLetter, x + colWidth / 2, y - rowHeight / 4)
205
235
  }
206
236
  }
207
237
  }
@@ -2,6 +2,8 @@ import type { MsaViewModel } from '../../model'
2
2
 
3
3
  const hoverColor = 'rgba(0,0,0,0.15)'
4
4
  const highlightColor = 'rgba(128,128,0,0.2)'
5
+ const referenceColor = 'rgba(0,128,255,0.3)' // Blue highlight for reference row
6
+ const multiRowHoverColor = 'rgba(255,165,0,0.15)' // Orange highlight for multi-row tree hover
5
7
 
6
8
  export function renderMouseover({
7
9
  ctx,
@@ -23,9 +25,33 @@ export function renderMouseover({
23
25
  mouseCol2,
24
26
  mouseClickRow,
25
27
  mouseClickCol,
28
+ relativeTo,
29
+ rowNamesSet,
30
+ hoveredTreeNode,
26
31
  } = model
27
32
  ctx.resetTransform()
28
33
  ctx.clearRect(0, 0, width, height)
34
+
35
+ // Highlight reference row (relativeTo) persistently
36
+ if (relativeTo) {
37
+ const referenceRowIndex = rowNamesSet.get(relativeTo)
38
+ if (referenceRowIndex !== undefined) {
39
+ ctx.fillStyle = referenceColor
40
+ ctx.fillRect(0, referenceRowIndex * rowHeight + scrollY, width, rowHeight)
41
+ }
42
+ }
43
+
44
+ // Highlight multiple rows when hovering over tree nodes with children
45
+ if (hoveredTreeNode) {
46
+ ctx.fillStyle = multiRowHoverColor
47
+ for (const descendantName of hoveredTreeNode.descendantNames) {
48
+ const rowIndex = rowNamesSet.get(descendantName)
49
+ if (rowIndex !== undefined) {
50
+ ctx.fillRect(0, rowIndex * rowHeight + scrollY, width, rowHeight)
51
+ }
52
+ }
53
+ }
54
+
29
55
  if (mouseCol !== undefined) {
30
56
  ctx.fillStyle = hoverColor
31
57
  ctx.fillRect(mouseCol * colWidth + scrollX, 0, colWidth, height)
@@ -1,6 +1,8 @@
1
1
  import React, { useEffect, useRef, useState } from 'react'
2
2
 
3
+ import { autorun } from 'mobx'
3
4
  import { observer } from 'mobx-react'
5
+ import { isAlive } from 'mobx-state-tree'
4
6
 
5
7
  import TreeCanvasBlock from './TreeCanvasBlock'
6
8
  import { padding } from './renderTreeCanvas'
@@ -9,10 +11,11 @@ import type { MsaViewModel } from '../../model'
9
11
 
10
12
  const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
11
13
  const ref = useRef<HTMLDivElement>(null)
14
+ const mouseoverRef = useRef<HTMLCanvasElement>(null)
12
15
  const scheduled = useRef(false)
13
16
  const deltaY = useRef(0)
14
17
  const prevY = useRef<number>(0)
15
- const { treeWidth, height, blocksY } = model
18
+ const { treeWidth, height, blocksY, treeAreaWidth, scrollY } = model
16
19
  const [mouseDragging, setMouseDragging] = useState(false)
17
20
 
18
21
  useEffect(() => {
@@ -80,6 +83,64 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
80
83
  return cleanup
81
84
  }, [model, mouseDragging])
82
85
 
86
+ // Global tree mouseover effect
87
+ useEffect(() => {
88
+ const ctx = mouseoverRef.current?.getContext('2d')
89
+ return ctx
90
+ ? autorun(() => {
91
+ if (isAlive(model)) {
92
+ ctx.resetTransform()
93
+ ctx.clearRect(0, 0, treeAreaWidth, height)
94
+
95
+ // Highlight reference row (relativeTo) persistently
96
+ const { relativeTo, leaves, rowHeight, hoveredTreeNode } = model
97
+ if (relativeTo) {
98
+ const referenceLeaf = leaves.find(
99
+ leaf => leaf.data.name === relativeTo,
100
+ )
101
+ if (referenceLeaf) {
102
+ const y = referenceLeaf.x! + scrollY
103
+ ctx.fillStyle = 'rgba(0,128,255,0.3)' // Blue highlight for reference row
104
+ ctx.fillRect(0, y - rowHeight / 2, treeAreaWidth, rowHeight)
105
+ }
106
+ }
107
+
108
+ // Highlight multiple rows when hovering over tree nodes
109
+ if (hoveredTreeNode) {
110
+ ctx.fillStyle = 'rgba(255,165,0,0.2)' // Orange highlight for tree hover
111
+ for (const descendantName of hoveredTreeNode.descendantNames) {
112
+ const matchingLeaf = leaves.find(
113
+ leaf => leaf.data.name === descendantName,
114
+ )
115
+ if (matchingLeaf) {
116
+ const y = matchingLeaf.x! + scrollY
117
+ ctx.fillRect(0, y - rowHeight / 2, treeAreaWidth, rowHeight)
118
+ }
119
+ }
120
+ }
121
+
122
+ // Highlight single tree row corresponding to MSA mouseover (if not part of multi-row hover)
123
+ const { mouseOverRowName } = model
124
+ if (
125
+ mouseOverRowName &&
126
+ mouseOverRowName !== relativeTo &&
127
+ !hoveredTreeNode?.descendantNames.includes(mouseOverRowName)
128
+ ) {
129
+ // Find the leaf node that matches the hovered row
130
+ const matchingLeaf = leaves.find(
131
+ leaf => leaf.data.name === mouseOverRowName,
132
+ )
133
+ if (matchingLeaf) {
134
+ const y = matchingLeaf.x! + scrollY
135
+ ctx.fillStyle = 'rgba(255,165,0,0.2)' // Orange highlight for MSA sync
136
+ ctx.fillRect(0, y - rowHeight / 2, treeAreaWidth, rowHeight)
137
+ }
138
+ }
139
+ }
140
+ })
141
+ : undefined
142
+ }, [model, treeAreaWidth, height, scrollY])
143
+
83
144
  function mouseDown(event: React.MouseEvent) {
84
145
  // check if clicking a draggable element or a resize handle
85
146
  const target = event.target as HTMLElement
@@ -117,6 +178,20 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
117
178
  {blocksY.map(block => (
118
179
  <TreeCanvasBlock key={block} model={model} offsetY={block} />
119
180
  ))}
181
+ <canvas
182
+ ref={mouseoverRef}
183
+ width={treeAreaWidth}
184
+ height={height}
185
+ style={{
186
+ position: 'absolute',
187
+ top: 0,
188
+ left: 0,
189
+ width: treeAreaWidth,
190
+ height,
191
+ zIndex: 1000,
192
+ pointerEvents: 'none',
193
+ }}
194
+ />
120
195
  </div>
121
196
  )
122
197
  })
@@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
3
3
  import { useTheme } from '@mui/material'
4
4
  import { autorun } from 'mobx'
5
5
  import { observer } from 'mobx-react'
6
- import RBush from 'rbush'
6
+ import Flatbush from 'flatbush'
7
7
 
8
8
  import TreeBranchMenu from './TreeBranchMenu'
9
9
  import TreeNodeMenu from './TreeNodeMenu'
@@ -28,6 +28,46 @@ interface ClickEntry {
28
28
  maxY: number
29
29
  }
30
30
 
31
+ class ClickMapIndex {
32
+ private flatbush: Flatbush | null = null
33
+ private entries: ClickEntry[] = []
34
+
35
+ clear() {
36
+ this.flatbush = null
37
+ this.entries = []
38
+ }
39
+
40
+ insert(entry: ClickEntry) {
41
+ this.entries.push(entry)
42
+ }
43
+
44
+ finish() {
45
+ if (this.entries.length === 0) {
46
+ this.flatbush = null
47
+ return
48
+ } else {
49
+ this.flatbush = new Flatbush(this.entries.length)
50
+ for (const entry of this.entries) {
51
+ this.flatbush.add(entry.minX, entry.minY, entry.maxX, entry.maxY)
52
+ }
53
+ this.flatbush.finish()
54
+ }
55
+ }
56
+
57
+ search(box: {
58
+ minX: number
59
+ maxX: number
60
+ minY: number
61
+ maxY: number
62
+ }): ClickEntry[] {
63
+ return (
64
+ this.flatbush
65
+ ?.search(box.minX, box.minY, box.maxX, box.maxY)
66
+ .map(i => this.entries[i]!) ?? []
67
+ )
68
+ }
69
+ }
70
+
31
71
  const TreeCanvasBlock = observer(function ({
32
72
  model,
33
73
  offsetY,
@@ -37,7 +77,7 @@ const TreeCanvasBlock = observer(function ({
37
77
  }) {
38
78
  const theme = useTheme()
39
79
  const ref = useRef<HTMLCanvasElement>(null)
40
- const clickMap = useRef(new RBush<ClickEntry>())
80
+ const clickMap = useRef(new ClickMapIndex())
41
81
  const mouseoverRef = useRef<HTMLCanvasElement>(null)
42
82
  const [branchMenu, setBranchMenu] = useState<TooltipData>()
43
83
  const [toggleNodeMenu, setToggleNodeMenu] = useState<TooltipData>()
@@ -93,6 +133,7 @@ const TreeCanvasBlock = observer(function ({
93
133
  ctx.clearRect(0, 0, treeAreaWidth + padding, blockSize)
94
134
  ctx.translate(0, -offsetY)
95
135
 
136
+ // Highlight tree element being directly hovered
96
137
  if (hoverElt) {
97
138
  const { minX, maxX, minY, maxY } = hoverElt
98
139
 
@@ -169,9 +210,29 @@ const TreeCanvasBlock = observer(function ({
169
210
  return
170
211
  }
171
212
 
172
- const ret = hoverNameClickMap(event) || hoverBranchClickMap(event)
173
- ref.current.style.cursor = ret ? 'pointer' : 'default'
174
- setHoverElt(hoverNameClickMap(event))
213
+ const hoveredLeaf = hoverNameClickMap(event)
214
+ const hoveredBranch = hoverBranchClickMap(event)
215
+ const hoveredAny = hoveredLeaf || hoveredBranch
216
+
217
+ ref.current.style.cursor = hoveredAny ? 'pointer' : 'default'
218
+ setHoverElt(hoveredLeaf) // Only show direct hover highlight for leaf nodes
219
+
220
+ // Handle tree node hover for multi-row highlighting
221
+ if (hoveredAny) {
222
+ model.setHoveredTreeNode(hoveredAny.id)
223
+
224
+ // For leaf nodes, also set single row highlight for backward compatibility
225
+ if (hoveredLeaf?.name) {
226
+ const rowIndex = model.rowNamesSet.get(hoveredLeaf.name)
227
+ if (rowIndex !== undefined) {
228
+ model.setMousePos(undefined, rowIndex)
229
+ }
230
+ }
231
+ } else {
232
+ // Clear all highlighting when not hovering over any tree node
233
+ model.setHoveredTreeNode(undefined)
234
+ model.setMousePos(undefined, undefined)
235
+ }
175
236
  }}
176
237
  onClick={event => {
177
238
  const { clientX: x, clientY: y } = event
@@ -188,6 +249,9 @@ const TreeCanvasBlock = observer(function ({
188
249
  }}
189
250
  onMouseLeave={() => {
190
251
  setHoverElt(undefined)
252
+ // Clear all highlighting when leaving tree area
253
+ model.setHoveredTreeNode(undefined)
254
+ model.setMousePos(undefined, undefined)
191
255
  }}
192
256
  ref={vref}
193
257
  />
@@ -79,12 +79,23 @@ const TreeMenu = observer(function ({
79
79
  <MenuItem
80
80
  dense
81
81
  onClick={() => {
82
- model.drawRelativeTo(node.id)
82
+ model.drawRelativeTo(node.name)
83
83
  onClose()
84
84
  }}
85
85
  >
86
86
  Indicate differences from this row
87
87
  </MenuItem>
88
+ {model.relativeTo ? (
89
+ <MenuItem
90
+ dense
91
+ onClick={() => {
92
+ model.drawRelativeTo(undefined)
93
+ onClose()
94
+ }}
95
+ >
96
+ Clear reference row
97
+ </MenuItem>
98
+ ) : null}
88
99
  </Menu>
89
100
  )
90
101
  })
@@ -1,6 +1,5 @@
1
1
  import type { MsaViewModel } from '../../model'
2
2
  import type { Theme } from '@mui/material'
3
- import type RBush from 'rbush'
4
3
 
5
4
  export const padding = 600
6
5
 
@@ -18,6 +17,18 @@ interface ClickEntry {
18
17
  maxY: number
19
18
  }
20
19
 
20
+ interface ClickMapIndex {
21
+ clear(): void
22
+ insert(entry: ClickEntry): void
23
+ finish(): void
24
+ search(box: {
25
+ minX: number
26
+ maxX: number
27
+ minY: number
28
+ maxY: number
29
+ }): ClickEntry[]
30
+ }
31
+
21
32
  export function renderTree({
22
33
  offsetY,
23
34
  ctx,
@@ -75,7 +86,7 @@ export function renderNodeBubbles({
75
86
  blockSizeYOverride,
76
87
  }: {
77
88
  ctx: CanvasRenderingContext2D
78
- clickMap?: RBush<ClickEntry>
89
+ clickMap?: ClickMapIndex
79
90
  offsetY: number
80
91
  model: MsaViewModel
81
92
  theme: Theme
@@ -133,7 +144,7 @@ export function renderTreeLabels({
133
144
  model: MsaViewModel
134
145
  offsetY: number
135
146
  ctx: CanvasRenderingContext2D
136
- clickMap?: RBush<ClickEntry>
147
+ clickMap?: ClickMapIndex
137
148
  theme: Theme
138
149
  blockSizeYOverride?: number
139
150
  }) {
@@ -239,12 +250,14 @@ export function renderTreeCanvas({
239
250
  model: MsaViewModel
240
251
  offsetY: number
241
252
  ctx: CanvasRenderingContext2D
242
- clickMap?: RBush<ClickEntry>
253
+ clickMap?: ClickMapIndex
243
254
  theme: Theme
244
255
  highResScaleFactorOverride?: number
245
256
  blockSizeYOverride?: number
246
257
  }) {
247
258
  clickMap?.clear()
259
+
260
+ // Defer the finish call until after all inserts are done
248
261
  const {
249
262
  noTree,
250
263
  drawTree,
@@ -306,4 +319,7 @@ export function renderTreeCanvas({
306
319
  blockSizeYOverride,
307
320
  })
308
321
  }
322
+
323
+ // Finish the index so it's ready for queries
324
+ clickMap?.finish()
309
325
  }
@@ -11,7 +11,7 @@ export function chooseGridPitch(
11
11
  scale = Math.abs(scale)
12
12
  const minMajorPitchBp = minMajorPitchPx * scale
13
13
  const majorMagnitude = Number.parseInt(
14
- Number(minMajorPitchBp).toExponential().split(/e/i)[1]!,
14
+ minMajorPitchBp.toExponential().split(/e/i)[1]!,
15
15
  10,
16
16
  )
17
17
 
package/src/layout.ts CHANGED
@@ -1,4 +1,4 @@
1
- import RBush from 'rbush'
1
+ import Flatbush from 'flatbush'
2
2
 
3
3
  export default class Layout {
4
4
  public rectangles: Map<
@@ -13,11 +13,13 @@ export default class Layout {
13
13
  }
14
14
  >
15
15
 
16
- public maxHeightReached: boolean
16
+ public maxHeightReached = false
17
17
 
18
18
  private maxHeight: number
19
19
 
20
- private rbush: RBush<{ id: string }>
20
+ private flatbush: Flatbush | null = null
21
+
22
+ private indexToId: string[] = []
21
23
 
22
24
  private pTotalHeight: number
23
25
 
@@ -26,13 +28,51 @@ export default class Layout {
26
28
  }: {
27
29
  maxHeight?: number
28
30
  } = {}) {
29
- this.maxHeightReached = false
30
- this.rbush = new RBush()
31
31
  this.rectangles = new Map()
32
32
  this.maxHeight = Math.ceil(maxHeight)
33
33
  this.pTotalHeight = 0 // total height, in units of bitmap squares (px/pitchY)
34
34
  }
35
35
 
36
+ private rebuildIndex() {
37
+ // Rebuild the spatial index with current rectangles
38
+ const rects = Array.from(this.rectangles.values())
39
+ if (rects.length === 0) {
40
+ this.flatbush = null
41
+ this.indexToId = []
42
+ return
43
+ }
44
+
45
+ this.flatbush = new Flatbush(rects.length)
46
+ this.indexToId = []
47
+
48
+ for (const rect of rects) {
49
+ this.flatbush.add(rect.minX, rect.minY, rect.maxX, rect.maxY)
50
+ this.indexToId.push(rect.id)
51
+ }
52
+
53
+ this.flatbush.finish()
54
+ }
55
+
56
+ private collides(box: {
57
+ minX: number
58
+ minY: number
59
+ maxX: number
60
+ maxY: number
61
+ }): boolean {
62
+ if (!this.flatbush) {
63
+ return false
64
+ }
65
+
66
+ const results = this.flatbush.search(
67
+ box.minX,
68
+ box.minY,
69
+ box.maxX,
70
+ box.maxY,
71
+ )
72
+
73
+ return results.length > 0
74
+ }
75
+
36
76
  /**
37
77
  * @returns top position for the rect, or Null if laying
38
78
  * out the rect would exceed maxHeighe
@@ -44,7 +84,7 @@ export default class Layout {
44
84
  height: number,
45
85
  data: unknown,
46
86
  ): number | null {
47
- // add to rbush
87
+ // add to flatbush
48
88
  const existingRecord = this.rectangles.get(id)
49
89
  if (existingRecord) {
50
90
  return existingRecord.minY
@@ -52,7 +92,7 @@ export default class Layout {
52
92
 
53
93
  let currHeight = 0
54
94
  while (
55
- this.rbush.collides({
95
+ this.collides({
56
96
  minX: left,
57
97
  minY: currHeight,
58
98
  maxX: right,
@@ -71,8 +111,8 @@ export default class Layout {
71
111
  id,
72
112
  data,
73
113
  }
74
- this.rbush.insert(record)
75
114
  this.rectangles.set(id, record)
115
+ this.rebuildIndex()
76
116
  this.pTotalHeight = Math.max(this.pTotalHeight, currHeight)
77
117
  return currHeight
78
118
  }
@@ -70,14 +70,14 @@ export function TreeModelF() {
70
70
  * set tree area width (px)
71
71
  */
72
72
  setTreeAreaWidth(n: number) {
73
- self.treeAreaWidth = n
73
+ self.treeAreaWidth = Math.round(n)
74
74
  },
75
75
  /**
76
76
  * #action
77
77
  * set tree width (px)
78
78
  */
79
79
  setTreeWidth(n: number) {
80
- self.treeWidth = n
80
+ self.treeWidth = Math.round(n)
81
81
  },
82
82
 
83
83
  /**