react-msaview 5.6.0 → 5.6.2

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 (94) hide show
  1. package/bundle/index.js +95 -95
  2. package/bundle/index.js.map +4 -4
  3. package/dist/clustalX.d.ts +4 -3
  4. package/dist/clustalX.js +25 -4
  5. package/dist/clustalX.js.map +1 -1
  6. package/dist/columnCounts.d.ts +37 -0
  7. package/dist/columnCounts.js +148 -0
  8. package/dist/columnCounts.js.map +1 -0
  9. package/dist/components/MSAView.js +1 -8
  10. package/dist/components/MSAView.js.map +1 -1
  11. package/dist/components/getVisibleLeaves.d.ts +6 -0
  12. package/dist/components/getVisibleLeaves.js.map +1 -0
  13. package/dist/components/msa/MSACanvasBlock.js +17 -54
  14. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  15. package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +2 -1
  16. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +21 -19
  17. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  18. package/dist/components/msa/renderMSABlock.js +97 -96
  19. package/dist/components/msa/renderMSABlock.js.map +1 -1
  20. package/dist/components/msa/renderMSAMouseover.js +9 -32
  21. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  22. package/dist/components/msa/useMsaBlockMouse.d.ts +23 -0
  23. package/dist/components/msa/useMsaBlockMouse.js +45 -0
  24. package/dist/components/msa/useMsaBlockMouse.js.map +1 -0
  25. package/dist/components/tree/TreeCanvas.js +14 -19
  26. package/dist/components/tree/TreeCanvas.js.map +1 -1
  27. package/dist/components/tree/TreeCanvasBlock.js +40 -111
  28. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  29. package/dist/components/tree/renderTreeCanvas.d.ts +2 -0
  30. package/dist/components/tree/renderTreeCanvas.js +15 -3
  31. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  32. package/dist/components/tree/useTreeHover.d.ts +24 -0
  33. package/dist/components/tree/useTreeHover.js +44 -0
  34. package/dist/components/tree/useTreeHover.js.map +1 -0
  35. package/dist/hierarchy.js +15 -9
  36. package/dist/hierarchy.js.map +1 -1
  37. package/dist/model/msaModel.js +4 -3
  38. package/dist/model/msaModel.js.map +1 -1
  39. package/dist/model/treeModel.js +9 -8
  40. package/dist/model/treeModel.js.map +1 -1
  41. package/dist/model.d.ts +52 -10
  42. package/dist/model.js +208 -93
  43. package/dist/model.js.map +1 -1
  44. package/dist/propertyConservation.d.ts +2 -1
  45. package/dist/propertyConservation.js +36 -25
  46. package/dist/propertyConservation.js.map +1 -1
  47. package/dist/seqPosToGlobalCol.d.ts +7 -0
  48. package/dist/seqPosToGlobalCol.js +17 -0
  49. package/dist/seqPosToGlobalCol.js.map +1 -1
  50. package/dist/stripDefault.d.ts +21 -0
  51. package/dist/stripDefault.js +26 -0
  52. package/dist/stripDefault.js.map +1 -0
  53. package/dist/types.d.ts +16 -0
  54. package/dist/version.d.ts +1 -1
  55. package/dist/version.js +1 -1
  56. package/package.json +3 -3
  57. package/src/clustalX.test.ts +9 -11
  58. package/src/clustalX.ts +27 -28
  59. package/src/colorSchemes.docs.test.ts +3 -1
  60. package/src/columnCounts.test.ts +58 -0
  61. package/src/columnCounts.ts +170 -0
  62. package/src/components/MSAView.tsx +1 -9
  63. package/src/components/{msa/getVisibleLeaves.ts → getVisibleLeaves.ts} +1 -1
  64. package/src/components/msa/MSACanvasBlock.tsx +42 -110
  65. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +25 -18
  66. package/src/components/msa/renderMSABlock.ts +113 -125
  67. package/src/components/msa/renderMSAMouseover.ts +11 -34
  68. package/src/components/msa/useMsaBlockMouse.ts +81 -0
  69. package/src/components/tree/TreeCanvas.tsx +16 -23
  70. package/src/components/tree/TreeCanvasBlock.tsx +62 -143
  71. package/src/components/tree/renderTreeCanvas.test.ts +100 -190
  72. package/src/components/tree/renderTreeCanvas.ts +14 -3
  73. package/src/components/tree/useTreeHover.ts +69 -0
  74. package/src/domainBands.test.ts +98 -0
  75. package/src/hierarchy.ts +15 -9
  76. package/src/model/msaModel.ts +4 -3
  77. package/src/model/treeModel.ts +9 -8
  78. package/src/model.ts +241 -106
  79. package/src/propertyConservation.test.ts +2 -13
  80. package/src/propertyConservation.ts +40 -31
  81. package/src/rowSpace.test.ts +46 -0
  82. package/src/seqPosToGlobalCol.ts +18 -0
  83. package/src/stripDefault.test.ts +48 -0
  84. package/src/stripDefault.ts +35 -0
  85. package/src/stripDefaultSnapshot.test.ts +4 -1
  86. package/src/types.ts +23 -0
  87. package/src/version.ts +1 -1
  88. package/dist/components/msa/getVisibleLeaves.d.ts +0 -6
  89. package/dist/components/msa/getVisibleLeaves.js.map +0 -1
  90. package/dist/useDevicePixelRatio.d.ts +0 -1
  91. package/dist/useDevicePixelRatio.js +0 -27
  92. package/dist/useDevicePixelRatio.js.map +0 -1
  93. package/src/useDevicePixelRatio.ts +0 -29
  94. /package/dist/components/{msa/getVisibleLeaves.js → getVisibleLeaves.js} +0 -0
