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.
- package/bundle/index.js +95 -95
- package/bundle/index.js.map +4 -4
- package/dist/clustalX.d.ts +4 -3
- package/dist/clustalX.js +25 -4
- package/dist/clustalX.js.map +1 -1
- package/dist/columnCounts.d.ts +37 -0
- package/dist/columnCounts.js +148 -0
- package/dist/columnCounts.js.map +1 -0
- package/dist/components/MSAView.js +1 -8
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/getVisibleLeaves.d.ts +6 -0
- package/dist/components/getVisibleLeaves.js.map +1 -0
- package/dist/components/msa/MSACanvasBlock.js +17 -54
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +2 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js +21 -19
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSABlock.js +97 -96
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +9 -32
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/msa/useMsaBlockMouse.d.ts +23 -0
- package/dist/components/msa/useMsaBlockMouse.js +45 -0
- package/dist/components/msa/useMsaBlockMouse.js.map +1 -0
- package/dist/components/tree/TreeCanvas.js +14 -19
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +40 -111
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.d.ts +2 -0
- package/dist/components/tree/renderTreeCanvas.js +15 -3
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/components/tree/useTreeHover.d.ts +24 -0
- package/dist/components/tree/useTreeHover.js +44 -0
- package/dist/components/tree/useTreeHover.js.map +1 -0
- package/dist/hierarchy.js +15 -9
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/model/msaModel.js +4 -3
- package/dist/model/msaModel.js.map +1 -1
- package/dist/model/treeModel.js +9 -8
- package/dist/model/treeModel.js.map +1 -1
- package/dist/model.d.ts +52 -10
- package/dist/model.js +208 -93
- package/dist/model.js.map +1 -1
- package/dist/propertyConservation.d.ts +2 -1
- package/dist/propertyConservation.js +36 -25
- package/dist/propertyConservation.js.map +1 -1
- package/dist/seqPosToGlobalCol.d.ts +7 -0
- package/dist/seqPosToGlobalCol.js +17 -0
- package/dist/seqPosToGlobalCol.js.map +1 -1
- package/dist/stripDefault.d.ts +21 -0
- package/dist/stripDefault.js +26 -0
- package/dist/stripDefault.js.map +1 -0
- package/dist/types.d.ts +16 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/clustalX.test.ts +9 -11
- package/src/clustalX.ts +27 -28
- package/src/columnCounts.test.ts +58 -0
- package/src/columnCounts.ts +170 -0
- package/src/components/MSAView.tsx +1 -9
- package/src/components/{msa/getVisibleLeaves.ts → getVisibleLeaves.ts} +1 -1
- package/src/components/msa/MSACanvasBlock.tsx +42 -110
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +25 -18
- package/src/components/msa/renderMSABlock.ts +113 -125
- package/src/components/msa/renderMSAMouseover.ts +11 -34
- package/src/components/msa/useMsaBlockMouse.ts +81 -0
- package/src/components/tree/TreeCanvas.tsx +16 -23
- package/src/components/tree/TreeCanvasBlock.tsx +62 -143
- package/src/components/tree/renderTreeCanvas.test.ts +100 -190
- package/src/components/tree/renderTreeCanvas.ts +14 -3
- package/src/components/tree/useTreeHover.ts +69 -0
- package/src/domainBands.test.ts +98 -0
- package/src/hierarchy.ts +15 -9
- package/src/index.ts +7 -0
- package/src/model/msaModel.ts +4 -6
- package/src/model/treeModel.ts +9 -14
- package/src/model.ts +241 -112
- package/src/propertyConservation.test.ts +2 -13
- package/src/propertyConservation.ts +40 -31
- package/src/rowSpace.test.ts +46 -0
- package/src/seqPosToGlobalCol.ts +18 -0
- package/src/stripDefault.test.ts +48 -0
- package/src/stripDefault.ts +35 -0
- package/src/types.ts +23 -0
- package/src/version.ts +1 -1
- package/dist/components/msa/getVisibleLeaves.d.ts +0 -6
- package/dist/components/msa/getVisibleLeaves.js.map +0 -1
- package/dist/useDevicePixelRatio.d.ts +0 -1
- package/dist/useDevicePixelRatio.js +0 -27
- package/dist/useDevicePixelRatio.js.map +0 -1
- package/src/useDevicePixelRatio.ts +0 -29
- /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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
123
|
-
|
|
143
|
+
subFeatureRows,
|
|
144
|
+
domainBandsByStart,
|
|
124
145
|
} = model
|
|
125
146
|
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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 (
|
|
61
|
-
|
|
62
|
-
|
|
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 (
|
|
49
|
+
if (hoveredRowIndices.length > 0) {
|
|
70
50
|
ctx.fillStyle = multiRowHoverColor
|
|
71
|
-
for (const
|
|
72
|
-
|
|
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 (
|
|
58
|
+
if (highlightedColumnRuns.length > 0) {
|
|
82
59
|
ctx.lineWidth = 2
|
|
83
|
-
for (const { start, end } of
|
|
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
|
-
|
|
33
|
+
mouseRow,
|
|
34
|
+
referenceRowIndex,
|
|
35
|
+
hoveredRowIndices,
|
|
37
36
|
} = model
|
|
38
37
|
ctx.resetTransform()
|
|
39
38
|
ctx.clearRect(0, 0, w, h)
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 (
|
|
46
|
+
if (referenceRowIndex !== undefined) {
|
|
50
47
|
ctx.fillStyle = referenceColor
|
|
51
|
-
fillRow(
|
|
48
|
+
fillRow(referenceRowIndex)
|
|
52
49
|
}
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
fillRow(descendantName)
|
|
58
|
-
}
|
|
51
|
+
ctx.fillStyle = treeHoverColor
|
|
52
|
+
for (const index of hoveredRowIndices) {
|
|
53
|
+
fillRow(index)
|
|
59
54
|
}
|
|
60
|
-
|
|
61
55
|
if (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
!
|
|
56
|
+
mouseRow !== undefined &&
|
|
57
|
+
mouseRow !== referenceRowIndex &&
|
|
58
|
+
!hoveredRowIndices.includes(mouseRow)
|
|
65
59
|
) {
|
|
66
|
-
|
|
67
|
-
fillRow(mouseOverRowName)
|
|
60
|
+
fillRow(mouseRow)
|
|
68
61
|
}
|
|
69
62
|
}
|
|
70
63
|
},
|