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.
- 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/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/colorSchemes.docs.test.ts +3 -1
- 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/model/msaModel.ts +4 -3
- package/src/model/treeModel.ts +9 -8
- package/src/model.ts +241 -106
- 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/stripDefaultSnapshot.test.ts +4 -1
- 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
|
@@ -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
|
|
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 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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
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:
|
|
95
|
+
clientPoint={{ x: tooltipPoint.x, y: tooltipPoint.y + 15 }}
|
|
176
96
|
>
|
|
177
|
-
{
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
{
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
|
82
|
+
const bands = domainBands.get(node.data.name)
|
|
72
83
|
|
|
73
|
-
if (
|
|
74
|
-
for (
|
|
75
|
-
const {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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) {
|