@@ -1,7 +1,7 @@
1
- import { getVisibleLeaves } from './getVisibleLeaves.ts'
2
1
  import { visibleColRange } from './visibleColRange.ts'
3
2
  import { setFontSize } from '../../setFontSize.ts'
4
3
  import { adjustColorForContrast } from '../../util.ts'
4
+ import { getVisibleLeaves } from '../getVisibleLeaves.ts'
5
5
 
6
6
  import type { HierarchyNode } from '../../hierarchy.ts'
7
7
  import type { MsaViewModel } from '../../model.ts'
@@ -60,27 +60,19 @@ export function renderMSABlock({
60
60
  ? columns.get(relativeTo)?.slice(xStart, xEnd)
61
61
  : null
62
62
 
63
- if (!actuallyShowDomains) {
64
- drawTilesAndText({
65
- model,
66
- ctx,
67
- theme,
68
- contrastScheme,
69
- xStart,
70
- xEnd,
71
- visibleLeaves,
72
- referenceSeq,
73
- })
74
- } else {
75
- drawText({
76
- model,
77
- ctx,
78
- xStart,
79
- xEnd,
80
- visibleLeaves,
81
- referenceSeq,
82
- })
83
- }
63
+ drawTilesAndText({
64
+ model,
65
+ ctx,
66
+ theme,
67
+ contrastScheme,
68
+ xStart,
69
+ xEnd,
70
+ visibleLeaves,
71
+ referenceSeq,
72
+ // when domains are shown the background tiles come from
73
+ // renderBoxFeatureCanvasBlock, so only the letters are drawn here
74
+ drawTiles: !actuallyShowDomains,
75
+ })
84
76
  drawInsertionIndicators({
85
77
  model,
86
78
  ctx,
@@ -91,6 +83,35 @@ export function renderMSABlock({
91
83
  ctx.resetTransform()
92
84
  }
93
85
 
86
+ // Per-cell tile color for the active scheme. The scheme is fixed for the whole
87
+ // block, so resolve which rule applies once rather than re-testing the scheme
88
+ // name inside the innermost loop.
89
+ function tileColorFn(model: MsaViewModel) {
90
+ const { colorSchemeName, colorScheme, colClustalX, colConsensus } = model
91
+ if (colorSchemeName === 'clustalx_protein_dynamic') {
92
+ return (col: number, letter: string) => colClustalX[col]![letter]
93
+ }
94
+ if (colorSchemeName === 'percent_identity_dynamic') {
95
+ return (col: number, letter: string) => {
96
+ const consensus = colConsensus[col]!
97
+ return letter === consensus.letter ? consensus.color : undefined
98
+ }
99
+ }
100
+ return (_col: number, letter: string) => colorScheme[letter]
101
+ }
102
+
103
+ // Letter color to use over a domain box, keyed by accession. Domain fills come
104
+ // from a categorical palette that has no fixed lightness, so ask the theme for a
105
+ // readable text color per fill instead of assuming dark-on-light.
106
+ function domainLetterColors(model: MsaViewModel, theme: Theme) {
107
+ return new Map(
108
+ Object.entries(model.fillPalette).map(([accession, fill]) => [
109
+ accession,
110
+ theme.palette.getContrastText(fill),
111
+ ]),
112
+ )
113
+ }
114
+
94
115
  function drawTilesAndText({
95
116
  model,
96
117
  ctx,
@@ -100,6 +121,7 @@ function drawTilesAndText({
100
121
  xStart,
101
122
  xEnd,
102
123
  referenceSeq,
124
+ drawTiles,
103
125
  }: {
104
126
  model: MsaViewModel
105
127
  theme: Theme
@@ -109,128 +131,94 @@ function drawTilesAndText({
109
131
  xStart: number
110
132
  xEnd: number
111
133
  referenceSeq: string | null | undefined
134
+ drawTiles: boolean
112
135
  }) {
113
136
  const {
114
137
  bgColor,
115
- colorSchemeName,
116
- colorScheme,
117
138
  columns,
118
139
  colWidth,
119
140
  rowHeight,
120
141
  relativeTo,
121
142
  showMsaLetters,
122
- colClustalX,
123
- colConsensus,
143
+ subFeatureRows,
144
+ domainBandsByStart,
124
145
  } = model
125
146
 
126
- const isClustalX = colorSchemeName === 'clustalx_protein_dynamic'
127
- const isPercentIdentity = colorSchemeName === 'percent_identity_dynamic'
128
- if (!bgColor && !showMsaLetters) {
129
- return
130
- }
131
- const offsetXAligned = xStart * colWidth
132
- const halfColWidth = colWidth / 2
133
- const quarterRowHeight = rowHeight / 4
134
-
135
- for (let i = 0, l1 = visibleLeaves.length; i < l1; i++) {
136
- const node = visibleLeaves[i]!
137
- const { name } = node.data
138
- const y = node.x!
139
- const str = columns.get(name)?.slice(xStart, xEnd)
140
- if (!str) {
141
- continue
142
- }
143
- const tileY = y - rowHeight
144
- const textY = y - quarterRowHeight
147
+ const tiles = drawTiles && bgColor
148
+ if (tiles || showMsaLetters) {
149
+ const tileColor = tileColorFn(model)
150
+ const offsetXAligned = xStart * colWidth
151
+ const halfColWidth = colWidth / 2
152
+ // note: -rowHeight/4 matches +rowHeight/4 in tree
153
+ const quarterRowHeight = rowHeight / 4
154
+ // with the tiles coming from the domain overlay instead, letters sit either
155
+ // on a domain box or on the plain background; sub-row layout stacks the boxes
156
+ // above the letters, so those rows are all plain background
157
+ const overDomains = !drawTiles && !subFeatureRows
158
+ const domainColors = overDomains
159
+ ? domainLetterColors(model, theme)
160
+ : undefined
145
161
 
146
- for (let j = 0, l2 = str.length; j < l2; j++) {
147
- const letter = str[j]!
148
- const x = j * colWidth + offsetXAligned
149
- const isMatchingReference =
150
- referenceSeq && name !== relativeTo && letter === referenceSeq[j]
162
+ for (let i = 0, l1 = visibleLeaves.length; i < l1; i++) {
163
+ const node = visibleLeaves[i]!
164
+ const { name } = node.data
165
+ const y = node.x!
166
+ const str = columns.get(name)?.slice(xStart, xEnd)
167
+ if (str) {
168
+ const tileY = y - rowHeight
169
+ const textY = y - quarterRowHeight
170
+ const bands = overDomains ? domainBandsByStart.get(name) : undefined
171
+ let band = 0
151
172
 
152
- let color: string | undefined
153
- if (isClustalX) {
154
- color = colClustalX[xStart + j]![letter]
155
- } else if (isPercentIdentity) {
156
- const consensus = colConsensus[xStart + j]!
157
- color = letter === consensus.letter ? consensus.color : undefined
158
- } else {
159
- color = colorScheme[letter]
160
- }
173
+ for (let j = 0, l2 = str.length; j < l2; j++) {
174
+ const col = xStart + j
175
+ const letter = str[j]!
176
+ const x = j * colWidth + offsetXAligned
177
+ const isMatchingReference =
178
+ referenceSeq && name !== relativeTo && letter === referenceSeq[j]
179
+ const color = tileColor(col, letter)
161
180
 
162
- if (bgColor) {
163
- ctx.fillStyle = isMatchingReference
164
- ? theme.palette.action.hover
165
- : color || theme.palette.background.default
166
- ctx.fillRect(x, tileY, colWidth, rowHeight)
167
- }
181
+ if (tiles) {
182
+ ctx.fillStyle = isMatchingReference
183
+ ? theme.palette.action.hover
184
+ : color || theme.palette.background.default
185
+ ctx.fillRect(x, tileY, colWidth, rowHeight)
186
+ }
168
187
 
169
- if (showMsaLetters) {
170
- ctx.fillStyle = bgColor
171
- ? (contrastScheme[letter] ?? 'black')
172
- : color
173
- ? adjustColorForContrast(color, theme.palette.background.default)
174
- : theme.palette.text.primary
175
- ctx.fillText(
176
- isMatchingReference ? '.' : letter,
177
- x + halfColWidth,
178
- textY,
179
- )
188
+ if (showMsaLetters) {
189
+ // bands are sorted by start column and the sweep is left to right,
190
+ // so one forward-only cursor finds the band covering each column
191
+ while (bands && band < bands.length && bands[band]!.endCol <= col) {
192
+ band++
193
+ }
194
+ const covering = bands?.[band]
195
+ ctx.fillStyle =
196
+ covering && covering.startCol <= col
197
+ ? // on top of a domain box: contrast against the box fill
198
+ domainColors!.get(covering.annotation.accession)!
199
+ : tiles
200
+ ? // on top of a colored tile
201
+ (contrastScheme[letter] ?? 'black')
202
+ : !drawTiles || !color
203
+ ? // plain background, uncolored letters
204
+ theme.palette.text.primary
205
+ : // letter-color mode: darken/lighten to stay readable
206
+ adjustColorForContrast(
207
+ color,
208
+ theme.palette.background.default,
209
+ )
210
+ ctx.fillText(
211
+ isMatchingReference ? '.' : letter,
212
+ x + halfColWidth,
213
+ textY,
214
+ )
215
+ }
216
+ }
180
217
  }
181
218
  }
182
219
  }
183
220
  }
184
221
 
185
- // When domains are shown the background tiles come from renderBoxFeatureCanvasBlock,
186
- // so we only need to draw text here.
187
- function drawText({
188
- model,
189
- ctx,
190
- visibleLeaves,
191
- xStart,
192
- xEnd,
193
- referenceSeq,
194
- }: {
195
- model: MsaViewModel
196
- ctx: RenderCtx
197
- visibleLeaves: HierarchyNode<NodeWithIdsAndLength>[]
198
- xStart: number
199
- xEnd: number
200
- referenceSeq: string | null | undefined
201
- }) {
202
- const { showMsaLetters, columns, colWidth, rowHeight, relativeTo } = model
203
-
204
- if (!showMsaLetters) {
205
- return
206
- }
207
- const offsetXAligned = xStart * colWidth
208
- const halfColWidth = colWidth / 2
209
- const quarterRowHeight = rowHeight / 4
210
- // note: -rowHeight/4 matches +rowHeight/4 in tree
211
- ctx.fillStyle = 'black'
212
-
213
- for (let i = 0, l1 = visibleLeaves.length; i < l1; i++) {
214
- const node = visibleLeaves[i]!
215
- const { name } = node.data
216
- const y = node.x! - quarterRowHeight
217
- const str = columns.get(name)?.slice(xStart, xEnd)
218
- if (!str) {
219
- continue
220
- }
221
- for (let j = 0, l2 = str.length; j < l2; j++) {
222
- const letter = str[j]!
223
- const isMatchingReference =
224
- referenceSeq && name !== relativeTo && letter === referenceSeq[j]
225
- ctx.fillText(
226
- isMatchingReference ? '.' : letter,
227
- j * colWidth + offsetXAligned + halfColWidth,
228
- y,
229
- )
230
- }
231
- }
232
- }
233
-
234
222
  function drawInsertionIndicators({
235
223
  model,
236
224
  ctx,
@@ -10,23 +10,6 @@ const highlightColor = 'rgba(128,128,0,0.2)'
10
10
  const highlightColumnsFill = 'rgba(255,140,0,0.28)'
11
11
  const highlightColumnsBorder = 'rgba(210,90,0,0.95)'
12
12
 
13
- // Collapse sorted column indices into contiguous [start,end] runs so a run of
14
- // highlighted columns draws as one bordered band rather than per-column slices
15
- // (which would draw internal borders).
16
- function contiguousRuns(columns: number[]) {
17
- const sorted = [...columns].sort((a, b) => a - b)
18
- const runs: { start: number; end: number }[] = []
19
- for (const col of sorted) {
20
- const last = runs.at(-1)
21
- if (last && col === last.end + 1) {
22
- last.end = col
23
- } else {
24
- runs.push({ start: col, end: col })
25
- }
26
- }
27
- return runs
28
- }
29
-
30
13
  export function renderMouseover({
31
14
  ctx,
32
15
  model,
@@ -46,10 +29,10 @@ export function renderMouseover({
46
29
  mouseRow,
47
30
  mouseClickRow,
48
31
  mouseClickCol,
49
- relativeTo,
50
- hoveredTreeNode,
51
- highlightedColumns,
52
32
  highResScaleFactor,
33
+ referenceRowIndex,
34
+ hoveredRowIndices,
35
+ highlightedColumnRuns,
53
36
  } = model
54
37
  const width = msaAreaWidth - verticalScrollbarWidth
55
38
  ctx.resetTransform()
@@ -57,30 +40,24 @@ export function renderMouseover({
57
40
  ctx.scale(highResScaleFactor, highResScaleFactor)
58
41
 
59
42
  // Highlight reference row (relativeTo) persistently
60
- if (relativeTo) {
61
- const referenceRowIndex = model.rowNamesSet.get(relativeTo)
62
- if (referenceRowIndex !== undefined) {
63
- ctx.fillStyle = referenceColor
64
- ctx.fillRect(0, referenceRowIndex * rowHeight + scrollY, width, rowHeight)
65
- }
43
+ if (referenceRowIndex !== undefined) {
44
+ ctx.fillStyle = referenceColor
45
+ ctx.fillRect(0, referenceRowIndex * rowHeight + scrollY, width, rowHeight)
66
46
  }
67
47
 
68
48
  // Highlight multiple rows when hovering over tree nodes with children
69
- if (hoveredTreeNode) {
49
+ if (hoveredRowIndices.length > 0) {
70
50
  ctx.fillStyle = multiRowHoverColor
71
- for (const descendantName of hoveredTreeNode.descendantNames) {
72
- const rowIndex = model.rowNamesSet.get(descendantName)
73
- if (rowIndex !== undefined) {
74
- ctx.fillRect(0, rowIndex * rowHeight + scrollY, width, rowHeight)
75
- }
51
+ for (const rowIndex of hoveredRowIndices) {
52
+ ctx.fillRect(0, rowIndex * rowHeight + scrollY, width, rowHeight)
76
53
  }
77
54
  }
78
55
 
79
56
  // Highlight multiple columns — draw each contiguous run as a filled, bordered
80
57
  // band so a domain/motif highlight is clearly visible over the alignment
81
- if (highlightedColumns?.length) {
58
+ if (highlightedColumnRuns.length > 0) {
82
59
  ctx.lineWidth = 2
83
- for (const { start, end } of contiguousRuns(highlightedColumns)) {
60
+ for (const { start, end } of highlightedColumnRuns) {
84
61
  const x = start * colWidth + scrollX
85
62
  const w = (end - start + 1) * colWidth
86
63
  ctx.fillStyle = highlightColumnsFill
@@ -0,0 +1,81 @@
1
+ import { useState } from 'react'
2
+ import type React from 'react'
3
+
4
+ import type { MsaViewModel } from '../../model.ts'
5
+
6
+ function eventToColRow({
7
+ event,
8
+ el,
9
+ offsetX,
10
+ offsetY,
11
+ colWidth,
12
+ rowHeight,
13
+ }: {
14
+ event: React.MouseEvent
15
+ el: HTMLElement
16
+ offsetX: number
17
+ offsetY: number
18
+ colWidth: number
19
+ rowHeight: number
20
+ }) {
21
+ const { left, top } = el.getBoundingClientRect()
22
+ return {
23
+ col: Math.floor((event.clientX - left + offsetX) / colWidth),
24
+ row: Math.floor((event.clientY - top + offsetY) / rowHeight),
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Translates pointer events over one MSA block into model column/row hover and
30
+ * click state, and reports where a tooltip should be anchored.
31
+ *
32
+ * The anchor is only tracked when there is something to show (an insertion, a
33
+ * domain under the cursor, or column stats), so plain mouse movement over the
34
+ * alignment updates the canvas overlay without re-rendering this component.
35
+ */
36
+ export function useMsaBlockMouse({
37
+ model,
38
+ offsetX,
39
+ offsetY,
40
+ }: {
41
+ model: MsaViewModel
42
+ offsetX: number
43
+ offsetY: number
44
+ }) {
45
+ const [tooltipPoint, setTooltipPoint] = useState<{ x: number; y: number }>()
46
+
47
+ function colRow(event: React.MouseEvent, el: HTMLElement) {
48
+ const { colWidth, rowHeight } = model
49
+ return eventToColRow({ event, el, offsetX, offsetY, colWidth, rowHeight })
50
+ }
51
+
52
+ function onMouseMove(event: React.MouseEvent, el: HTMLElement) {
53
+ const { col, row } = colRow(event, el)
54
+ const inBounds =
55
+ col >= 0 && col < model.numColumns && row >= 0 && row < model.numRows
56
+ model.setMousePos(inBounds ? col : undefined, inBounds ? row : undefined)
57
+
58
+ const hasTooltip =
59
+ !!model.hoveredInsertion ||
60
+ model.mouseOverDomains.length > 0 ||
61
+ (model.showColumnStats && !!model.mouseOverColumnStats)
62
+ setTooltipPoint(
63
+ hasTooltip ? { x: event.clientX, y: event.clientY } : undefined,
64
+ )
65
+ }
66
+
67
+ function onClick(event: React.MouseEvent, el: HTMLElement) {
68
+ const { col, row } = colRow(event, el)
69
+ const { mouseClickCol, mouseClickRow } = model
70
+ // clicking the same cell again clears the pinned crosshair
71
+ const same = col === mouseClickCol && row === mouseClickRow
72
+ model.setMouseClickPos(same ? undefined : col, same ? undefined : row)
73
+ }
74
+
75
+ function onMouseLeave() {
76
+ model.setMousePos()
77
+ setTooltipPoint(undefined)
78
+ }
79
+
80
+ return { tooltipPoint, onMouseMove, onClick, onMouseLeave }
81
+ }
@@ -26,45 +26,38 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
26
26
  ctx => {
27
27
  if (isAlive(model)) {
28
28
  const {
29
- relativeTo,
30
- leaves,
31
29
  rowHeight,
32
- hoveredTreeNode,
33
30
  treeAreaWidth: w,
34
31
  height: h,
35
32
  scrollY: sy,
36
- mouseOverRowName,
33
+ mouseRow,
34
+ referenceRowIndex,
35
+ hoveredRowIndices,
37
36
  } = model
38
37
  ctx.resetTransform()
39
38
  ctx.clearRect(0, 0, w, h)
40
39
 
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
- }
40
+ // rows are laid out at a fixed pitch, so a row index is all that is
41
+ // needed to place its band (leaf.x is rowHeight*(index+0.5))
42
+ const fillRow = (index: number) => {
43
+ ctx.fillRect(0, index * rowHeight + sy, w, rowHeight)
47
44
  }
48
45
 
49
- if (relativeTo) {
46
+ if (referenceRowIndex !== undefined) {
50
47
  ctx.fillStyle = referenceColor
51
- fillRow(relativeTo)
48
+ fillRow(referenceRowIndex)
52
49
  }
53
50
 
54
- if (hoveredTreeNode) {
55
- ctx.fillStyle = treeHoverColor
56
- for (const descendantName of hoveredTreeNode.descendantNames) {
57
- fillRow(descendantName)
58
- }
51
+ ctx.fillStyle = treeHoverColor
52
+ for (const index of hoveredRowIndices) {
53
+ fillRow(index)
59
54
  }
60
-
61
55
  if (
62
- mouseOverRowName &&
63
- mouseOverRowName !== relativeTo &&
64
- !hoveredTreeNode?.descendantNames.includes(mouseOverRowName)
56
+ mouseRow !== undefined &&
57
+ mouseRow !== referenceRowIndex &&
58
+ !hoveredRowIndices.includes(mouseRow)
65
59
  ) {
66
- ctx.fillStyle = treeHoverColor
67
- fillRow(mouseOverRowName)
60
+ fillRow(mouseRow)
68
61
  }
69
62
  }
70
63
  },