react-msaview 5.6.1 → 5.6.3

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/index.d.ts +2 -1
  38. package/dist/model/msaModel.js +4 -3
  39. package/dist/model/msaModel.js.map +1 -1
  40. package/dist/model/treeModel.js +9 -8
  41. package/dist/model/treeModel.js.map +1 -1
  42. package/dist/model.d.ts +52 -10
  43. package/dist/model.js +208 -93
  44. package/dist/model.js.map +1 -1
  45. package/dist/propertyConservation.d.ts +2 -1
  46. package/dist/propertyConservation.js +36 -25
  47. package/dist/propertyConservation.js.map +1 -1
  48. package/dist/seqPosToGlobalCol.d.ts +7 -0
  49. package/dist/seqPosToGlobalCol.js +17 -0
  50. package/dist/seqPosToGlobalCol.js.map +1 -1
  51. package/dist/stripDefault.d.ts +21 -0
  52. package/dist/stripDefault.js +26 -0
  53. package/dist/stripDefault.js.map +1 -0
  54. package/dist/types.d.ts +16 -0
  55. package/dist/version.d.ts +1 -1
  56. package/dist/version.js +1 -1
  57. package/package.json +3 -3
  58. package/src/clustalX.test.ts +9 -11
  59. package/src/clustalX.ts +27 -28
  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/index.ts +7 -0
  77. package/src/model/msaModel.ts +4 -6
  78. package/src/model/treeModel.ts +9 -14
  79. package/src/model.ts +241 -112
  80. package/src/propertyConservation.test.ts +2 -13
  81. package/src/propertyConservation.ts +40 -31
  82. package/src/rowSpace.test.ts +46 -0
  83. package/src/seqPosToGlobalCol.ts +18 -0
  84. package/src/stripDefault.test.ts +48 -0
  85. package/src/stripDefault.ts +35 -0
  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
