react-msaview 6.0.0 → 6.1.1
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 +98 -98
- package/bundle/index.js.map +4 -4
- package/dist/columnCounts.d.ts +7 -0
- package/dist/columnCounts.js +18 -0
- package/dist/columnCounts.js.map +1 -1
- package/dist/components/DragHandle.d.ts +25 -0
- package/dist/components/DragHandle.js +49 -0
- package/dist/components/DragHandle.js.map +1 -0
- package/dist/components/ResizeHandles.d.ts +4 -2
- package/dist/components/ResizeHandles.js +29 -32
- package/dist/components/ResizeHandles.js.map +1 -1
- package/dist/components/VerticalScrollbar.js +2 -2
- package/dist/components/VerticalScrollbar.js.map +1 -1
- package/dist/components/dialogs/InterProScanDialog.js +52 -129
- package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
- package/dist/components/header/settingsMenuItems.js +16 -1
- package/dist/components/header/settingsMenuItems.js.map +1 -1
- package/dist/components/minimap/Minimap.js +32 -4
- package/dist/components/minimap/Minimap.js.map +1 -1
- package/dist/components/msa/MSACanvas.js +8 -2
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +12 -3
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/msaRaster.d.ts +63 -0
- package/dist/components/msa/msaRaster.js +252 -0
- package/dist/components/msa/msaRaster.js.map +1 -0
- package/dist/components/msa/renderMSABlock.d.ts +2 -1
- package/dist/components/msa/renderMSABlock.js +13 -21
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/tileColor.d.ts +6 -0
- package/dist/components/msa/tileColor.js +35 -0
- package/dist/components/msa/tileColor.js.map +1 -0
- package/dist/components/renderCtx.d.ts +1 -1
- package/dist/components/tracks/TrackBlocks.d.ts +14 -0
- package/dist/components/tracks/TrackBlocks.js +65 -0
- package/dist/components/tracks/TrackBlocks.js.map +1 -0
- package/dist/components/tracks/{renderTracksSvg.d.ts → drawTracks.d.ts} +22 -10
- package/dist/components/tracks/drawTracks.js +193 -0
- package/dist/components/tracks/drawTracks.js.map +1 -0
- package/dist/components/tree/TreeCanvas.js +8 -2
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +7 -4
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +27 -21
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +8 -0
- package/dist/constants.js.map +1 -1
- package/dist/model/DataModel.d.ts +1 -4
- package/dist/model/DataModel.js +8 -10
- package/dist/model/DataModel.js.map +1 -1
- package/dist/model/treeModel.d.ts +1 -1
- package/dist/model/treeModel.js +1 -1
- package/dist/model.d.ts +36 -7
- package/dist/model.js +113 -58
- package/dist/model.js.map +1 -1
- package/dist/neighborJoining.js +34 -18
- package/dist/neighborJoining.js.map +1 -1
- package/dist/renderTestEnv.d.ts +43 -0
- package/dist/renderTestEnv.js +88 -0
- package/dist/renderTestEnv.js.map +1 -0
- package/dist/renderToSvg.js +1 -1
- package/dist/renderToSvg.js.map +1 -1
- package/dist/sequenceLogo.d.ts +28 -0
- package/dist/sequenceLogo.js +47 -0
- package/dist/sequenceLogo.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/columnCounts.test.ts +15 -0
- package/src/columnCounts.ts +19 -0
- package/src/components/DragHandle.tsx +81 -0
- package/src/components/ResizeHandles.tsx +46 -54
- package/src/components/VerticalScrollbar.tsx +3 -2
- package/src/components/dialogs/InterProScanDialog.tsx +54 -131
- package/src/components/header/settingsMenuItems.ts +19 -1
- package/src/components/minimap/Minimap.tsx +47 -4
- package/src/components/msa/MSACanvas.tsx +30 -10
- package/src/components/msa/MSACanvasBlock.tsx +13 -3
- package/src/components/msa/msaRaster.test.ts +145 -0
- package/src/components/msa/msaRaster.ts +350 -0
- package/src/components/msa/renderMSABlock.ts +15 -20
- package/src/components/msa/tileColor.ts +37 -0
- package/src/components/renderCtx.ts +2 -0
- package/src/components/tracks/TrackBlocks.tsx +111 -0
- package/src/components/tracks/drawTracks.ts +323 -0
- package/src/components/tree/TreeCanvas.tsx +16 -4
- package/src/components/tree/TreeCanvasBlock.tsx +18 -11
- package/src/components/tree/renderTreeCanvas.test.ts +55 -0
- package/src/components/tree/renderTreeCanvas.ts +28 -19
- package/src/constants.ts +10 -0
- package/src/impgRender.test.tsx +2 -57
- package/src/largeTree.test.ts +16 -2
- package/src/model/DataModel.ts +11 -11
- package/src/model/treeModel.ts +1 -1
- package/src/model.ts +128 -60
- package/src/neighborJoining.ts +36 -18
- package/src/renderDomainsSvg.test.tsx +2 -57
- package/src/renderMinimapSvg.test.tsx +2 -56
- package/src/renderSequenceLogoSvg.test.tsx +176 -0
- package/src/renderTestEnv.ts +96 -0
- package/src/renderToSvg.tsx +1 -1
- package/src/sequenceLogo.test.ts +88 -0
- package/src/sequenceLogo.ts +63 -0
- package/src/types.ts +7 -1
- package/src/version.ts +1 -1
- package/dist/components/ConservationTrack.d.ts +0 -8
- package/dist/components/ConservationTrack.js +0 -49
- package/dist/components/ConservationTrack.js.map +0 -1
- package/dist/components/ScrollbarThumb.d.ts +0 -14
- package/dist/components/ScrollbarThumb.js +0 -28
- package/dist/components/ScrollbarThumb.js.map +0 -1
- package/dist/components/TextTrack.d.ts +0 -8
- package/dist/components/TextTrack.js +0 -58
- package/dist/components/TextTrack.js.map +0 -1
- package/dist/components/tracks/renderTracksSvg.js +0 -126
- package/dist/components/tracks/renderTracksSvg.js.map +0 -1
- package/src/components/ConservationTrack.tsx +0 -99
- package/src/components/ScrollbarThumb.tsx +0 -48
- package/src/components/TextTrack.tsx +0 -103
- package/src/components/tracks/renderTracksSvg.ts +0 -247
|
@@ -2,6 +2,7 @@ import { setFontSize } from '../../setFontSize.ts'
|
|
|
2
2
|
import { adjustColorForContrast } from '../../util.ts'
|
|
3
3
|
import { getVisibleLeaves } from '../getVisibleLeaves.ts'
|
|
4
4
|
import { domainBandCursor } from './domainBandCursor.ts'
|
|
5
|
+
import { tileColorFn } from './tileColor.ts'
|
|
5
6
|
import { visibleColRange } from './visibleColRange.ts'
|
|
6
7
|
|
|
7
8
|
import type { HierarchyNode } from '../../hierarchy.ts'
|
|
@@ -20,6 +21,7 @@ export function renderMSABlock({
|
|
|
20
21
|
highResScaleFactorOverride,
|
|
21
22
|
blockSizeXOverride,
|
|
22
23
|
blockSizeYOverride,
|
|
24
|
+
rasterTiles,
|
|
23
25
|
}: {
|
|
24
26
|
offsetX: number
|
|
25
27
|
offsetY: number
|
|
@@ -30,6 +32,7 @@ export function renderMSABlock({
|
|
|
30
32
|
highResScaleFactorOverride?: number
|
|
31
33
|
blockSizeXOverride?: number
|
|
32
34
|
blockSizeYOverride?: number
|
|
35
|
+
rasterTiles?: boolean
|
|
33
36
|
}) {
|
|
34
37
|
const {
|
|
35
38
|
colWidth,
|
|
@@ -73,6 +76,9 @@ export function renderMSABlock({
|
|
|
73
76
|
// when domains are shown the background tiles come from
|
|
74
77
|
// renderBoxFeatureCanvasBlock, so only the letters are drawn here
|
|
75
78
|
drawTiles: !actuallyShowDomains,
|
|
79
|
+
// ...and when the caller has already laid the raster down, the tiles are
|
|
80
|
+
// there but nothing here has to paint them
|
|
81
|
+
rasterTiles,
|
|
76
82
|
})
|
|
77
83
|
drawInsertionIndicators({
|
|
78
84
|
model,
|
|
@@ -84,23 +90,6 @@ export function renderMSABlock({
|
|
|
84
90
|
ctx.resetTransform()
|
|
85
91
|
}
|
|
86
92
|
|
|
87
|
-
// Per-cell tile color for the active scheme. The scheme is fixed for the whole
|
|
88
|
-
// block, so resolve which rule applies once rather than re-testing the scheme
|
|
89
|
-
// name inside the innermost loop.
|
|
90
|
-
function tileColorFn(model: MsaViewModel) {
|
|
91
|
-
const { colorSchemeName, colorScheme, colClustalX, colConsensus } = model
|
|
92
|
-
if (colorSchemeName === 'clustalx_protein_dynamic') {
|
|
93
|
-
return (col: number, letter: string) => colClustalX[col]![letter]
|
|
94
|
-
}
|
|
95
|
-
if (colorSchemeName === 'percent_identity_dynamic') {
|
|
96
|
-
return (col: number, letter: string) => {
|
|
97
|
-
const consensus = colConsensus[col]!
|
|
98
|
-
return letter === consensus.letter ? consensus.color : undefined
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return (_col: number, letter: string) => colorScheme[letter]
|
|
102
|
-
}
|
|
103
|
-
|
|
104
93
|
// Letter color to use over a domain box, keyed by accession. Domain fills come
|
|
105
94
|
// from a categorical palette that has no fixed lightness, so ask the theme for a
|
|
106
95
|
// readable text color per fill instead of assuming dark-on-light.
|
|
@@ -123,6 +112,7 @@ function drawTilesAndText({
|
|
|
123
112
|
xEnd,
|
|
124
113
|
referenceSeq,
|
|
125
114
|
drawTiles,
|
|
115
|
+
rasterTiles,
|
|
126
116
|
}: {
|
|
127
117
|
model: MsaViewModel
|
|
128
118
|
theme: Theme
|
|
@@ -133,6 +123,7 @@ function drawTilesAndText({
|
|
|
133
123
|
xEnd: number
|
|
134
124
|
referenceSeq: string | null | undefined
|
|
135
125
|
drawTiles: boolean
|
|
126
|
+
rasterTiles?: boolean
|
|
136
127
|
}) {
|
|
137
128
|
const {
|
|
138
129
|
bgColor,
|
|
@@ -146,7 +137,11 @@ function drawTilesAndText({
|
|
|
146
137
|
} = model
|
|
147
138
|
|
|
148
139
|
const tiles = drawTiles && bgColor
|
|
149
|
-
|
|
140
|
+
const paintTiles = tiles && !rasterTiles
|
|
141
|
+
if (paintTiles || showMsaLetters) {
|
|
142
|
+
// over raster tiles the letters take their color from contrastScheme, so
|
|
143
|
+
// the scheme lookup per cell is pure waste
|
|
144
|
+
const needsColor = paintTiles || !tiles
|
|
150
145
|
const tileColor = tileColorFn(model)
|
|
151
146
|
const offsetXAligned = xStart * colWidth
|
|
152
147
|
const halfColWidth = colWidth / 2
|
|
@@ -178,9 +173,9 @@ function drawTilesAndText({
|
|
|
178
173
|
const x = j * colWidth + offsetXAligned
|
|
179
174
|
const isMatchingReference =
|
|
180
175
|
referenceSeq && name !== relativeTo && letter === referenceSeq[j]
|
|
181
|
-
const color = tileColor(col, letter)
|
|
176
|
+
const color = needsColor ? tileColor(col, letter) : undefined
|
|
182
177
|
|
|
183
|
-
if (
|
|
178
|
+
if (paintTiles) {
|
|
184
179
|
ctx.fillStyle = isMatchingReference
|
|
185
180
|
? theme.palette.action.hover
|
|
186
181
|
: color || theme.palette.background.default
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { MsaViewModel } from '../../model.ts'
|
|
2
|
+
|
|
3
|
+
// Per-cell tile color for the active scheme. The scheme is fixed for the whole
|
|
4
|
+
// block, so resolve which rule applies once rather than re-testing the scheme
|
|
5
|
+
// name inside the innermost loop. Reading only the column table the active
|
|
6
|
+
// scheme needs also keeps the other one -- a per-column map for every column of
|
|
7
|
+
// the alignment -- from being computed at all.
|
|
8
|
+
export function tileColorFn(model: MsaViewModel) {
|
|
9
|
+
const { colorSchemeName } = model
|
|
10
|
+
if (colorSchemeName === 'clustalx_protein_dynamic') {
|
|
11
|
+
const { colClustalX } = model
|
|
12
|
+
return (col: number, letter: string) => colClustalX[col]![letter]
|
|
13
|
+
}
|
|
14
|
+
if (colorSchemeName === 'percent_identity_dynamic') {
|
|
15
|
+
const { colConsensus } = model
|
|
16
|
+
return (col: number, letter: string) => {
|
|
17
|
+
const consensus = colConsensus[col]!
|
|
18
|
+
return letter === consensus.letter ? consensus.color : undefined
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const { colorScheme } = model
|
|
22
|
+
return (_col: number, letter: string) => colorScheme[letter]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The column table the active scheme colors from, or undefined for the static
|
|
26
|
+
// schemes. Identity of this plus the scheme name is what a cache of rendered
|
|
27
|
+
// colors has to key on.
|
|
28
|
+
export function tileColorTable(model: MsaViewModel) {
|
|
29
|
+
const { colorSchemeName } = model
|
|
30
|
+
if (colorSchemeName === 'clustalx_protein_dynamic') {
|
|
31
|
+
return model.colClustalX
|
|
32
|
+
}
|
|
33
|
+
if (colorSchemeName === 'percent_identity_dynamic') {
|
|
34
|
+
return model.colConsensus
|
|
35
|
+
}
|
|
36
|
+
return model.colorScheme
|
|
37
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { observer } from 'mobx-react'
|
|
4
|
+
|
|
5
|
+
import { useCanvasAutorun } from '../../useCanvasAutorun.ts'
|
|
6
|
+
import { useColorContrast } from '../../useColorContrast.ts'
|
|
7
|
+
import { TrackResizeHandle } from '../ResizeHandles.tsx'
|
|
8
|
+
import { drawTrackBlock } from './drawTracks.ts'
|
|
9
|
+
|
|
10
|
+
import type { MsaViewModel } from '../../model.ts'
|
|
11
|
+
import type { BasicTrack } from '../../types.ts'
|
|
12
|
+
|
|
13
|
+
const TrackBlock = observer(function ({
|
|
14
|
+
model,
|
|
15
|
+
track,
|
|
16
|
+
offsetX,
|
|
17
|
+
}: {
|
|
18
|
+
model: MsaViewModel
|
|
19
|
+
track: BasicTrack
|
|
20
|
+
offsetX: number
|
|
21
|
+
}) {
|
|
22
|
+
const { blockSize, colorScheme, highResScaleFactor } = model
|
|
23
|
+
const { height, customColorScheme } = track.model
|
|
24
|
+
const { theme, contrastScheme } = useColorContrast(
|
|
25
|
+
customColorScheme ?? colorScheme,
|
|
26
|
+
)
|
|
27
|
+
const canvasWidth = blockSize * highResScaleFactor
|
|
28
|
+
const canvasHeight = height * highResScaleFactor
|
|
29
|
+
|
|
30
|
+
const ref = useCanvasAutorun({
|
|
31
|
+
draw: ctx => {
|
|
32
|
+
ctx.resetTransform()
|
|
33
|
+
ctx.clearRect(0, 0, canvasWidth, canvasHeight)
|
|
34
|
+
drawTrackBlock({ model, ctx, track, offsetX, theme, contrastScheme })
|
|
35
|
+
},
|
|
36
|
+
width: canvasWidth,
|
|
37
|
+
height: canvasHeight,
|
|
38
|
+
deps: [model, track, offsetX, theme, contrastScheme],
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<canvas
|
|
43
|
+
ref={ref}
|
|
44
|
+
width={canvasWidth}
|
|
45
|
+
height={canvasHeight}
|
|
46
|
+
style={{
|
|
47
|
+
position: 'absolute',
|
|
48
|
+
left: offsetX,
|
|
49
|
+
width: blockSize,
|
|
50
|
+
height,
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The canvas blocks covering one track's row of the alignment. Every track kind
|
|
58
|
+
* shares this host: what differs between conservation, sequence logo and the
|
|
59
|
+
* Stockholm text tracks is which draw function `drawTrackBlock` dispatches to,
|
|
60
|
+
* not how the blocks are laid out or kept in sync with the horizontal scroll.
|
|
61
|
+
*/
|
|
62
|
+
const TrackBlocks = observer(function ({
|
|
63
|
+
model,
|
|
64
|
+
track,
|
|
65
|
+
}: {
|
|
66
|
+
model: MsaViewModel
|
|
67
|
+
track: BasicTrack
|
|
68
|
+
}) {
|
|
69
|
+
// msaCanvasWidth, not msaAreaWidth: a track row is a legend for the columns
|
|
70
|
+
// directly below it, so it has to stop where the alignment canvas does rather
|
|
71
|
+
// than running on another 20px under the vertical scrollbar -- otherwise the
|
|
72
|
+
// last columns appear in the tracks but not in the alignment, and the hovered
|
|
73
|
+
// -column indicator (sized to the canvas) stops short of the track it crosses
|
|
74
|
+
const { blocksX, msaCanvasWidth, scrollX } = model
|
|
75
|
+
const { kind, height, data } = track.model
|
|
76
|
+
|
|
77
|
+
// a text track with no data draws nothing, and an empty row of its own would
|
|
78
|
+
// still take up vertical space
|
|
79
|
+
if (kind === 'text' && !data) {
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div
|
|
85
|
+
style={{
|
|
86
|
+
position: 'relative',
|
|
87
|
+
height,
|
|
88
|
+
width: msaCanvasWidth,
|
|
89
|
+
overflow: 'hidden',
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{/* one transform for the whole block set, matching MSACanvas */}
|
|
93
|
+
<div
|
|
94
|
+
style={{
|
|
95
|
+
position: 'absolute',
|
|
96
|
+
top: 0,
|
|
97
|
+
left: 0,
|
|
98
|
+
transform: `translateX(${scrollX}px)`,
|
|
99
|
+
willChange: 'transform',
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
{blocksX.map(bx => (
|
|
103
|
+
<TrackBlock key={bx} model={model} track={track} offsetX={bx} />
|
|
104
|
+
))}
|
|
105
|
+
</div>
|
|
106
|
+
<TrackResizeHandle model={model} kind={kind} />
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
export default TrackBlocks
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { columnLogoStack } from '../../sequenceLogo.ts'
|
|
2
|
+
import { setFontSize } from '../../setFontSize.ts'
|
|
3
|
+
import { visibleColRange } from '../msa/visibleColRange.ts'
|
|
4
|
+
|
|
5
|
+
import type { ColumnCounts } from '../../columnCounts.ts'
|
|
6
|
+
import type { MsaViewModel } from '../../model.ts'
|
|
7
|
+
import type { BasicTrack } from '../../types.ts'
|
|
8
|
+
import type { RenderCtx } from '../renderCtx.ts'
|
|
9
|
+
import type { Theme } from '@mui/material'
|
|
10
|
+
|
|
11
|
+
export function drawConservationBars({
|
|
12
|
+
ctx,
|
|
13
|
+
values,
|
|
14
|
+
color,
|
|
15
|
+
colWidth,
|
|
16
|
+
trackHeight,
|
|
17
|
+
offsetX,
|
|
18
|
+
blockSize,
|
|
19
|
+
}: {
|
|
20
|
+
ctx: RenderCtx
|
|
21
|
+
values: number[]
|
|
22
|
+
color: string
|
|
23
|
+
colWidth: number
|
|
24
|
+
trackHeight: number
|
|
25
|
+
offsetX: number
|
|
26
|
+
blockSize: number
|
|
27
|
+
}) {
|
|
28
|
+
const { xStart, xEnd } = visibleColRange({
|
|
29
|
+
offsetX,
|
|
30
|
+
blockWidth: blockSize,
|
|
31
|
+
colWidth,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
ctx.fillStyle = color
|
|
35
|
+
for (let i = xStart; i < xEnd && i < values.length; i++) {
|
|
36
|
+
const value = values[i]!
|
|
37
|
+
const barHeight = value * trackHeight
|
|
38
|
+
const x = i * colWidth
|
|
39
|
+
ctx.fillRect(x, trackHeight - barHeight, colWidth, barHeight)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// the bar-track values live on model getters (kept off the plain track object so
|
|
44
|
+
// the live canvas autorun stays reactive); select the right one by track id
|
|
45
|
+
export function barTrackValues(model: MsaViewModel, trackId: string) {
|
|
46
|
+
return trackId === 'property-conservation'
|
|
47
|
+
? model.propertyConservation
|
|
48
|
+
: model.conservation
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function drawTextTrackContent({
|
|
52
|
+
ctx,
|
|
53
|
+
data,
|
|
54
|
+
colorScheme,
|
|
55
|
+
contrastScheme,
|
|
56
|
+
bgColor,
|
|
57
|
+
textColor,
|
|
58
|
+
colWidth,
|
|
59
|
+
rowHeight,
|
|
60
|
+
offsetX,
|
|
61
|
+
blockSize,
|
|
62
|
+
}: {
|
|
63
|
+
ctx: RenderCtx
|
|
64
|
+
data: string | undefined
|
|
65
|
+
colorScheme: Record<string, string>
|
|
66
|
+
contrastScheme: Record<string, string>
|
|
67
|
+
bgColor: boolean
|
|
68
|
+
textColor: string
|
|
69
|
+
colWidth: number
|
|
70
|
+
rowHeight: number
|
|
71
|
+
offsetX: number
|
|
72
|
+
blockSize: number
|
|
73
|
+
}) {
|
|
74
|
+
const { xStart, xEnd } = visibleColRange({
|
|
75
|
+
offsetX,
|
|
76
|
+
blockWidth: blockSize,
|
|
77
|
+
colWidth,
|
|
78
|
+
})
|
|
79
|
+
const str = data?.slice(xStart, xEnd)
|
|
80
|
+
const drawLetters = rowHeight >= 10 && colWidth >= rowHeight / 2
|
|
81
|
+
|
|
82
|
+
for (let i = 0; str && i < str.length; i++) {
|
|
83
|
+
const letter = str[i]!
|
|
84
|
+
const upper = letter.toUpperCase()
|
|
85
|
+
const color = colorScheme[upper]
|
|
86
|
+
const x = (xStart + i) * colWidth
|
|
87
|
+
const filled = bgColor && !!color
|
|
88
|
+
if (filled) {
|
|
89
|
+
ctx.fillStyle = color!
|
|
90
|
+
ctx.fillRect(x, 0, colWidth, rowHeight)
|
|
91
|
+
}
|
|
92
|
+
if (drawLetters) {
|
|
93
|
+
// a letter on a colored tile needs the tile's contrast color; otherwise it
|
|
94
|
+
// sits on the plain background and takes the theme's text color
|
|
95
|
+
ctx.fillStyle = filled ? (contrastScheme[upper] ?? 'black') : textColor
|
|
96
|
+
ctx.fillText(letter, x + colWidth / 2, rowHeight / 2 + 1)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Cap height as a fraction of the font size. Every logo letter is scaled from
|
|
102
|
+
// one reference font size to the height its frequency earns, and that scale is
|
|
103
|
+
// only right if we know how tall the glyph actually draws. Measuring per letter
|
|
104
|
+
// would be exact but `measureText` reports no vertical box in the SVG export
|
|
105
|
+
// backend, so the ratio is a constant: the cap height of the sans-serif faces
|
|
106
|
+
// both backends use, within a percent or two. It only has to be consistent --
|
|
107
|
+
// the same ratio for every letter is what makes the slices sum to the stack
|
|
108
|
+
// height instead of drifting apart.
|
|
109
|
+
const CAP_HEIGHT_RATIO = 0.72
|
|
110
|
+
// letters below this many pixels are noise, and a stack that thin reads better
|
|
111
|
+
// as the bar chart it approximates
|
|
112
|
+
const MIN_LETTER_PX = 3
|
|
113
|
+
|
|
114
|
+
export function drawSequenceLogo({
|
|
115
|
+
ctx,
|
|
116
|
+
colStats,
|
|
117
|
+
colorScheme,
|
|
118
|
+
maxBits,
|
|
119
|
+
textColor,
|
|
120
|
+
colWidth,
|
|
121
|
+
trackHeight,
|
|
122
|
+
offsetX,
|
|
123
|
+
blockSize,
|
|
124
|
+
}: {
|
|
125
|
+
ctx: RenderCtx
|
|
126
|
+
colStats: ColumnCounts
|
|
127
|
+
colorScheme: Record<string, string>
|
|
128
|
+
maxBits: number
|
|
129
|
+
textColor: string
|
|
130
|
+
colWidth: number
|
|
131
|
+
trackHeight: number
|
|
132
|
+
offsetX: number
|
|
133
|
+
blockSize: number
|
|
134
|
+
}) {
|
|
135
|
+
const { xStart, xEnd } = visibleColRange({
|
|
136
|
+
offsetX,
|
|
137
|
+
blockWidth: blockSize,
|
|
138
|
+
colWidth,
|
|
139
|
+
})
|
|
140
|
+
const end = Math.min(xEnd, colStats.numColumns)
|
|
141
|
+
|
|
142
|
+
// one reference size for every glyph; each letter's own transform takes it
|
|
143
|
+
// from here to the height it earned, so the font is set once for the block
|
|
144
|
+
const referenceFontSize = trackHeight
|
|
145
|
+
setFontSize(ctx, referenceFontSize)
|
|
146
|
+
ctx.textAlign = 'left'
|
|
147
|
+
ctx.textBaseline = 'alphabetic'
|
|
148
|
+
const capHeight = referenceFontSize * CAP_HEIGHT_RATIO
|
|
149
|
+
|
|
150
|
+
for (let col = xStart; col < end; col++) {
|
|
151
|
+
const stack = columnLogoStack(colStats, col, maxBits)
|
|
152
|
+
if (stack.length === 0) {
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
const x = col * colWidth
|
|
156
|
+
// stack up from the baseline: the array is ascending, so the tallest letter
|
|
157
|
+
// lands on top, which is the convention that makes a logo readable at a
|
|
158
|
+
// glance
|
|
159
|
+
let bottom = trackHeight
|
|
160
|
+
for (const { letter, bits } of stack) {
|
|
161
|
+
const h = (bits / maxBits) * trackHeight
|
|
162
|
+
if (h < MIN_LETTER_PX) {
|
|
163
|
+
bottom -= h
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
const width = ctx.measureText(letter).width
|
|
167
|
+
if (width === 0) {
|
|
168
|
+
bottom -= h
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
ctx.fillStyle = colorScheme[letter] ?? textColor
|
|
172
|
+
ctx.save()
|
|
173
|
+
// land the glyph's baseline on the bottom of its slice, then stretch it to
|
|
174
|
+
// fill the slice vertically and the column horizontally
|
|
175
|
+
ctx.translate(x, bottom)
|
|
176
|
+
ctx.scale(colWidth / width, h / capHeight)
|
|
177
|
+
ctx.fillText(letter, 0, 0)
|
|
178
|
+
ctx.restore()
|
|
179
|
+
bottom -= h
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Draw one track's content into a block of the alignment's column space, at
|
|
186
|
+
* `offsetY` within the ctx. Owns the transform, so both callers -- the live
|
|
187
|
+
* canvas blocks and the SVG export -- draw into the same coordinate space, and
|
|
188
|
+
* adding a track kind means a `kind`, a draw function above, and a case here.
|
|
189
|
+
*/
|
|
190
|
+
export function drawTrackBlock({
|
|
191
|
+
model,
|
|
192
|
+
ctx,
|
|
193
|
+
track,
|
|
194
|
+
offsetX,
|
|
195
|
+
offsetY = 0,
|
|
196
|
+
theme,
|
|
197
|
+
contrastScheme,
|
|
198
|
+
blockSizeXOverride,
|
|
199
|
+
highResScaleFactorOverride,
|
|
200
|
+
}: {
|
|
201
|
+
model: MsaViewModel
|
|
202
|
+
ctx: RenderCtx
|
|
203
|
+
track: BasicTrack
|
|
204
|
+
offsetX: number
|
|
205
|
+
offsetY?: number
|
|
206
|
+
theme: Theme
|
|
207
|
+
contrastScheme: Record<string, string>
|
|
208
|
+
blockSizeXOverride?: number
|
|
209
|
+
highResScaleFactorOverride?: number
|
|
210
|
+
}) {
|
|
211
|
+
const {
|
|
212
|
+
blockSize,
|
|
213
|
+
bgColor,
|
|
214
|
+
colWidth,
|
|
215
|
+
colStats,
|
|
216
|
+
colorScheme: modelColorScheme,
|
|
217
|
+
fontSize,
|
|
218
|
+
rowHeight,
|
|
219
|
+
alphabetMaxBits,
|
|
220
|
+
highResScaleFactor,
|
|
221
|
+
} = model
|
|
222
|
+
const {
|
|
223
|
+
id,
|
|
224
|
+
kind,
|
|
225
|
+
height: trackHeight,
|
|
226
|
+
barColor,
|
|
227
|
+
customColorScheme,
|
|
228
|
+
data,
|
|
229
|
+
} = track.model
|
|
230
|
+
const blockSizeX = blockSizeXOverride ?? blockSize
|
|
231
|
+
const k = highResScaleFactorOverride ?? highResScaleFactor
|
|
232
|
+
const textColor = theme.palette.text.primary
|
|
233
|
+
|
|
234
|
+
ctx.resetTransform()
|
|
235
|
+
ctx.scale(k, k)
|
|
236
|
+
ctx.translate(-offsetX, offsetY)
|
|
237
|
+
|
|
238
|
+
switch (kind) {
|
|
239
|
+
case 'bar': {
|
|
240
|
+
drawConservationBars({
|
|
241
|
+
ctx,
|
|
242
|
+
values: barTrackValues(model, id),
|
|
243
|
+
color: barColor ?? 'gray',
|
|
244
|
+
colWidth,
|
|
245
|
+
trackHeight,
|
|
246
|
+
offsetX,
|
|
247
|
+
blockSize: blockSizeX,
|
|
248
|
+
})
|
|
249
|
+
break
|
|
250
|
+
}
|
|
251
|
+
case 'logo': {
|
|
252
|
+
drawSequenceLogo({
|
|
253
|
+
ctx,
|
|
254
|
+
colStats,
|
|
255
|
+
colorScheme: modelColorScheme,
|
|
256
|
+
maxBits: alphabetMaxBits,
|
|
257
|
+
textColor,
|
|
258
|
+
colWidth,
|
|
259
|
+
trackHeight,
|
|
260
|
+
offsetX,
|
|
261
|
+
blockSize: blockSizeX,
|
|
262
|
+
})
|
|
263
|
+
break
|
|
264
|
+
}
|
|
265
|
+
case 'text': {
|
|
266
|
+
ctx.textAlign = 'center'
|
|
267
|
+
setFontSize(ctx, fontSize)
|
|
268
|
+
drawTextTrackContent({
|
|
269
|
+
ctx,
|
|
270
|
+
data,
|
|
271
|
+
colorScheme: customColorScheme ?? modelColorScheme,
|
|
272
|
+
contrastScheme,
|
|
273
|
+
bgColor,
|
|
274
|
+
textColor,
|
|
275
|
+
colWidth,
|
|
276
|
+
rowHeight,
|
|
277
|
+
offsetX,
|
|
278
|
+
blockSize: blockSizeX,
|
|
279
|
+
})
|
|
280
|
+
break
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
ctx.resetTransform()
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Every turned-on track stacked top to bottom, for the SVG export. The live
|
|
289
|
+
* view draws the same content one canvas block at a time -- see TrackBlocks.
|
|
290
|
+
*/
|
|
291
|
+
export function renderAllTracks({
|
|
292
|
+
model,
|
|
293
|
+
ctx,
|
|
294
|
+
offsetX,
|
|
295
|
+
contrastScheme,
|
|
296
|
+
theme,
|
|
297
|
+
blockSizeXOverride,
|
|
298
|
+
highResScaleFactorOverride,
|
|
299
|
+
}: {
|
|
300
|
+
model: MsaViewModel
|
|
301
|
+
ctx: RenderCtx
|
|
302
|
+
offsetX: number
|
|
303
|
+
contrastScheme: Record<string, string>
|
|
304
|
+
theme: Theme
|
|
305
|
+
blockSizeXOverride?: number
|
|
306
|
+
highResScaleFactorOverride?: number
|
|
307
|
+
}) {
|
|
308
|
+
let currentY = 0
|
|
309
|
+
for (const track of model.turnedOnTracks) {
|
|
310
|
+
drawTrackBlock({
|
|
311
|
+
model,
|
|
312
|
+
ctx,
|
|
313
|
+
track,
|
|
314
|
+
offsetX,
|
|
315
|
+
offsetY: currentY,
|
|
316
|
+
theme,
|
|
317
|
+
contrastScheme,
|
|
318
|
+
blockSizeXOverride,
|
|
319
|
+
highResScaleFactorOverride,
|
|
320
|
+
})
|
|
321
|
+
currentY += track.model.height
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -13,7 +13,7 @@ import type { MsaViewModel } from '../../model.ts'
|
|
|
13
13
|
|
|
14
14
|
const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
15
15
|
const ref = useRef<HTMLDivElement>(null)
|
|
16
|
-
const { treeWidth, height, blocksY, treeAreaWidth } = model
|
|
16
|
+
const { treeWidth, height, blocksY, treeAreaWidth, scrollY } = model
|
|
17
17
|
const onScrollY = useCallback(
|
|
18
18
|
(d: number) => {
|
|
19
19
|
model.doScrollY(d)
|
|
@@ -80,9 +80,21 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
80
80
|
width: treeWidth + padding,
|
|
81
81
|
}}
|
|
82
82
|
>
|
|
83
|
-
{
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
{/* one transform for the whole block set, matching MSACanvas: the tree
|
|
84
|
+
scrolls in lockstep with the alignment and on the same compositor move */}
|
|
85
|
+
<div
|
|
86
|
+
style={{
|
|
87
|
+
position: 'absolute',
|
|
88
|
+
top: 0,
|
|
89
|
+
left: 0,
|
|
90
|
+
transform: `translateY(${scrollY}px)`,
|
|
91
|
+
willChange: 'transform',
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
{blocksY.map(block => (
|
|
95
|
+
<TreeCanvasBlock key={block} model={model} offsetY={block} />
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
86
98
|
<canvas
|
|
87
99
|
ref={mouseoverRef}
|
|
88
100
|
width={treeAreaWidth}
|
|
@@ -3,6 +3,7 @@ import React, { useState } from 'react'
|
|
|
3
3
|
import { makeStyles } from '@jbrowse/core/util/tss-react'
|
|
4
4
|
import { useTheme } from '@mui/material'
|
|
5
5
|
import { observer } from 'mobx-react'
|
|
6
|
+
import { createPortal } from 'react-dom'
|
|
6
7
|
|
|
7
8
|
import { useCanvasAutorun } from '../../useCanvasAutorun.ts'
|
|
8
9
|
import TreeBranchMenu from './TreeBranchMenu.tsx'
|
|
@@ -56,7 +57,7 @@ const TreeCanvasBlock = observer(function ({
|
|
|
56
57
|
const { clickMap, hovered, hitTest, onMouseMove, onMouseLeave } =
|
|
57
58
|
useTreeHover({ model, offsetY })
|
|
58
59
|
|
|
59
|
-
const {
|
|
60
|
+
const { treeAreaWidth, blockSize, highResScaleFactor } = model
|
|
60
61
|
const width = treeAreaWidth + padding
|
|
61
62
|
const height = blockSize
|
|
62
63
|
|
|
@@ -82,7 +83,7 @@ const TreeCanvasBlock = observer(function ({
|
|
|
82
83
|
const style = {
|
|
83
84
|
width,
|
|
84
85
|
height,
|
|
85
|
-
top:
|
|
86
|
+
top: offsetY,
|
|
86
87
|
left: 0,
|
|
87
88
|
position: 'absolute',
|
|
88
89
|
} as const
|
|
@@ -144,21 +145,27 @@ const TreeCanvasBlock = observer(function ({
|
|
|
144
145
|
className={classes.hover}
|
|
145
146
|
style={{
|
|
146
147
|
left: hovered.minX,
|
|
147
|
-
top: hovered.minY
|
|
148
|
+
top: hovered.minY,
|
|
148
149
|
width: hovered.maxX - hovered.minX,
|
|
149
150
|
height: hovered.maxY - hovered.minY,
|
|
150
151
|
}}
|
|
151
152
|
/>
|
|
152
153
|
) : null}
|
|
153
154
|
|
|
154
|
-
{
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
155
|
+
{/* portaled: the block set sits under a transform, which would otherwise
|
|
156
|
+
make this fixed tooltip position against the scrolled tree instead of the
|
|
157
|
+
window */}
|
|
158
|
+
{hovered
|
|
159
|
+
? createPortal(
|
|
160
|
+
<div
|
|
161
|
+
className={classes.tooltip}
|
|
162
|
+
style={{ left: hovered.clientX + 12, top: hovered.clientY + 12 }}
|
|
163
|
+
>
|
|
164
|
+
{hovered.name}
|
|
165
|
+
</div>,
|
|
166
|
+
document.body,
|
|
167
|
+
)
|
|
168
|
+
: null}
|
|
162
169
|
</>
|
|
163
170
|
)
|
|
164
171
|
})
|