react-msaview 5.10.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.
Files changed (226) hide show
  1. package/bundle/index.js +99 -99
  2. package/bundle/index.js.map +4 -4
  3. package/dist/clustalX.js +22 -22
  4. package/dist/clustalX.js.map +1 -1
  5. package/dist/columnCounts.d.ts +7 -0
  6. package/dist/columnCounts.js +18 -0
  7. package/dist/columnCounts.js.map +1 -1
  8. package/dist/components/CopyButton.d.ts +9 -0
  9. package/dist/components/CopyButton.js +23 -0
  10. package/dist/components/CopyButton.js.map +1 -0
  11. package/dist/components/DragHandle.d.ts +25 -0
  12. package/dist/components/DragHandle.js +49 -0
  13. package/dist/components/DragHandle.js.map +1 -0
  14. package/dist/components/Loading.js +2 -2
  15. package/dist/components/Loading.js.map +1 -1
  16. package/dist/components/MSAView.js +2 -2
  17. package/dist/components/MSAView.js.map +1 -1
  18. package/dist/components/ResizeHandles.d.ts +4 -2
  19. package/dist/components/ResizeHandles.js +35 -54
  20. package/dist/components/ResizeHandles.js.map +1 -1
  21. package/dist/components/SequenceTextArea.js +3 -10
  22. package/dist/components/SequenceTextArea.js.map +1 -1
  23. package/dist/components/VerticalScrollbar.js +11 -25
  24. package/dist/components/VerticalScrollbar.js.map +1 -1
  25. package/dist/components/dialogs/FeatureDialog.js +3 -3
  26. package/dist/components/dialogs/FeatureDialog.js.map +1 -1
  27. package/dist/components/dialogs/InterProScanDialog.js +52 -129
  28. package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
  29. package/dist/components/dialogs/{UserProvidedDomainsDialog.d.ts → InterProScanFileDialog.d.ts} +2 -2
  30. package/dist/components/dialogs/{UserProvidedDomainsDialog.js → InterProScanFileDialog.js} +6 -10
  31. package/dist/components/dialogs/InterProScanFileDialog.js.map +1 -0
  32. package/dist/components/dialogs/TrackInfoDialog.d.ts +2 -4
  33. package/dist/components/dialogs/TrackInfoDialog.js +8 -14
  34. package/dist/components/dialogs/TrackInfoDialog.js.map +1 -1
  35. package/dist/components/header/FileMenu.js +3 -3
  36. package/dist/components/header/FileMenu.js.map +1 -1
  37. package/dist/components/header/GappynessSlider.js +1 -1
  38. package/dist/components/header/GappynessSlider.js.map +1 -1
  39. package/dist/components/header/{getDomainsMenu.d.ts → getAnnotationsMenu.d.ts} +1 -1
  40. package/dist/components/header/{getDomainsMenu.js → getAnnotationsMenu.js} +10 -9
  41. package/dist/components/header/getAnnotationsMenu.js.map +1 -0
  42. package/dist/components/header/settingsMenuItems.js +15 -0
  43. package/dist/components/header/settingsMenuItems.js.map +1 -1
  44. package/dist/components/import/ImportForm.js +1 -1
  45. package/dist/components/import/ImportForm.js.map +1 -1
  46. package/dist/components/import/ImportFormExamples.js +66 -57
  47. package/dist/components/import/ImportFormExamples.js.map +1 -1
  48. package/dist/components/minimap/Minimap.js +40 -23
  49. package/dist/components/minimap/Minimap.js.map +1 -1
  50. package/dist/components/minimap/MinimapSVG.d.ts +2 -1
  51. package/dist/components/minimap/MinimapSVG.js +3 -4
  52. package/dist/components/minimap/MinimapSVG.js.map +1 -1
  53. package/dist/components/minimap/minimapLayout.d.ts +10 -1
  54. package/dist/components/minimap/minimapLayout.js +15 -10
  55. package/dist/components/minimap/minimapLayout.js.map +1 -1
  56. package/dist/components/msa/{DomainLegend.d.ts → AnnotationLegend.d.ts} +2 -2
  57. package/dist/components/msa/{DomainLegend.js → AnnotationLegend.js} +4 -4
  58. package/dist/components/msa/AnnotationLegend.js.map +1 -0
  59. package/dist/components/msa/MSACanvas.js +9 -3
  60. package/dist/components/msa/MSACanvas.js.map +1 -1
  61. package/dist/components/msa/MSACanvasBlock.js +12 -3
  62. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  63. package/dist/components/msa/MSAMouseoverCanvas.js +1 -2
  64. package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
  65. package/dist/components/msa/MSAPanel.js +2 -2
  66. package/dist/components/msa/MSAPanel.js.map +1 -1
  67. package/dist/components/msa/domainBandCursor.d.ts +17 -0
  68. package/dist/components/msa/domainBandCursor.js +39 -0
  69. package/dist/components/msa/domainBandCursor.js.map +1 -0
  70. package/dist/components/msa/msaRaster.d.ts +63 -0
  71. package/dist/components/msa/msaRaster.js +252 -0
  72. package/dist/components/msa/msaRaster.js.map +1 -0
  73. package/dist/components/msa/renderMSABlock.d.ts +2 -1
  74. package/dist/components/msa/renderMSABlock.js +27 -41
  75. package/dist/components/msa/renderMSABlock.js.map +1 -1
  76. package/dist/components/msa/renderMSAMouseover.js +1 -2
  77. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  78. package/dist/components/msa/tileColor.d.ts +6 -0
  79. package/dist/components/msa/tileColor.js +35 -0
  80. package/dist/components/msa/tileColor.js.map +1 -0
  81. package/dist/components/renderCtx.d.ts +1 -1
  82. package/dist/components/tracks/TrackBlocks.d.ts +14 -0
  83. package/dist/components/tracks/TrackBlocks.js +65 -0
  84. package/dist/components/tracks/TrackBlocks.js.map +1 -0
  85. package/dist/components/tracks/{renderTracksSvg.d.ts → drawTracks.d.ts} +22 -10
  86. package/dist/components/tracks/drawTracks.js +193 -0
  87. package/dist/components/tracks/drawTracks.js.map +1 -0
  88. package/dist/components/tree/TreeCanvas.js +8 -2
  89. package/dist/components/tree/TreeCanvas.js.map +1 -1
  90. package/dist/components/tree/TreeCanvasBlock.js +7 -4
  91. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  92. package/dist/components/tree/TreeRuler.js +5 -2
  93. package/dist/components/tree/TreeRuler.js.map +1 -1
  94. package/dist/components/tree/renderTreeCanvas.js +5 -2
  95. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  96. package/dist/constants.d.ts +3 -0
  97. package/dist/constants.js +8 -0
  98. package/dist/constants.js.map +1 -1
  99. package/dist/ggplotPalettes.d.ts +0 -1
  100. package/dist/ggplotPalettes.js +0 -3
  101. package/dist/ggplotPalettes.js.map +1 -1
  102. package/dist/hierarchy.d.ts +2 -6
  103. package/dist/hierarchy.js +4 -6
  104. package/dist/hierarchy.js.map +1 -1
  105. package/dist/index.d.ts +2 -2
  106. package/dist/launchInterProScan.d.ts +0 -23
  107. package/dist/launchInterProScan.js +2 -5
  108. package/dist/launchInterProScan.js.map +1 -1
  109. package/dist/measureTextCanvas.js +21 -6
  110. package/dist/measureTextCanvas.js.map +1 -1
  111. package/dist/model/DataModel.d.ts +1 -4
  112. package/dist/model/DataModel.js +8 -10
  113. package/dist/model/DataModel.js.map +1 -1
  114. package/dist/model.d.ts +86 -107
  115. package/dist/model.js +238 -162
  116. package/dist/model.js.map +1 -1
  117. package/dist/neighborJoining.js +34 -18
  118. package/dist/neighborJoining.js.map +1 -1
  119. package/dist/renderTestEnv.d.ts +43 -0
  120. package/dist/renderTestEnv.js +88 -0
  121. package/dist/renderTestEnv.js.map +1 -0
  122. package/dist/renderToSvg.js +3 -3
  123. package/dist/renderToSvg.js.map +1 -1
  124. package/dist/rowCoordinateCalculations.d.ts +13 -17
  125. package/dist/rowCoordinateCalculations.js +45 -39
  126. package/dist/rowCoordinateCalculations.js.map +1 -1
  127. package/dist/sequenceLogo.d.ts +28 -0
  128. package/dist/sequenceLogo.js +47 -0
  129. package/dist/sequenceLogo.js.map +1 -0
  130. package/dist/types.d.ts +6 -11
  131. package/dist/version.d.ts +1 -1
  132. package/dist/version.js +1 -1
  133. package/dist/version.js.map +1 -1
  134. package/package.json +5 -5
  135. package/src/clustalX.ts +22 -22
  136. package/src/colSpace.test.ts +27 -0
  137. package/src/columnCounts.test.ts +15 -0
  138. package/src/columnCounts.ts +19 -0
  139. package/src/components/CopyButton.tsx +44 -0
  140. package/src/components/DragHandle.tsx +81 -0
  141. package/src/components/Loading.tsx +10 -1
  142. package/src/components/MSAView.tsx +2 -3
  143. package/src/components/ResizeHandles.tsx +70 -87
  144. package/src/components/SequenceTextArea.tsx +3 -16
  145. package/src/components/VerticalScrollbar.tsx +20 -33
  146. package/src/components/dialogs/FeatureDialog.tsx +3 -4
  147. package/src/components/dialogs/InterProScanDialog.tsx +54 -131
  148. package/src/components/dialogs/{UserProvidedDomainsDialog.tsx → InterProScanFileDialog.tsx} +6 -14
  149. package/src/components/dialogs/TrackInfoDialog.tsx +18 -20
  150. package/src/components/header/FileMenu.tsx +3 -3
  151. package/src/components/header/GappynessSlider.tsx +4 -1
  152. package/src/components/header/{getDomainsMenu.ts → getAnnotationsMenu.ts} +10 -9
  153. package/src/components/header/settingsMenuItems.ts +18 -0
  154. package/src/components/import/ImportForm.tsx +1 -1
  155. package/src/components/import/ImportFormExamples.tsx +82 -133
  156. package/src/components/minimap/Minimap.tsx +67 -32
  157. package/src/components/minimap/MinimapSVG.tsx +32 -31
  158. package/src/components/minimap/minimapLayout.test.ts +60 -0
  159. package/src/components/minimap/minimapLayout.ts +15 -10
  160. package/src/components/msa/{DomainLegend.tsx → AnnotationLegend.tsx} +7 -3
  161. package/src/components/msa/MSACanvas.tsx +24 -11
  162. package/src/components/msa/MSACanvasBlock.tsx +13 -3
  163. package/src/components/msa/MSAMouseoverCanvas.tsx +1 -3
  164. package/src/components/msa/MSAPanel.tsx +2 -2
  165. package/src/components/msa/domainBandCursor.ts +41 -0
  166. package/src/components/msa/msaRaster.test.ts +145 -0
  167. package/src/components/msa/msaRaster.ts +350 -0
  168. package/src/components/msa/renderMSABlock.ts +34 -43
  169. package/src/components/msa/renderMSAMouseover.ts +1 -3
  170. package/src/components/msa/tileColor.ts +37 -0
  171. package/src/components/renderCtx.ts +2 -0
  172. package/src/components/tracks/TrackBlocks.tsx +111 -0
  173. package/src/components/tracks/drawTracks.ts +323 -0
  174. package/src/components/tree/TreeCanvas.tsx +16 -4
  175. package/src/components/tree/TreeCanvasBlock.tsx +18 -11
  176. package/src/components/tree/TreeRuler.tsx +7 -2
  177. package/src/components/tree/renderTreeCanvas.test.ts +15 -7
  178. package/src/components/tree/renderTreeCanvas.ts +5 -1
  179. package/src/constants.ts +10 -0
  180. package/src/domainBands.test.ts +24 -0
  181. package/src/domainLetterColor.test.ts +80 -0
  182. package/src/ggplotPalettes.ts +0 -4
  183. package/src/hierarchy.test.ts +7 -7
  184. package/src/hierarchy.ts +6 -9
  185. package/src/impgRender.test.tsx +2 -57
  186. package/src/index.ts +4 -1
  187. package/src/largeTree.test.ts +39 -4
  188. package/src/launchInterProScan.ts +3 -30
  189. package/src/loadErrorDisplay.test.tsx +102 -0
  190. package/src/measureTextCanvas.ts +22 -6
  191. package/src/model/DataModel.ts +11 -11
  192. package/src/model.ts +262 -172
  193. package/src/neighborJoining.ts +36 -18
  194. package/src/renderDomainsSvg.test.tsx +2 -57
  195. package/src/renderMinimapSvg.test.tsx +55 -0
  196. package/src/renderSequenceLogoSvg.test.tsx +176 -0
  197. package/src/renderTestEnv.ts +96 -0
  198. package/src/renderToSvg.tsx +3 -3
  199. package/src/rowCoordinateCalculations.ts +48 -44
  200. package/src/sequenceLogo.test.ts +88 -0
  201. package/src/sequenceLogo.ts +63 -0
  202. package/src/types.ts +17 -16
  203. package/src/version.ts +1 -1
  204. package/src/viewChrome.test.tsx +134 -0
  205. package/dist/components/ConservationTrack.d.ts +0 -8
  206. package/dist/components/ConservationTrack.js +0 -49
  207. package/dist/components/ConservationTrack.js.map +0 -1
  208. package/dist/components/TextTrack.d.ts +0 -8
  209. package/dist/components/TextTrack.js +0 -58
  210. package/dist/components/TextTrack.js.map +0 -1
  211. package/dist/components/dialogs/DomainDialog.d.ts +0 -6
  212. package/dist/components/dialogs/DomainDialog.js +0 -19
  213. package/dist/components/dialogs/DomainDialog.js.map +0 -1
  214. package/dist/components/dialogs/UserProvidedDomainsDialog.js.map +0 -1
  215. package/dist/components/header/SettingsMenu.d.ts +0 -6
  216. package/dist/components/header/SettingsMenu.js +0 -40
  217. package/dist/components/header/SettingsMenu.js.map +0 -1
  218. package/dist/components/header/getDomainsMenu.js.map +0 -1
  219. package/dist/components/msa/DomainLegend.js.map +0 -1
  220. package/dist/components/tracks/renderTracksSvg.js +0 -126
  221. package/dist/components/tracks/renderTracksSvg.js.map +0 -1
  222. package/src/components/ConservationTrack.tsx +0 -99
  223. package/src/components/TextTrack.tsx +0 -103
  224. package/src/components/dialogs/DomainDialog.tsx +0 -45
  225. package/src/components/header/SettingsMenu.tsx +0 -57
  226. package/src/components/tracks/renderTracksSvg.ts +0 -247
