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.
Files changed (122) hide show
  1. package/bundle/index.js +98 -98
  2. package/bundle/index.js.map +4 -4
  3. package/dist/columnCounts.d.ts +7 -0
  4. package/dist/columnCounts.js +18 -0
  5. package/dist/columnCounts.js.map +1 -1
  6. package/dist/components/DragHandle.d.ts +25 -0
  7. package/dist/components/DragHandle.js +49 -0
  8. package/dist/components/DragHandle.js.map +1 -0
  9. package/dist/components/ResizeHandles.d.ts +4 -2
  10. package/dist/components/ResizeHandles.js +29 -32
  11. package/dist/components/ResizeHandles.js.map +1 -1
  12. package/dist/components/VerticalScrollbar.js +2 -2
  13. package/dist/components/VerticalScrollbar.js.map +1 -1
  14. package/dist/components/dialogs/InterProScanDialog.js +52 -129
  15. package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
  16. package/dist/components/header/settingsMenuItems.js +16 -1
  17. package/dist/components/header/settingsMenuItems.js.map +1 -1
  18. package/dist/components/minimap/Minimap.js +32 -4
  19. package/dist/components/minimap/Minimap.js.map +1 -1
  20. package/dist/components/msa/MSACanvas.js +8 -2
  21. package/dist/components/msa/MSACanvas.js.map +1 -1
  22. package/dist/components/msa/MSACanvasBlock.js +12 -3
  23. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  24. package/dist/components/msa/msaRaster.d.ts +63 -0
  25. package/dist/components/msa/msaRaster.js +252 -0
  26. package/dist/components/msa/msaRaster.js.map +1 -0
  27. package/dist/components/msa/renderMSABlock.d.ts +2 -1
  28. package/dist/components/msa/renderMSABlock.js +13 -21
  29. package/dist/components/msa/renderMSABlock.js.map +1 -1
  30. package/dist/components/msa/tileColor.d.ts +6 -0
  31. package/dist/components/msa/tileColor.js +35 -0
  32. package/dist/components/msa/tileColor.js.map +1 -0
  33. package/dist/components/renderCtx.d.ts +1 -1
  34. package/dist/components/tracks/TrackBlocks.d.ts +14 -0
  35. package/dist/components/tracks/TrackBlocks.js +65 -0
  36. package/dist/components/tracks/TrackBlocks.js.map +1 -0
  37. package/dist/components/tracks/{renderTracksSvg.d.ts → drawTracks.d.ts} +22 -10
  38. package/dist/components/tracks/drawTracks.js +193 -0
  39. package/dist/components/tracks/drawTracks.js.map +1 -0
  40. package/dist/components/tree/TreeCanvas.js +8 -2
  41. package/dist/components/tree/TreeCanvas.js.map +1 -1
  42. package/dist/components/tree/TreeCanvasBlock.js +7 -4
  43. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  44. package/dist/components/tree/renderTreeCanvas.js +27 -21
  45. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  46. package/dist/constants.d.ts +3 -0
  47. package/dist/constants.js +8 -0
  48. package/dist/constants.js.map +1 -1
  49. package/dist/model/DataModel.d.ts +1 -4
  50. package/dist/model/DataModel.js +8 -10
  51. package/dist/model/DataModel.js.map +1 -1
  52. package/dist/model/treeModel.d.ts +1 -1
  53. package/dist/model/treeModel.js +1 -1
  54. package/dist/model.d.ts +36 -7
  55. package/dist/model.js +113 -58
  56. package/dist/model.js.map +1 -1
  57. package/dist/neighborJoining.js +34 -18
  58. package/dist/neighborJoining.js.map +1 -1
  59. package/dist/renderTestEnv.d.ts +43 -0
  60. package/dist/renderTestEnv.js +88 -0
  61. package/dist/renderTestEnv.js.map +1 -0
  62. package/dist/renderToSvg.js +1 -1
  63. package/dist/renderToSvg.js.map +1 -1
  64. package/dist/sequenceLogo.d.ts +28 -0
  65. package/dist/sequenceLogo.js +47 -0
  66. package/dist/sequenceLogo.js.map +1 -0
  67. package/dist/types.d.ts +2 -0
  68. package/dist/version.d.ts +1 -1
  69. package/dist/version.js +1 -1
  70. package/package.json +3 -3
  71. package/src/columnCounts.test.ts +15 -0
  72. package/src/columnCounts.ts +19 -0
  73. package/src/components/DragHandle.tsx +81 -0
  74. package/src/components/ResizeHandles.tsx +46 -54
  75. package/src/components/VerticalScrollbar.tsx +3 -2
  76. package/src/components/dialogs/InterProScanDialog.tsx +54 -131
  77. package/src/components/header/settingsMenuItems.ts +19 -1
  78. package/src/components/minimap/Minimap.tsx +47 -4
  79. package/src/components/msa/MSACanvas.tsx +30 -10
  80. package/src/components/msa/MSACanvasBlock.tsx +13 -3
  81. package/src/components/msa/msaRaster.test.ts +145 -0
  82. package/src/components/msa/msaRaster.ts +350 -0
  83. package/src/components/msa/renderMSABlock.ts +15 -20
  84. package/src/components/msa/tileColor.ts +37 -0
  85. package/src/components/renderCtx.ts +2 -0
  86. package/src/components/tracks/TrackBlocks.tsx +111 -0
  87. package/src/components/tracks/drawTracks.ts +323 -0
  88. package/src/components/tree/TreeCanvas.tsx +16 -4
  89. package/src/components/tree/TreeCanvasBlock.tsx +18 -11
  90. package/src/components/tree/renderTreeCanvas.test.ts +55 -0
  91. package/src/components/tree/renderTreeCanvas.ts +28 -19
  92. package/src/constants.ts +10 -0
  93. package/src/impgRender.test.tsx +2 -57
  94. package/src/largeTree.test.ts +16 -2
  95. package/src/model/DataModel.ts +11 -11
  96. package/src/model/treeModel.ts +1 -1
  97. package/src/model.ts +128 -60
  98. package/src/neighborJoining.ts +36 -18
  99. package/src/renderDomainsSvg.test.tsx +2 -57
  100. package/src/renderMinimapSvg.test.tsx +2 -56
  101. package/src/renderSequenceLogoSvg.test.tsx +176 -0
  102. package/src/renderTestEnv.ts +96 -0
  103. package/src/renderToSvg.tsx +1 -1
  104. package/src/sequenceLogo.test.ts +88 -0
  105. package/src/sequenceLogo.ts +63 -0
  106. package/src/types.ts +7 -1
  107. package/src/version.ts +1 -1
  108. package/dist/components/ConservationTrack.d.ts +0 -8
  109. package/dist/components/ConservationTrack.js +0 -49
  110. package/dist/components/ConservationTrack.js.map +0 -1
  111. package/dist/components/ScrollbarThumb.d.ts +0 -14
  112. package/dist/components/ScrollbarThumb.js +0 -28
  113. package/dist/components/ScrollbarThumb.js.map +0 -1
  114. package/dist/components/TextTrack.d.ts +0 -8
  115. package/dist/components/TextTrack.js +0 -58
  116. package/dist/components/TextTrack.js.map +0 -1
  117. package/dist/components/tracks/renderTracksSvg.js +0 -126
  118. package/dist/components/tracks/renderTracksSvg.js.map +0 -1
  119. package/src/components/ConservationTrack.tsx +0 -99
  120. package/src/components/ScrollbarThumb.tsx +0 -48
  121. package/src/components/TextTrack.tsx +0 -103
  122. 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
- if (tiles || showMsaLetters) {
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 (tiles) {
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
+ }
@@ -20,6 +20,8 @@ export type RenderCtx = Pick<
20
20
  | 'measureText'
21
21
  | 'moveTo'
22
22
  | 'resetTransform'
23
+ | 'restore'
24
+ | 'save'
23
25
  | 'scale'
24
26
  | 'setLineDash'
25
27
  | 'stroke'
@@ -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
- {blocksY.map(block => (
84
- <TreeCanvasBlock key={block} model={model} offsetY={block} />
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 { scrollY, treeAreaWidth, blockSize, highResScaleFactor } = model
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: scrollY + offsetY,
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 + scrollY,
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
- {hovered ? (
155
- <div
156
- className={classes.tooltip}
157
- style={{ left: hovered.clientX + 12, top: hovered.clientY + 12 }}
158
- >
159
- {hovered.name}
160
- </div>
161
- ) : null}
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
  })