react-msaview 6.0.0 → 6.1.0
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 +15 -0
- 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 +5 -2
- 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.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 +18 -0
- 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.ts +5 -1
- 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.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
|
@@ -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
|
})
|
|
@@ -264,6 +264,7 @@ function renderTreeLabels({
|
|
|
264
264
|
marginLeft,
|
|
265
265
|
noTree,
|
|
266
266
|
labelWidthMap,
|
|
267
|
+
labelWidthScale,
|
|
267
268
|
} = model
|
|
268
269
|
const by = blockSizeYOverride ?? blockSize
|
|
269
270
|
// labels only exist for leaves, which are laid out top to bottom, so take the
|
|
@@ -295,8 +296,11 @@ function renderTreeLabels({
|
|
|
295
296
|
xp = getNodeX(node, showBranchLen, tipX, maxDepthToLeaf) ?? 0
|
|
296
297
|
}
|
|
297
298
|
|
|
299
|
+
const measured = labelWidthMap.get(name)
|
|
298
300
|
const width =
|
|
299
|
-
|
|
301
|
+
measured === undefined
|
|
302
|
+
? ctx.measureText(displayName).width
|
|
303
|
+
: measured * labelWidthScale
|
|
300
304
|
|
|
301
305
|
ctx.fillStyle = theme.palette.text.primary
|
|
302
306
|
if (labelsAlignRight) {
|
package/src/constants.ts
CHANGED
|
@@ -31,6 +31,16 @@ export const segmentShades = ['#9fb6d4', '#d4dcea']
|
|
|
31
31
|
export const defaultDrawMsaLetters = true
|
|
32
32
|
export const defaultScrollZoom = false
|
|
33
33
|
|
|
34
|
+
// Cell size floors for drawing residue letters and tree labels. Below these a
|
|
35
|
+
// 500px block holds thousands of glyphs, and fillText -- which no sprite atlas
|
|
36
|
+
// beats, measured -- dominates every zoom frame.
|
|
37
|
+
export const minLetterRowHeight = 8
|
|
38
|
+
export const minLetterColWidth = 5
|
|
39
|
+
|
|
40
|
+
// Tree labels are measured once at this size and scaled to the current font
|
|
41
|
+
// size, so a vertical zoom never re-measures the tree.
|
|
42
|
+
export const labelReferenceFontSize = 16
|
|
43
|
+
|
|
34
44
|
// Zoom limits, px per cell. maxCellSize caps both smooth zoom and the stepwise
|
|
35
45
|
// zoom-in buttons (which otherwise grow unbounded). minColWidth/minRowHeight are
|
|
36
46
|
// the smooth-zoom floors.
|
package/src/impgRender.test.tsx
CHANGED
|
@@ -12,67 +12,12 @@ import { enableStaticRendering } from 'mobx-react'
|
|
|
12
12
|
import { beforeAll, expect, test } from 'vitest'
|
|
13
13
|
|
|
14
14
|
import MSAModelF from './model.ts'
|
|
15
|
+
import { installRenderTestEnv } from './renderTestEnv.ts'
|
|
15
16
|
import { renderToSvg } from './renderToSvg.tsx'
|
|
16
17
|
|
|
17
|
-
class Mat {
|
|
18
|
-
constructor(
|
|
19
|
-
public a = 1,
|
|
20
|
-
public b = 0,
|
|
21
|
-
public c = 0,
|
|
22
|
-
public d = 1,
|
|
23
|
-
public e = 0,
|
|
24
|
-
public f = 0,
|
|
25
|
-
) {}
|
|
26
|
-
multiply(o: Mat) {
|
|
27
|
-
return new Mat(
|
|
28
|
-
this.a * o.a + this.c * o.b,
|
|
29
|
-
this.b * o.a + this.d * o.b,
|
|
30
|
-
this.a * o.c + this.c * o.d,
|
|
31
|
-
this.b * o.c + this.d * o.d,
|
|
32
|
-
this.a * o.e + this.c * o.f + this.e,
|
|
33
|
-
this.b * o.e + this.d * o.f + this.f,
|
|
34
|
-
)
|
|
35
|
-
}
|
|
36
|
-
translate(x: number, y = 0) {
|
|
37
|
-
return this.multiply(new Mat(1, 0, 0, 1, x, y))
|
|
38
|
-
}
|
|
39
|
-
scale(x: number, y = x) {
|
|
40
|
-
return this.multiply(new Mat(x, 0, 0, y, 0, 0))
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
class Pt {
|
|
44
|
-
constructor(
|
|
45
|
-
public x = 0,
|
|
46
|
-
public y = 0,
|
|
47
|
-
) {}
|
|
48
|
-
matrixTransform(m: Mat) {
|
|
49
|
-
return new Pt(
|
|
50
|
-
m.a * this.x + m.c * this.y + m.e,
|
|
51
|
-
m.b * this.x + m.d * this.y + m.f,
|
|
52
|
-
)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
18
|
beforeAll(() => {
|
|
57
19
|
enableStaticRendering(true)
|
|
58
|
-
|
|
59
|
-
g.DOMMatrix = Mat
|
|
60
|
-
g.DOMPoint = Pt
|
|
61
|
-
HTMLCanvasElement.prototype.getContext = function () {
|
|
62
|
-
let font = '10px sans-serif'
|
|
63
|
-
return {
|
|
64
|
-
get font() {
|
|
65
|
-
return font
|
|
66
|
-
},
|
|
67
|
-
set font(v: string) {
|
|
68
|
-
font = v
|
|
69
|
-
},
|
|
70
|
-
measureText(t: string) {
|
|
71
|
-
const size = Number.parseFloat(font) || 10
|
|
72
|
-
return { width: t.length * size * 0.6 } as TextMetrics
|
|
73
|
-
},
|
|
74
|
-
} as unknown as CanvasRenderingContext2D
|
|
75
|
-
} as unknown as typeof HTMLCanvasElement.prototype.getContext
|
|
20
|
+
installRenderTestEnv()
|
|
76
21
|
})
|
|
77
22
|
|
|
78
23
|
// representative `impg query -o fasta-aln` block: PanSN names with region
|
package/src/largeTree.test.ts
CHANGED
|
@@ -26,8 +26,9 @@ test('a tree far past the argument limit still lays out', () => {
|
|
|
26
26
|
model.setWidth(1000)
|
|
27
27
|
|
|
28
28
|
expect(model.numRows).toBe(200_000)
|
|
29
|
-
// 's199999' is the longest name, at 7 chars * 7px
|
|
30
|
-
|
|
29
|
+
// 's199999' is the longest name, at 7 chars * 7px, scaled from the reference
|
|
30
|
+
// font size the labels are measured at down to the current one (13/16)
|
|
31
|
+
expect(model.labelsWidth).toBeCloseTo((49 * 13) / 16)
|
|
31
32
|
expect(model.treeWidth).toBeGreaterThan(0)
|
|
32
33
|
})
|
|
33
34
|
|
|
@@ -51,3 +52,16 @@ test('re-hovering the same tree node does not re-walk the tree', () => {
|
|
|
51
52
|
model.setHoveredTreeNode(undefined)
|
|
52
53
|
expect(model.hoveredTreeNode).toBeUndefined()
|
|
53
54
|
})
|
|
55
|
+
|
|
56
|
+
test('a vertical zoom does not re-measure every label', () => {
|
|
57
|
+
const model = MSAModelF().create({
|
|
58
|
+
type: 'MsaView',
|
|
59
|
+
data: { tree: flatNewick(200_000) },
|
|
60
|
+
})
|
|
61
|
+
model.setWidth(1000)
|
|
62
|
+
|
|
63
|
+
const before = model.labelWidthMap
|
|
64
|
+
model.setRowHeight(24)
|
|
65
|
+
expect(model.labelWidthMap).toBe(before)
|
|
66
|
+
expect(model.labelsWidth).toBeCloseTo((49 * 18) / 16)
|
|
67
|
+
})
|
package/src/model/DataModel.ts
CHANGED
|
@@ -53,15 +53,15 @@ export function DataModelF() {
|
|
|
53
53
|
self.gff = gff
|
|
54
54
|
},
|
|
55
55
|
}))
|
|
56
|
-
.postProcessSnapshot(snap =>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
56
|
+
.postProcessSnapshot(snap =>
|
|
57
|
+
// a large document is dropped from the snapshot rather than inlined
|
|
58
|
+
// into a session or a shared URL, and every field here holds one, so
|
|
59
|
+
// the rule is the same for all of them
|
|
60
|
+
Object.fromEntries(
|
|
61
|
+
Object.entries(snap).map(([key, text]) => [
|
|
62
|
+
key,
|
|
63
|
+
text && text.length > 50_000 ? undefined : text,
|
|
64
|
+
]),
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
67
|
}
|