@@ -0,0 +1,58 @@
1
+ import { describe, expect, test } from 'vitest'
2
+
3
+ import {
4
+ columnCountsFromColumns,
5
+ columnCountsFromRows,
6
+ letterOfResidueSlot,
7
+ } from './columnCounts.ts'
8
+
9
+ describe('columnCounts', () => {
10
+ test('tallies residues and gaps per column', () => {
11
+ const counts = columnCountsFromRows(['AC-', 'AG.', 'AC-'])
12
+ expect(counts.numColumns).toBe(3)
13
+ expect(counts.count(0, 'A')).toBe(3)
14
+ expect(counts.count(1, 'C')).toBe(2)
15
+ expect(counts.count(1, 'G')).toBe(1)
16
+ expect(counts.total(2)).toBe(3)
17
+ expect(counts.gapCount(2)).toBe(3)
18
+ expect(counts.gapCount(0)).toBe(0)
19
+ })
20
+
21
+ test('counts both gap characters, and neither as a residue', () => {
22
+ const counts = columnCountsFromColumns(['-.A'])
23
+ expect(counts.count(0, '-')).toBe(1)
24
+ expect(counts.count(0, '.')).toBe(1)
25
+ expect(counts.gapCount(0)).toBe(2)
26
+ expect(counts.residueEntries(0)).toEqual([['A', 1]])
27
+ expect(counts.lettersPresent).toEqual(new Set(['A']))
28
+ })
29
+
30
+ test('folds case, so lowercase (a3m insertions) counts as its residue', () => {
31
+ const counts = columnCountsFromColumns(['aA'])
32
+ expect(counts.count(0, 'A')).toBe(2)
33
+ })
34
+
35
+ test('digits and punctuation do not land on letter slots', () => {
36
+ // a plain `code & 31` would fold '0' onto 'P' and '!' onto 'A'
37
+ const counts = columnCountsFromColumns(['0!'])
38
+ expect(counts.count(0, 'P')).toBe(0)
39
+ expect(counts.count(0, 'A')).toBe(0)
40
+ expect(counts.residueEntries(0)).toEqual([['?', 2]])
41
+ })
42
+
43
+ test('ragged rows: the widest row sets the column count', () => {
44
+ const counts = columnCountsFromRows(['AAAA', 'AA'])
45
+ expect(counts.numColumns).toBe(4)
46
+ expect(counts.total(0)).toBe(2)
47
+ expect(counts.total(3)).toBe(1)
48
+ })
49
+
50
+ test('residue slots round-trip to their letters', () => {
51
+ const counts = columnCountsFromColumns(['WY'])
52
+ const slots: number[] = []
53
+ counts.forEachResidue(0, slot => {
54
+ slots.push(slot)
55
+ })
56
+ expect(slots.map(letterOfResidueSlot)).toEqual(['W', 'Y'])
57
+ })
58
+ })
@@ -0,0 +1,170 @@
1
+ // Per-column residue counts for an alignment, stored as a flat typed array
2
+ // instead of one `Record<string, number>` per column. Column statistics are the
3
+ // hottest computation in the viewer (they feed conservation, ClustalX coloring,
4
+ // consensus and the hover tooltip), and the object-per-column form spent all of
5
+ // its time on megamorphic string-keyed property increments: 1000x5000 residues
6
+ // took ~1.6s to tally that way versus ~35ms here.
7
+ //
8
+ // Each column owns a fixed stride of counter slots. Slots 1-26 are A-Z (case
9
+ // folded), plus a slot each for the two gap characters, '*', and anything else.
10
+
11
+ const SLOTS = 32
12
+ const DASH = 27
13
+ const DOT = 28
14
+ const STAR = 29
15
+ const OTHER = 30
16
+
17
+ const A = 'A'.charCodeAt(0)
18
+ const Z = 'Z'.charCodeAt(0)
19
+ const a = 'a'.charCodeAt(0)
20
+ const z = 'z'.charCodeAt(0)
21
+
22
+ // charCode -> slot. A plain `code & 31` would fold digits and punctuation onto
23
+ // letter slots ('0' onto 'P'), so the mapping is an explicit table.
24
+ const slotOfCode = new Uint8Array(128).fill(OTHER)
25
+ for (let c = A; c <= Z; c++) {
26
+ slotOfCode[c] = c - A + 1
27
+ }
28
+ for (let c = a; c <= z; c++) {
29
+ slotOfCode[c] = c - a + 1
30
+ }
31
+ slotOfCode['-'.charCodeAt(0)] = DASH
32
+ slotOfCode['.'.charCodeAt(0)] = DOT
33
+ slotOfCode['*'.charCodeAt(0)] = STAR
34
+
35
+ const letterOfSlot = Array.from({ length: SLOTS }, (_, slot) =>
36
+ slot >= 1 && slot <= 26 ? String.fromCharCode(A + slot - 1) : '',
37
+ )
38
+ letterOfSlot[DASH] = '-'
39
+ letterOfSlot[DOT] = '.'
40
+ letterOfSlot[STAR] = '*'
41
+ letterOfSlot[OTHER] = '?'
42
+
43
+ function slotOf(code: number) {
44
+ return code < 128 ? slotOfCode[code]! : OTHER
45
+ }
46
+
47
+ export function isGapSlot(slot: number) {
48
+ return slot === DASH || slot === DOT
49
+ }
50
+
51
+ export class ColumnCounts {
52
+ readonly numColumns: number
53
+ private readonly counts: Uint32Array
54
+ // per-column residue+gap totals
55
+ readonly totals: Uint32Array
56
+ // whole-alignment totals per slot, so "which letters appear anywhere" is a
57
+ // 32-slot scan rather than a walk over every column
58
+ private readonly slotTotals = new Uint32Array(SLOTS)
59
+
60
+ constructor(numColumns: number) {
61
+ this.numColumns = numColumns
62
+ this.counts = new Uint32Array(numColumns * SLOTS)
63
+ this.totals = new Uint32Array(numColumns)
64
+ }
65
+
66
+ addRow(row: string) {
67
+ const { counts, totals, slotTotals } = this
68
+ const n = Math.min(row.length, this.numColumns)
69
+ for (let col = 0; col < n; col++) {
70
+ const slot = slotOf(row.charCodeAt(col))
71
+ counts[col * SLOTS + slot]!++
72
+ totals[col]!++
73
+ slotTotals[slot]!++
74
+ }
75
+ }
76
+
77
+ add(col: number, letter: string) {
78
+ const slot = slotOf(letter.charCodeAt(0))
79
+ this.counts[col * SLOTS + slot]!++
80
+ this.totals[col]!++
81
+ this.slotTotals[slot]!++
82
+ }
83
+
84
+ total(col: number) {
85
+ return this.totals[col] ?? 0
86
+ }
87
+
88
+ count(col: number, letter: string) {
89
+ return this.counts[col * SLOTS + slotOf(letter.charCodeAt(0))] ?? 0
90
+ }
91
+
92
+ gapCount(col: number) {
93
+ const base = col * SLOTS
94
+ return (this.counts[base + DASH] ?? 0) + (this.counts[base + DOT] ?? 0)
95
+ }
96
+
97
+ /**
98
+ * Visits every non-gap residue present in a column. Slots with a zero count
99
+ * are skipped, so this is ~the number of distinct residues, not 32.
100
+ */
101
+ forEachResidue(col: number, cb: (slot: number, count: number) => void) {
102
+ const { counts } = this
103
+ const base = col * SLOTS
104
+ for (let slot = 1; slot < SLOTS; slot++) {
105
+ const count = counts[base + slot]!
106
+ if (count > 0 && !isGapSlot(slot)) {
107
+ cb(slot, count)
108
+ }
109
+ }
110
+ }
111
+
112
+ /** non-gap [letter, count] pairs for one column, for display */
113
+ residueEntries(col: number) {
114
+ const entries: [string, number][] = []
115
+ this.forEachResidue(col, (slot, count) => {
116
+ entries.push([letterOfSlot[slot]!, count])
117
+ })
118
+ return entries
119
+ }
120
+
121
+ /** the distinct non-gap letters appearing anywhere in the alignment */
122
+ get lettersPresent() {
123
+ const letters = new Set<string>()
124
+ for (let slot = 1; slot < SLOTS; slot++) {
125
+ if (this.slotTotals[slot]! > 0 && !isGapSlot(slot)) {
126
+ letters.add(letterOfSlot[slot]!)
127
+ }
128
+ }
129
+ return letters
130
+ }
131
+ }
132
+
133
+ export function letterOfResidueSlot(slot: number) {
134
+ return letterOfSlot[slot]!
135
+ }
136
+
137
+ export function residueSlotOfLetter(letter: string) {
138
+ return slotOf(letter.charCodeAt(0))
139
+ }
140
+
141
+ export const numResidueSlots = SLOTS
142
+
143
+ /**
144
+ * Tally counts from alignment rows (gapped sequence strings, one per row). The
145
+ * column count follows the longest row, matching what a per-column tally of
146
+ * ragged input produces.
147
+ */
148
+ export function columnCountsFromRows(rows: string[]) {
149
+ const numColumns = rows.reduce((max, row) => Math.max(max, row.length), 0)
150
+ const counts = new ColumnCounts(numColumns)
151
+ for (const row of rows) {
152
+ counts.addRow(row)
153
+ }
154
+ return counts
155
+ }
156
+
157
+ /**
158
+ * Tally counts from column strings (one string per column, listing that
159
+ * column's residues top to bottom). Convenient for tests and for callers that
160
+ * already think column-wise.
161
+ */
162
+ export function columnCountsFromColumns(columns: string[]) {
163
+ const counts = new ColumnCounts(columns.length)
164
+ for (const [col, column] of columns.entries()) {
165
+ for (const letter of column) {
166
+ counts.add(col, letter)
167
+ }
168
+ }
169
+ return counts
170
+ }
@@ -1,8 +1,7 @@
1
- import React, { Suspense, useEffect } from 'react'
1
+ import React, { Suspense } from 'react'
2
2
 