@@ -1,6 +1,8 @@
1
1
  import { setFontSize } from '../../setFontSize.ts'
2
2
  import { adjustColorForContrast } from '../../util.ts'
3
3
  import { getVisibleLeaves } from '../getVisibleLeaves.ts'
4
+ import { domainBandCursor } from './domainBandCursor.ts'
5
+ import { tileColorFn } from './tileColor.ts'
4
6
  import { visibleColRange } from './visibleColRange.ts'
5
7
 
6
8
  import type { HierarchyNode } from '../../hierarchy.ts'
@@ -19,6 +21,7 @@ export function renderMSABlock({
19
21
  highResScaleFactorOverride,
20
22
  blockSizeXOverride,
21
23
  blockSizeYOverride,
24
+ rasterTiles,
22
25
  }: {
23
26
  offsetX: number
24
27
  offsetY: number
@@ -29,6 +32,7 @@ export function renderMSABlock({
29
32
  highResScaleFactorOverride?: number
30
33
  blockSizeXOverride?: number
31
34
  blockSizeYOverride?: number
35
+ rasterTiles?: boolean
32
36
  }) {
33
37
  const {
34
38
  colWidth,
@@ -72,6 +76,9 @@ export function renderMSABlock({
72
76
  // when domains are shown the background tiles come from
73
77
  // renderBoxFeatureCanvasBlock, so only the letters are drawn here
74
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,
75
82
  })
76
83
  drawInsertionIndicators({
77
84
  model,
@@ -83,23 +90,6 @@ export function renderMSABlock({
83
90
  ctx.resetTransform()
84
91
  }
85
92
 
86
- // Per-cell tile color for the active scheme. The scheme is fixed for the whole
87
- // block, so resolve which rule applies once rather than re-testing the scheme
88
- // name inside the innermost loop.
89
- function tileColorFn(model: MsaViewModel) {
90
- const { colorSchemeName, colorScheme, colClustalX, colConsensus } = model
91
- if (colorSchemeName === 'clustalx_protein_dynamic') {
92
- return (col: number, letter: string) => colClustalX[col]![letter]
93
- }
94
- if (colorSchemeName === 'percent_identity_dynamic') {
95
- return (col: number, letter: string) => {
96
- const consensus = colConsensus[col]!
97
- return letter === consensus.letter ? consensus.color : undefined
98
- }
99
- }
100
- return (_col: number, letter: string) => colorScheme[letter]
101
- }
102
-
103
93
  // Letter color to use over a domain box, keyed by accession. Domain fills come
104
94
  // from a categorical palette that has no fixed lightness, so ask the theme for a
105
95
  // readable text color per fill instead of assuming dark-on-light.
@@ -122,6 +112,7 @@ function drawTilesAndText({
122
112
  xEnd,
123
113
  referenceSeq,
124
114
  drawTiles,
115
+ rasterTiles,
125
116
  }: {
126
117
  model: MsaViewModel
127
118
  theme: Theme
@@ -132,6 +123,7 @@ function drawTilesAndText({
132
123
  xEnd: number
133
124
  referenceSeq: string | null | undefined
134
125
  drawTiles: boolean
126
+ rasterTiles?: boolean
135
127
  }) {
136
128
  const {
137
129
  bgColor,
@@ -145,7 +137,11 @@ function drawTilesAndText({
145
137
  } = model
146
138
 
147
139
  const tiles = drawTiles && bgColor
148
- 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
149
145
  const tileColor = tileColorFn(model)
150
146
  const offsetXAligned = xStart * colWidth
151
147
  const halfColWidth = colWidth / 2
@@ -167,8 +163,9 @@ function drawTilesAndText({
167
163
  if (str) {
168
164
  const tileY = y - rowHeight
169
165
  const textY = y - quarterRowHeight
170
- const bands = overDomains ? domainBandsByStart.get(name) : undefined
171
- let band = 0
166
+ const bandAt = domainBandCursor(
167
+ overDomains ? domainBandsByStart.get(name) : undefined,
168
+ )
172
169
 
173
170
  for (let j = 0, l2 = str.length; j < l2; j++) {
174
171
  const col = xStart + j
@@ -176,9 +173,9 @@ function drawTilesAndText({
176
173
  const x = j * colWidth + offsetXAligned
177
174
  const isMatchingReference =
178
175
  referenceSeq && name !== relativeTo && letter === referenceSeq[j]
179
- const color = tileColor(col, letter)
176
+ const color = needsColor ? tileColor(col, letter) : undefined
180
177
 
181
- if (tiles) {
178
+ if (paintTiles) {
182
179
  ctx.fillStyle = isMatchingReference
183
180
  ? theme.palette.action.hover
184
181
  : color || theme.palette.background.default
@@ -186,27 +183,21 @@ function drawTilesAndText({
186
183
  }
187
184
 
188
185
  if (showMsaLetters) {
189
- // bands are sorted by start column and the sweep is left to right,
190
- // so one forward-only cursor finds the band covering each column
191
- while (bands && band < bands.length && bands[band]!.endCol <= col) {
192
- band++
193
- }
194
- const covering = bands?.[band]
195
- ctx.fillStyle =
196
- covering && covering.startCol <= col
197
- ? // on top of a domain box: contrast against the box fill
198
- domainColors!.get(covering.annotation.accession)!
199
- : tiles
200
- ? // on top of a colored tile
201
- (contrastScheme[letter] ?? 'black')
202
- : !drawTiles || !color
203
- ? // plain background, uncolored letters
204
- theme.palette.text.primary
205
- : // letter-color mode: darken/lighten to stay readable
206
- adjustColorForContrast(
207
- color,
208
- theme.palette.background.default,
209
- )
186
+ const covering = bandAt(col)
187
+ ctx.fillStyle = covering
188
+ ? // on top of a domain box: contrast against the box fill
189
+ domainColors!.get(covering.annotation.accession)!
190
+ : tiles
191
+ ? // on top of a colored tile
192
+ (contrastScheme[letter] ?? 'black')
193
+ : !drawTiles || !color
194
+ ? // plain background, uncolored letters
195
+ theme.palette.text.primary
196
+ : // letter-color mode: darken/lighten to stay readable
197
+ adjustColorForContrast(
198
+ color,
199
+ theme.palette.background.default,
200
+ )
210
201
  ctx.fillText(
211
202
  isMatchingReference ? '.' : letter,
212
203
  x + halfColWidth,
@@ -23,8 +23,7 @@ export function renderMouseover({
23
23
  const {
24
24
  mouseCol,
25
25
  colWidth,
26
- msaAreaWidth,
27
- verticalScrollbarWidth,
26
+ msaCanvasWidth: width,
28
27
  height,
29
28
  rowHeight,
30
29
  scrollX,
@@ -37,7 +36,6 @@ export function renderMouseover({
37
36
  hoveredRowIndices,
38
37
  highlightedColumnRuns,
39
38
  } = model
40
- const width = msaAreaWidth - verticalScrollbarWidth
41
39
  ctx.resetTransform()
42
40
  ctx.clearRect(0, 0, width * highResScaleFactor, height * highResScaleFactor)
43
41
  ctx.scale(highResScaleFactor, highResScaleFactor)
@@ -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}