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.
- package/bundle/index.js +99 -99
- package/bundle/index.js.map +4 -4
- package/dist/clustalX.js +22 -22
- package/dist/clustalX.js.map +1 -1
- package/dist/columnCounts.d.ts +7 -0
- package/dist/columnCounts.js +18 -0
- package/dist/columnCounts.js.map +1 -1
- package/dist/components/CopyButton.d.ts +9 -0
- package/dist/components/CopyButton.js +23 -0
- package/dist/components/CopyButton.js.map +1 -0
- 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/Loading.js +2 -2
- package/dist/components/Loading.js.map +1 -1
- package/dist/components/MSAView.js +2 -2
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/ResizeHandles.d.ts +4 -2
- package/dist/components/ResizeHandles.js +35 -54
- package/dist/components/ResizeHandles.js.map +1 -1
- package/dist/components/SequenceTextArea.js +3 -10
- package/dist/components/SequenceTextArea.js.map +1 -1
- package/dist/components/VerticalScrollbar.js +11 -25
- package/dist/components/VerticalScrollbar.js.map +1 -1
- package/dist/components/dialogs/FeatureDialog.js +3 -3
- package/dist/components/dialogs/FeatureDialog.js.map +1 -1
- package/dist/components/dialogs/InterProScanDialog.js +52 -129
- package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
- package/dist/components/dialogs/{UserProvidedDomainsDialog.d.ts → InterProScanFileDialog.d.ts} +2 -2
- package/dist/components/dialogs/{UserProvidedDomainsDialog.js → InterProScanFileDialog.js} +6 -10
- package/dist/components/dialogs/InterProScanFileDialog.js.map +1 -0
- package/dist/components/dialogs/TrackInfoDialog.d.ts +2 -4
- package/dist/components/dialogs/TrackInfoDialog.js +8 -14
- package/dist/components/dialogs/TrackInfoDialog.js.map +1 -1
- package/dist/components/header/FileMenu.js +3 -3
- package/dist/components/header/FileMenu.js.map +1 -1
- package/dist/components/header/GappynessSlider.js +1 -1
- package/dist/components/header/GappynessSlider.js.map +1 -1
- package/dist/components/header/{getDomainsMenu.d.ts → getAnnotationsMenu.d.ts} +1 -1
- package/dist/components/header/{getDomainsMenu.js → getAnnotationsMenu.js} +10 -9
- package/dist/components/header/getAnnotationsMenu.js.map +1 -0
- package/dist/components/header/settingsMenuItems.js +15 -0
- package/dist/components/header/settingsMenuItems.js.map +1 -1
- package/dist/components/import/ImportForm.js +1 -1
- package/dist/components/import/ImportForm.js.map +1 -1
- package/dist/components/import/ImportFormExamples.js +66 -57
- package/dist/components/import/ImportFormExamples.js.map +1 -1
- package/dist/components/minimap/Minimap.js +40 -23
- package/dist/components/minimap/Minimap.js.map +1 -1
- package/dist/components/minimap/MinimapSVG.d.ts +2 -1
- package/dist/components/minimap/MinimapSVG.js +3 -4
- package/dist/components/minimap/MinimapSVG.js.map +1 -1
- package/dist/components/minimap/minimapLayout.d.ts +10 -1
- package/dist/components/minimap/minimapLayout.js +15 -10
- package/dist/components/minimap/minimapLayout.js.map +1 -1
- package/dist/components/msa/{DomainLegend.d.ts → AnnotationLegend.d.ts} +2 -2
- package/dist/components/msa/{DomainLegend.js → AnnotationLegend.js} +4 -4
- package/dist/components/msa/AnnotationLegend.js.map +1 -0
- package/dist/components/msa/MSACanvas.js +9 -3
- 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/MSAMouseoverCanvas.js +1 -2
- package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
- package/dist/components/msa/MSAPanel.js +2 -2
- package/dist/components/msa/MSAPanel.js.map +1 -1
- package/dist/components/msa/domainBandCursor.d.ts +17 -0
- package/dist/components/msa/domainBandCursor.js +39 -0
- package/dist/components/msa/domainBandCursor.js.map +1 -0
- 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 +27 -41
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +1 -2
- package/dist/components/msa/renderMSAMouseover.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/TreeRuler.js +5 -2
- package/dist/components/tree/TreeRuler.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/ggplotPalettes.d.ts +0 -1
- package/dist/ggplotPalettes.js +0 -3
- package/dist/ggplotPalettes.js.map +1 -1
- package/dist/hierarchy.d.ts +2 -6
- package/dist/hierarchy.js +4 -6
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/launchInterProScan.d.ts +0 -23
- package/dist/launchInterProScan.js +2 -5
- package/dist/launchInterProScan.js.map +1 -1
- package/dist/measureTextCanvas.js +21 -6
- package/dist/measureTextCanvas.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 +86 -107
- package/dist/model.js +238 -162
- 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 +3 -3
- package/dist/renderToSvg.js.map +1 -1
- package/dist/rowCoordinateCalculations.d.ts +13 -17
- package/dist/rowCoordinateCalculations.js +45 -39
- package/dist/rowCoordinateCalculations.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 +6 -11
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +5 -5
- package/src/clustalX.ts +22 -22
- package/src/colSpace.test.ts +27 -0
- package/src/columnCounts.test.ts +15 -0
- package/src/columnCounts.ts +19 -0
- package/src/components/CopyButton.tsx +44 -0
- package/src/components/DragHandle.tsx +81 -0
- package/src/components/Loading.tsx +10 -1
- package/src/components/MSAView.tsx +2 -3
- package/src/components/ResizeHandles.tsx +70 -87
- package/src/components/SequenceTextArea.tsx +3 -16
- package/src/components/VerticalScrollbar.tsx +20 -33
- package/src/components/dialogs/FeatureDialog.tsx +3 -4
- package/src/components/dialogs/InterProScanDialog.tsx +54 -131
- package/src/components/dialogs/{UserProvidedDomainsDialog.tsx → InterProScanFileDialog.tsx} +6 -14
- package/src/components/dialogs/TrackInfoDialog.tsx +18 -20
- package/src/components/header/FileMenu.tsx +3 -3
- package/src/components/header/GappynessSlider.tsx +4 -1
- package/src/components/header/{getDomainsMenu.ts → getAnnotationsMenu.ts} +10 -9
- package/src/components/header/settingsMenuItems.ts +18 -0
- package/src/components/import/ImportForm.tsx +1 -1
- package/src/components/import/ImportFormExamples.tsx +82 -133
- package/src/components/minimap/Minimap.tsx +67 -32
- package/src/components/minimap/MinimapSVG.tsx +32 -31
- package/src/components/minimap/minimapLayout.test.ts +60 -0
- package/src/components/minimap/minimapLayout.ts +15 -10
- package/src/components/msa/{DomainLegend.tsx → AnnotationLegend.tsx} +7 -3
- package/src/components/msa/MSACanvas.tsx +24 -11
- package/src/components/msa/MSACanvasBlock.tsx +13 -3
- package/src/components/msa/MSAMouseoverCanvas.tsx +1 -3
- package/src/components/msa/MSAPanel.tsx +2 -2
- package/src/components/msa/domainBandCursor.ts +41 -0
- package/src/components/msa/msaRaster.test.ts +145 -0
- package/src/components/msa/msaRaster.ts +350 -0
- package/src/components/msa/renderMSABlock.ts +34 -43
- package/src/components/msa/renderMSAMouseover.ts +1 -3
- 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/TreeRuler.tsx +7 -2
- package/src/components/tree/renderTreeCanvas.test.ts +15 -7
- package/src/components/tree/renderTreeCanvas.ts +5 -1
- package/src/constants.ts +10 -0
- package/src/domainBands.test.ts +24 -0
- package/src/domainLetterColor.test.ts +80 -0
- package/src/ggplotPalettes.ts +0 -4
- package/src/hierarchy.test.ts +7 -7
- package/src/hierarchy.ts +6 -9
- package/src/impgRender.test.tsx +2 -57
- package/src/index.ts +4 -1
- package/src/largeTree.test.ts +39 -4
- package/src/launchInterProScan.ts +3 -30
- package/src/loadErrorDisplay.test.tsx +102 -0
- package/src/measureTextCanvas.ts +22 -6
- package/src/model/DataModel.ts +11 -11
- package/src/model.ts +262 -172
- package/src/neighborJoining.ts +36 -18
- package/src/renderDomainsSvg.test.tsx +2 -57
- package/src/renderMinimapSvg.test.tsx +55 -0
- package/src/renderSequenceLogoSvg.test.tsx +176 -0
- package/src/renderTestEnv.ts +96 -0
- package/src/renderToSvg.tsx +3 -3
- package/src/rowCoordinateCalculations.ts +48 -44
- package/src/sequenceLogo.test.ts +88 -0
- package/src/sequenceLogo.ts +63 -0
- package/src/types.ts +17 -16
- package/src/version.ts +1 -1
- package/src/viewChrome.test.tsx +134 -0
- 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/TextTrack.d.ts +0 -8
- package/dist/components/TextTrack.js +0 -58
- package/dist/components/TextTrack.js.map +0 -1
- package/dist/components/dialogs/DomainDialog.d.ts +0 -6
- package/dist/components/dialogs/DomainDialog.js +0 -19
- package/dist/components/dialogs/DomainDialog.js.map +0 -1
- package/dist/components/dialogs/UserProvidedDomainsDialog.js.map +0 -1
- package/dist/components/header/SettingsMenu.d.ts +0 -6
- package/dist/components/header/SettingsMenu.js +0 -40
- package/dist/components/header/SettingsMenu.js.map +0 -1
- package/dist/components/header/getDomainsMenu.js.map +0 -1
- package/dist/components/msa/DomainLegend.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/TextTrack.tsx +0 -103
- package/src/components/dialogs/DomainDialog.tsx +0 -45
- package/src/components/header/SettingsMenu.tsx +0 -57
- 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
|
-
|
|
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
|
|
171
|
-
|
|
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 (
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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}
|