3
3
  import { observer } from 'mobx-react'
4
4
 
5
- import { useDevicePixelRatio } from '../useDevicePixelRatio.ts'
6
5
  import {
7
6
  HorizontalResizeHandle,
8
7
  VerticalResizeHandle,
@@ -130,13 +129,6 @@ const View = observer(function ({ model }: { model: MsaViewModel }) {
130
129
  const MSAView = observer(function ({ model }: { model: MsaViewModel }) {
131
130
  const { height, viewInitialized, DialogComponent, DialogProps } = model
132
131
 
133
- // mirror the live device pixel ratio into the model so canvas backing stores
134
- // re-scale when the window moves between monitors or the browser zooms
135
- const devicePixelRatio = useDevicePixelRatio()
136
- useEffect(() => {
137
- model.setHighResScaleFactor(devicePixelRatio)
138
- }, [model, devicePixelRatio])
139
-
140
132
  return (
141
133
  <div>
142
134
  {viewInitialized ? (
@@ -1,4 +1,4 @@
1
- import type { MsaViewModel } from '../../model.ts'
1
+ import type { MsaViewModel } from '../model.ts'
2
2
 
3
3
  // The slice of leaves whose rows intersect a block of height blockSizeY at
4
4
  // vertical offset offsetY, padded by one row on each side.
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react'
1
+ import React from 'react'
2
2
 
3
3
  import { BaseTooltip } from '@jbrowse/core/ui'
4
4
  import { observer } from 'mobx-react'
@@ -6,26 +6,12 @@ import { observer } from 'mobx-react'
6
6
  import ColumnStats from './ColumnStats.tsx'
7
7
  import { renderBoxFeatureCanvasBlock } from './renderBoxFeatureCanvasBlock.ts'
8
8
  import { renderMSABlock } from './renderMSABlock.ts'
9
+ import { useMsaBlockMouse } from './useMsaBlockMouse.ts'
9
10
  import { useCanvasAutorun } from '../../useCanvasAutorun.ts'
10
11
  import { useColorContrast } from '../../useColorContrast.ts'
11
12
 
12
13
  import type { MsaViewModel } from '../../model.ts'
13
14
 
14
- function eventToColRow(
15
- event: React.MouseEvent,
16
- el: HTMLCanvasElement,
17
- offsetX: number,
18
- offsetY: number,
19
- colWidth: number,
20
- rowHeight: number,
21
- ) {
22
- const { left, top } = el.getBoundingClientRect()
23
- return {
24
- col: Math.floor((event.clientX - left + offsetX) / colWidth),
25
- row: Math.floor((event.clientY - top + offsetY) / rowHeight),
26
- }
27
- }
28
-
29
15
  const MSACanvasBlock = observer(function ({
30
16
  model,
31
17
  offsetX,
@@ -35,21 +21,19 @@ const MSACanvasBlock = observer(function ({
35
21
  offsetX: number
36
22
  offsetY: number
37
23
  }) {
38
- const {
39
- colWidth,
40
- rowHeight,
41
- scrollY,
42
- scrollX,
43
- colorScheme,
44
- blockSize,
45
- mouseClickCol,
46
- mouseClickRow,
47
- highResScaleFactor,
48
- } = model
24
+ const { scrollY, scrollX, colorScheme, blockSize, highResScaleFactor } = model
49
25
  const { theme, contrastScheme } = useColorContrast(colorScheme)
26
+ const { tooltipPoint, onMouseMove, onClick, onMouseLeave } = useMsaBlockMouse(
27
+ {
28
+ model,
29
+ offsetX,
30
+ offsetY,
31
+ },
32
+ )
50
33
 
51
34
  const ref = useCanvasAutorun(
52
35
  ctx => {
36
+ const { blockSize, highResScaleFactor, actuallyShowDomains } = model
53
37
  ctx.resetTransform()
54
38
  ctx.clearRect(
55
39
  0,
@@ -57,7 +41,7 @@ const MSACanvasBlock = observer(function ({
57
41
  blockSize * highResScaleFactor,
58
42
  blockSize * highResScaleFactor,
59
43
  )
60
- if (model.actuallyShowDomains) {
44
+ if (actuallyShowDomains) {
61
45
  renderBoxFeatureCanvasBlock({
62
46
  ctx,
63
47
  offsetX,
@@ -74,18 +58,9 @@ const MSACanvasBlock = observer(function ({
74
58
  model,
75
59
  })
76
60
  },
77
- [
78
- model,
79
- offsetX,
80
- offsetY,
81
- theme,
82
- blockSize,
83
- highResScaleFactor,
84
- contrastScheme,
85
- ],
61
+ [model, offsetX, offsetY, theme, contrastScheme],
86
62
  )
87
63
 
88
- const [mousePosition, setMousePosition] = useState<{ x: number; y: number }>()
89
64
  const { hoveredInsertion, mouseOverDomains, showColumnStats } = model
90
65
 
91
66
  return (
@@ -94,58 +69,16 @@ const MSACanvasBlock = observer(function ({
94
69
  ref={ref}
95
70
  onMouseMove={event => {
96
71
  if (ref.current) {
97
- const { col, row } = eventToColRow(
98
- event,
99
- ref.current,
100
- offsetX,
101
- offsetY,
102
- colWidth,
103
- rowHeight,
104
- )
105
- // Only set mouse position if within valid MSA bounds
106
- if (
107
- col >= 0 &&
108
- col < model.numColumns &&
109
- row >= 0 &&
110
- row < model.numRows
111
- ) {
112
- model.setMousePos(col, row)
113
- } else {
114
- model.setMousePos(undefined, undefined)
115
- }
116
- // only track client coords when there's something to show in the
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)
124
- setMousePosition(
125
- hasTooltip ? { x: event.clientX, y: event.clientY } : undefined,
126
- )
72
+ onMouseMove(event, ref.current)
127
73
  }
128
74
  }}
129
75
  onClick={event => {
130
76
  if (ref.current) {
131
- const { col, row } = eventToColRow(
132
- event,
133
- ref.current,
134
- offsetX,
135
- offsetY,
136
- colWidth,
137
- rowHeight,
138
- )
139
- if (col === mouseClickCol && row === mouseClickRow) {
140
- model.setMouseClickPos(undefined, undefined)
141
- } else {
142
- model.setMouseClickPos(col, row)
143
- }
77
+ onClick(event, ref.current)
144
78
  }
145
79
  }}
146
80
  onMouseLeave={() => {
147
- model.setMousePos()
148
- setMousePosition(undefined)
81
+ onMouseLeave()
149
82
  }}
150
83
  width={blockSize * highResScaleFactor}
151
84
  height={blockSize * highResScaleFactor}
@@ -157,35 +90,34 @@ const MSACanvasBlock = observer(function ({
157
90
  height: blockSize,
158
91
  }}
159
92
  />
160
- {hoveredInsertion && mousePosition ? (
161
- <BaseTooltip
162
- clientPoint={{ x: mousePosition.x, y: mousePosition.y + 15 }}
163
- >
164
- Insertion ({hoveredInsertion.letters.length}
165
- {model.sequenceType === 'amino' ? 'aa' : 'bp'}):{' '}
166
- {hoveredInsertion.letters.length > 20
167
- ? `${hoveredInsertion.letters.slice(0, 20)}...`
168
- : hoveredInsertion.letters}
169
- </BaseTooltip>
170
- ) : null}
171
- {!hoveredInsertion &&
172
- mousePosition &&
173
- (mouseOverDomains.length > 0 || showColumnStats) ? (
93
+ {tooltipPoint ? (
174
94
  <BaseTooltip
175
- clientPoint={{ x: mousePosition.x, y: mousePosition.y + 15 }}
95
+ clientPoint={{ x: tooltipPoint.x, y: tooltipPoint.y + 15 }}
176
96
  >
177
- {mouseOverDomains.map(d => (
178
- <div key={`${d.accession}-${d.start}-${d.end}`}>
179
- <b>{d.name}</b> ({d.accession}) {d.start}-{d.end}
180
- {d.description ? (
181
- <>
182
- <br />
183
- {d.description}
184
- </>
185
- ) : null}
186
- </div>
187
- ))}
188
- {showColumnStats ? <ColumnStats model={model} /> : null}
97
+ {hoveredInsertion ? (
98
+ <>
99
+ Insertion ({hoveredInsertion.letters.length}
100
+ {model.sequenceType === 'amino' ? 'aa' : 'bp'}):{' '}
101
+ {hoveredInsertion.letters.length > 20
102
+ ? `${hoveredInsertion.letters.slice(0, 20)}...`
103
+ : hoveredInsertion.letters}
104
+ </>
105
+ ) : (
106
+ <>
107
+ {mouseOverDomains.map(d => (
108
+ <div key={`${d.accession}-${d.start}-${d.end}`}>
109
+ <b>{d.name}</b> ({d.accession}) {d.start}-{d.end}
110
+ {d.description ? (
111
+ <>
112
+ <br />
113
+ {d.description}
114
+ </>
115
+ ) : null}
116
+ </div>
117
+ ))}
118
+ {showColumnStats ? <ColumnStats model={model} /> : null}
119
+ </>
120
+ )}
189
121
  </BaseTooltip>
190
122
  ) : null}
191
123
  </>
@@ -1,5 +1,5 @@
1
- import { getVisibleLeaves } from './getVisibleLeaves.ts'
2
1
  import { subFeatureRowHeight } from '../../constants.ts'
2
+ import { getVisibleLeaves } from '../getVisibleLeaves.ts'
3
3
 
4
4
  import type { HierarchyNode } from '../../hierarchy.ts'
5
5
  import type { MsaViewModel } from '../../model.ts'
@@ -12,6 +12,7 @@ export function renderBoxFeatureCanvasBlock({
12
12
  offsetY,
13
13
  ctx,
14
14
  highResScaleFactorOverride,
15
+ blockSizeXOverride,
15
16
  blockSizeYOverride,
16
17
  }: {
17
18
  offsetX: number
@@ -19,22 +20,24 @@ export function renderBoxFeatureCanvasBlock({
19
20
  model: MsaViewModel
20
21
  ctx: RenderCtx
21
22
  highResScaleFactorOverride?: number
23
+ blockSizeXOverride?: number
22
24
  blockSizeYOverride?: number
23
25
  }) {
24
26
  const { blockSize, rowHeight, highResScaleFactor, showDomains } = model
25
27
  if (showDomains) {
26
28
  const k = highResScaleFactorOverride ?? highResScaleFactor
29
+ const bx = blockSizeXOverride ?? blockSize
27
30
  const by = blockSizeYOverride ?? blockSize
28
31
  ctx.resetTransform()
29
32
  ctx.scale(k, k)
30
33
  ctx.translate(-offsetX, rowHeight / 2 - offsetY)
31
34
 
32
- const visibleLeaves = getVisibleLeaves({ model, offsetY, blockSizeY: by })
33
-
34
35
  drawTiles({
35
36
  model,
36
37
  ctx,
37
- visibleLeaves,
38
+ visibleLeaves: getVisibleLeaves({ model, offsetY, blockSizeY: by }),
39
+ offsetX,
40
+ blockWidth: bx,
38
41
  })
39
42
  }
40
43
  }
@@ -43,10 +46,14 @@ function drawTiles({
43
46
  model,
44
47
  ctx,
45
48
  visibleLeaves,
49
+ offsetX,
50
+ blockWidth,
46
51
  }: {
47
52
  model: MsaViewModel
48
53
  ctx: RenderCtx
49
54
  visibleLeaves: HierarchyNode<NodeWithIdsAndLength>[]
55
+ offsetX: number
56
+ blockWidth: number
50
57
  }) {
51
58
  const {
52
59
  subFeatureRows,
@@ -56,31 +63,31 @@ function drawTiles({
56
63
  strokePalette,
57
64
  segmentLabels,
58
65
  showMsaLetters,
59
- tidyFilteredGatheredInterProAnnotations,
66
+ domainBands,
60
67
  } = model
61
68
  const h = subFeatureRows ? subFeatureRowHeight : rowHeight
62
69
  // exon numbers label the bands only when residue letters aren't drawn (zoomed
63
70
  // out); when letters show, the alternating shades alone mark the boundaries
64
71
  // and a number would collide with the sequence
65
72
  const drawSegmentLabels = !showMsaLetters && !subFeatureRows && h >= 9
73
+ // gene arrow heads stick out one row-height past the band, so pad the cull
74
+ // window enough that a band just outside the block still draws its head
75
+ const cull = h + colWidth
76
+ const xMin = offsetX - cull
77
+ const xMax = offsetX + blockWidth + cull
66
78
 
67
79
  for (let i = 0, l1 = visibleLeaves.length; i < l1; i++) {
68
80
  const node = visibleLeaves[i]!
69
- const { name } = node.data
70
81
  const y = node.x!
71
- const entry = tidyFilteredGatheredInterProAnnotations[name]
82
+ const bands = domainBands.get(node.data.name)
72
83
 
73
- if (entry) {
74
- for (let j = 0, l2 = entry.length; j < l2; j++) {
75
- const { start, end, accession, strand } = entry[j]!
76
- // Convert sequence positions to visible column positions
77
- // seqPos is 1-based from InterPro, so subtract 1 for 0-based
78
- const m1 = model.seqPosToVisibleCol(name, start - 1)
79
- const m2 = model.seqPosToVisibleCol(name, end)
80
- if (m1 !== undefined && m2 !== undefined) {
81
- const x = m1 * colWidth
82
- const t = y - rowHeight + (subFeatureRows ? j * h : 0)
83
- const lw = colWidth * (m2 - m1)
84
+ if (bands) {
85
+ for (const { annotation, startCol, endCol, stackIndex } of bands) {
86
+ const { accession, strand } = annotation
87
+ const x = startCol * colWidth
88
+ const lw = colWidth * (endCol - startCol)
89
+ if (x + lw >= xMin && x <= xMax) {
90
+ const t = y - rowHeight + (subFeatureRows ? stackIndex * h : 0)
84
91
  ctx.fillStyle = fillPalette[accession]!
85
92
  ctx.strokeStyle = strokePalette[accession]!
86
93
  if (strand === undefined) {