react-msaview 6.5.0 → 7.0.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/README.md +6 -5
- package/bundle/index.js +100 -101
- package/bundle/index.js.map +4 -4
- package/dist/components/Loading.js +2 -2
- package/dist/components/Loading.js.map +1 -1
- package/dist/components/MSAViewer.d.ts +10 -2
- package/dist/components/MSAViewer.js +90 -2
- package/dist/components/MSAViewer.js.map +1 -1
- package/dist/components/ResizeHandles.d.ts +3 -3
- package/dist/components/ResizeHandles.js +28 -21
- package/dist/components/ResizeHandles.js.map +1 -1
- package/dist/components/dialogs/{InterProScanDialog.d.ts → AnnotationFileDialog.d.ts} +2 -2
- package/dist/components/dialogs/AnnotationFileDialog.js +92 -0
- package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -0
- package/dist/components/dialogs/FeatureDialog.js +13 -9
- package/dist/components/dialogs/FeatureDialog.js.map +1 -1
- package/dist/components/header/Header.js +4 -0
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/HeaderStatusArea.js +0 -2
- package/dist/components/header/HeaderStatusArea.js.map +1 -1
- package/dist/components/header/LoadWarnings.d.ts +6 -0
- package/dist/components/header/LoadWarnings.js +36 -0
- package/dist/components/header/LoadWarnings.js.map +1 -0
- package/dist/components/header/MultiAlignmentSelector.js +0 -0
- package/dist/components/header/MultiAlignmentSelector.js.map +1 -1
- package/dist/components/header/TreeSettingsMenu.js +9 -1
- package/dist/components/header/TreeSettingsMenu.js.map +1 -1
- package/dist/components/header/UnshareableDataWarning.d.ts +6 -0
- package/dist/components/header/UnshareableDataWarning.js +42 -0
- package/dist/components/header/UnshareableDataWarning.js.map +1 -0
- package/dist/components/header/getAnnotationsMenu.js +14 -14
- package/dist/components/header/getAnnotationsMenu.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 +2 -6
- package/dist/components/import/ImportFormExamples.js.map +1 -1
- package/dist/components/import/util.js +3 -0
- package/dist/components/import/util.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +4 -5
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/msaRaster.d.ts +13 -3
- package/dist/components/msa/msaRaster.js +173 -61
- package/dist/components/msa/msaRaster.js.map +1 -1
- package/dist/components/msa/renderHighlights.d.ts +39 -1
- package/dist/components/msa/renderHighlights.js +71 -6
- package/dist/components/msa/renderHighlights.js.map +1 -1
- package/dist/components/msa/renderMSABlock.d.ts +1 -2
- package/dist/components/msa/renderMSABlock.js +24 -28
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +9 -30
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/renderCtx.d.ts +1 -1
- package/dist/components/tracks/TrackBlocks.js +10 -11
- package/dist/components/tracks/TrackBlocks.js.map +1 -1
- package/dist/components/tracks/drawTracks.d.ts +26 -8
- package/dist/components/tracks/drawTracks.js +123 -13
- package/dist/components/tracks/drawTracks.js.map +1 -1
- package/dist/components/tree/TreeCanvas.js +2 -3
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +4 -2
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/TreeRuler.js +13 -3
- package/dist/components/tree/TreeRuler.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.d.ts +0 -1
- package/dist/components/tree/renderTreeCanvas.js +70 -30
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/components/tree/scaleBar.d.ts +10 -0
- package/dist/components/tree/scaleBar.js +24 -0
- package/dist/components/tree/scaleBar.js.map +1 -0
- package/dist/constants.d.ts +3 -1
- package/dist/constants.js +18 -1
- package/dist/constants.js.map +1 -1
- package/dist/fetchUtils.d.ts +7 -7
- package/dist/fetchUtils.js +4 -32
- package/dist/fetchUtils.js.map +1 -1
- package/dist/hierarchy.d.ts +2 -1
- package/dist/hierarchy.js +16 -10
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/model/DataModel.js +5 -2
- package/dist/model/DataModel.js.map +1 -1
- package/dist/model.d.ts +454 -177
- package/dist/model.js +1180 -511
- package/dist/model.js.map +1 -1
- package/dist/renderToStaticMarkup.d.ts +11 -0
- package/dist/renderToStaticMarkup.js +30 -7
- package/dist/renderToStaticMarkup.js.map +1 -1
- package/dist/renderToSvg.js +25 -19
- package/dist/renderToSvg.js.map +1 -1
- package/dist/rowCoordinateCalculations.d.ts +25 -25
- package/dist/rowCoordinateCalculations.js +12 -36
- package/dist/rowCoordinateCalculations.js.map +1 -1
- package/dist/seqPosToGlobalCol.js +3 -1
- package/dist/seqPosToGlobalCol.js.map +1 -1
- package/dist/svgTestUtil.d.ts +144 -82
- package/dist/types.d.ts +97 -3
- package/dist/util.d.ts +11 -4
- package/dist/util.js +30 -10
- package/dist/util.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/wuss.d.ts +14 -0
- package/dist/wuss.js +55 -0
- package/dist/wuss.js.map +1 -0
- package/package.json +10 -8
- package/src/components/Loading.tsx +12 -2
- package/src/components/MSAViewer.tsx +106 -2
- package/src/components/ResizeHandles.tsx +31 -31
- package/src/components/dialogs/AnnotationFileDialog.tsx +162 -0
- package/src/components/dialogs/FeatureDialog.tsx +13 -9
- package/src/components/header/Header.tsx +4 -0
- package/src/components/header/HeaderStatusArea.tsx +0 -5
- package/src/components/header/LoadWarnings.tsx +49 -0
- package/src/components/header/MultiAlignmentSelector.tsx +0 -0
- package/src/components/header/TreeSettingsMenu.tsx +9 -1
- package/src/components/header/UnshareableDataWarning.tsx +58 -0
- package/src/components/header/getAnnotationsMenu.ts +16 -17
- package/src/components/import/ImportForm.tsx +2 -2
- package/src/components/import/ImportFormExamples.tsx +2 -6
- package/src/components/import/util.ts +3 -0
- package/src/components/msa/MSACanvasBlock.tsx +4 -5
- package/src/components/msa/msaRaster.ts +216 -59
- package/src/components/msa/renderHighlights.ts +103 -5
- package/src/components/msa/renderMSABlock.ts +25 -36
- package/src/components/msa/renderMSAMouseover.ts +9 -39
- package/src/components/renderCtx.ts +1 -0
- package/src/components/tracks/TrackBlocks.tsx +10 -13
- package/src/components/tracks/drawTracks.ts +165 -17
- package/src/components/tree/TreeCanvas.tsx +2 -3
- package/src/components/tree/TreeCanvasBlock.tsx +4 -2
- package/src/components/tree/TreeRuler.tsx +38 -3
- package/src/components/tree/renderTreeCanvas.ts +79 -35
- package/src/components/tree/scaleBar.ts +23 -0
- package/src/constants.ts +20 -1
- package/src/fetchUtils.ts +7 -40
- package/src/hierarchy.ts +20 -11
- package/src/index.ts +18 -2
- package/src/model/DataModel.ts +6 -2
- package/src/model.ts +1352 -616
- package/src/rawImports.d.ts +9 -0
- package/src/renderToStaticMarkup.ts +34 -7
- package/src/renderToSvg.tsx +33 -27
- package/src/rowCoordinateCalculations.ts +15 -59
- package/src/seqPosToGlobalCol.ts +3 -1
- package/src/types.ts +131 -4
- package/src/util.ts +31 -14
- package/src/version.ts +1 -1
- package/src/wuss.ts +62 -0
- package/dist/components/dialogs/InterProScanDialog.js +0 -137
- package/dist/components/dialogs/InterProScanDialog.js.map +0 -1
- package/dist/components/dialogs/InterProScanFileDialog.d.ts +0 -7
- package/dist/components/dialogs/InterProScanFileDialog.js +0 -64
- package/dist/components/dialogs/InterProScanFileDialog.js.map +0 -1
- package/dist/launchInterProScan.d.ts +0 -13
- package/dist/launchInterProScan.js +0 -110
- package/dist/launchInterProScan.js.map +0 -1
- package/dist/useColorContrast.d.ts +0 -10
- package/dist/useColorContrast.js +0 -13
- package/dist/useColorContrast.js.map +0 -1
- package/src/clustalX.test.ts +0 -74
- package/src/colSpace.test.ts +0 -67
- package/src/collapseLogic.test.ts +0 -115
- package/src/colorSchemes.docs.test.ts +0 -57
- package/src/columnCounts.test.ts +0 -73
- package/src/columnStats.test.ts +0 -94
- package/src/columnTracks.test.ts +0 -97
- package/src/components/dialogs/InterProScanDialog.tsx +0 -214
- package/src/components/dialogs/InterProScanFileDialog.tsx +0 -138
- package/src/components/minimap/minimapLayout.test.ts +0 -71
- package/src/components/msa/msaRaster.test.ts +0 -145
- package/src/components/msa/renderBoxFeatureCanvasBlock.test.ts +0 -51
- package/src/components/tree/renderTreeCanvas.test.ts +0 -296
- package/src/dataInitialized.test.ts +0 -42
- package/src/domainBands.test.ts +0 -122
- package/src/domainLetterColor.test.ts +0 -80
- package/src/exportFileName.test.ts +0 -40
- package/src/fetchUtils.test.ts +0 -83
- package/src/flatToTree.test.ts +0 -51
- package/src/hierarchy.test.ts +0 -228
- package/src/highlights.test.ts +0 -70
- package/src/impgRender.test.tsx +0 -68
- package/src/largeTree.test.ts +0 -67
- package/src/launchInterProScan.ts +0 -167
- package/src/loadErrorDisplay.test.tsx +0 -102
- package/src/modelFilehandleLoaders.test.ts +0 -137
- package/src/modelReset.test.ts +0 -120
- package/src/neighborJoining.test.ts +0 -164
- package/src/parseAsn1.test.ts +0 -36
- package/src/propertyConservation.test.ts +0 -36
- package/src/renderColumnTracksSvg.test.tsx +0 -108
- package/src/renderDomainsSvg.test.tsx +0 -118
- package/src/renderHighlightsSvg.test.tsx +0 -85
- package/src/renderMinimapSvg.test.tsx +0 -62
- package/src/renderRasterSvg.test.tsx +0 -144
- package/src/renderSequenceLogoSvg.test.tsx +0 -165
- package/src/renderToSvg.test.tsx +0 -65
- package/src/reset.test.ts +0 -61
- package/src/rowCoordinateCalculations.test.ts +0 -211
- package/src/rowSpace.test.ts +0 -46
- package/src/segmentOverlay.test.ts +0 -70
- package/src/seqPosToGlobalCol.test.ts +0 -59
- package/src/sequenceLogo.test.ts +0 -88
- package/src/setFontSize.test.ts +0 -33
- package/src/stripDefault.test.ts +0 -48
- package/src/stripDefaultSnapshot.test.ts +0 -87
- package/src/treeMetadata.test.ts +0 -39
- package/src/useCanvasAutorun.test.tsx +0 -98
- package/src/useColorContrast.ts +0 -18
- package/src/viewChrome.test.tsx +0 -134
- package/src/wheelZoom.test.ts +0 -46
|
@@ -63,6 +63,20 @@ export function cssColorToPixel(css: string) {
|
|
|
63
63
|
return packed
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
function cellPixelFn(spec: RasterSpec, toPixel: (css: string) => number) {
|
|
67
|
+
const { columns, relativeTo, colorAt, bg, hover } = spec
|
|
68
|
+
const bgPixel = toPixel(bg)
|
|
69
|
+
const hoverPixel = toPixel(hover)
|
|
70
|
+
const reference = relativeTo ? columns.get(relativeTo) : undefined
|
|
71
|
+
return (name: string, col: number, letter: string) => {
|
|
72
|
+
if (reference && name !== relativeTo && letter === reference[col]) {
|
|
73
|
+
return hoverPixel
|
|
74
|
+
}
|
|
75
|
+
const color = colorAt(col, letter)
|
|
76
|
+
return color === undefined ? bgPixel : toPixel(color)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
66
80
|
/**
|
|
67
81
|
* Packs a rectangle of the alignment into one pixel per cell, in `rowNames`
|
|
68
82
|
* order, ready to hand to putImageData. Cells past the end of a row, and rows
|
|
@@ -70,7 +84,9 @@ export function cssColorToPixel(css: string) {
|
|
|
70
84
|
* also leaves behind.
|
|
71
85
|
*
|
|
72
86
|
* `colStep`/`rowStep` sample instead of covering, which is how the minimap fits
|
|
73
|
-
* a whole alignment into a 12px bar.
|
|
87
|
+
* a whole alignment into a 12px bar. `colSpan`/`rowSpan` average each block of
|
|
88
|
+
* that many cells into one pixel instead, so a zoom below a device pixel per
|
|
89
|
+
* column still shows every column's color without blending neighbouring rows.
|
|
74
90
|
*/
|
|
75
91
|
export function rasterPixels({
|
|
76
92
|
spec,
|
|
@@ -80,7 +96,10 @@ export function rasterPixels({
|
|
|
80
96
|
height,
|
|
81
97
|
colStep = 1,
|
|
82
98
|
rowStep = 1,
|
|
99
|
+
colSpan = 1,
|
|
100
|
+
rowSpan = 1,
|
|
83
101
|
toPixel = cssColorToPixel,
|
|
102
|
+
out = new Uint32Array(width * height),
|
|
84
103
|
}: {
|
|
85
104
|
spec: RasterSpec
|
|
86
105
|
col0: number
|
|
@@ -89,13 +108,27 @@ export function rasterPixels({
|
|
|
89
108
|
height: number
|
|
90
109
|
colStep?: number
|
|
91
110
|
rowStep?: number
|
|
111
|
+
colSpan?: number
|
|
112
|
+
rowSpan?: number
|
|
92
113
|
toPixel?: (css: string) => number
|
|
114
|
+
out?: Uint32Array
|
|
93
115
|
}) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
116
|
+
if (colSpan > 1 || rowSpan > 1) {
|
|
117
|
+
averagePixels({
|
|
118
|
+
spec,
|
|
119
|
+
col0,
|
|
120
|
+
row0,
|
|
121
|
+
width,
|
|
122
|
+
height,
|
|
123
|
+
colSpan,
|
|
124
|
+
rowSpan,
|
|
125
|
+
toPixel,
|
|
126
|
+
out,
|
|
127
|
+
})
|
|
128
|
+
return out
|
|
129
|
+
}
|
|
130
|
+
const { rowNames, columns } = spec
|
|
131
|
+
const cellPixel = cellPixelFn(spec, toPixel)
|
|
99
132
|
|
|
100
133
|
for (let r = 0; r < height; r++) {
|
|
101
134
|
const name = rowNames[row0 + r * rowStep]
|
|
@@ -106,7 +139,6 @@ export function rasterPixels({
|
|
|
106
139
|
if (!str) {
|
|
107
140
|
continue
|
|
108
141
|
}
|
|
109
|
-
const isReference = name === relativeTo
|
|
110
142
|
const base = r * width
|
|
111
143
|
for (let c = 0; c < width; c++) {
|
|
112
144
|
const col = col0 + c * colStep
|
|
@@ -114,21 +146,92 @@ export function rasterPixels({
|
|
|
114
146
|
if (letter === undefined) {
|
|
115
147
|
break
|
|
116
148
|
}
|
|
117
|
-
|
|
118
|
-
out[base + c] = hoverPixel
|
|
119
|
-
} else {
|
|
120
|
-
const color = colorAt(col, letter)
|
|
121
|
-
out[base + c] = color === undefined ? bgPixel : toPixel(color)
|
|
122
|
-
}
|
|
149
|
+
out[base + c] = cellPixel(name, col, letter)
|
|
123
150
|
}
|
|
124
151
|
}
|
|
125
152
|
return out
|
|
126
153
|
}
|
|
127
154
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
155
|
+
function averagePixels({
|
|
156
|
+
spec,
|
|
157
|
+
col0,
|
|
158
|
+
row0,
|
|
159
|
+
width,
|
|
160
|
+
height,
|
|
161
|
+
colSpan,
|
|
162
|
+
rowSpan,
|
|
163
|
+
toPixel,
|
|
164
|
+
out,
|
|
165
|
+
}: {
|
|
166
|
+
spec: RasterSpec
|
|
167
|
+
col0: number
|
|
168
|
+
row0: number
|
|
169
|
+
width: number
|
|
170
|
+
height: number
|
|
171
|
+
colSpan: number
|
|
172
|
+
rowSpan: number
|
|
173
|
+
toPixel: (css: string) => number
|
|
174
|
+
out: Uint32Array
|
|
175
|
+
}) {
|
|
176
|
+
const { rowNames, columns } = spec
|
|
177
|
+
const cellPixel = cellPixelFn(spec, toPixel)
|
|
178
|
+
const cells = colSpan * rowSpan
|
|
179
|
+
const rShift = littleEndian ? 0 : 24
|
|
180
|
+
const gShift = littleEndian ? 8 : 16
|
|
181
|
+
const bShift = littleEndian ? 16 : 8
|
|
182
|
+
const aShift = littleEndian ? 24 : 0
|
|
183
|
+
const names: string[] = []
|
|
184
|
+
const strs: string[] = []
|
|
185
|
+
|
|
186
|
+
for (let r = 0; r < height; r++) {
|
|
187
|
+
names.length = 0
|
|
188
|
+
strs.length = 0
|
|
189
|
+
for (let dr = 0; dr < rowSpan; dr++) {
|
|
190
|
+
const name = rowNames[row0 + r * rowSpan + dr]
|
|
191
|
+
const str = name === undefined ? undefined : columns.get(name)
|
|
192
|
+
if (name !== undefined && str) {
|
|
193
|
+
names.push(name)
|
|
194
|
+
strs.push(str)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const base = r * width
|
|
198
|
+
for (let c = 0; c < width; c++) {
|
|
199
|
+
// premultiplied, so a cell past the end of a row dilutes the alpha of its
|
|
200
|
+
// pixel rather than darkening its color
|
|
201
|
+
let sumR = 0
|
|
202
|
+
let sumG = 0
|
|
203
|
+
let sumB = 0
|
|
204
|
+
let sumA = 0
|
|
205
|
+
for (let i = 0; i < strs.length; i++) {
|
|
206
|
+
const str = strs[i]!
|
|
207
|
+
const name = names[i]!
|
|
208
|
+
for (let dc = 0; dc < colSpan; dc++) {
|
|
209
|
+
const col = col0 + c * colSpan + dc
|
|
210
|
+
const letter = str[col]
|
|
211
|
+
if (letter === undefined) {
|
|
212
|
+
break
|
|
213
|
+
}
|
|
214
|
+
const p = cellPixel(name, col, letter)
|
|
215
|
+
const a = (p >>> aShift) & 255
|
|
216
|
+
sumR += ((p >>> rShift) & 255) * a
|
|
217
|
+
sumG += ((p >>> gShift) & 255) * a
|
|
218
|
+
sumB += ((p >>> bShift) & 255) * a
|
|
219
|
+
sumA += a
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
out[base + c] =
|
|
223
|
+
sumA === 0
|
|
224
|
+
? 0
|
|
225
|
+
: packRgba(
|
|
226
|
+
Math.round(sumR / sumA),
|
|
227
|
+
Math.round(sumG / sumA),
|
|
228
|
+
Math.round(sumB / sumA),
|
|
229
|
+
Math.round(sumA / cells),
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
132
235
|
function usableRasterCtx(ctx: unknown): ctx is RasterCtx {
|
|
133
236
|
const c = ctx as Record<string, unknown> | null
|
|
134
237
|
return (
|
|
@@ -154,13 +257,17 @@ function makeRasterCanvas(width: number, height: number) {
|
|
|
154
257
|
: undefined
|
|
155
258
|
}
|
|
156
259
|
|
|
157
|
-
function paint(
|
|
260
|
+
function paint(
|
|
261
|
+
width: number,
|
|
262
|
+
height: number,
|
|
263
|
+
fill: (out: Uint32Array) => void,
|
|
264
|
+
) {
|
|
158
265
|
const made = makeRasterCanvas(width, height)
|
|
159
266
|
if (!made) {
|
|
160
267
|
return undefined
|
|
161
268
|
}
|
|
162
269
|
const image = made.ctx.createImageData(width, height)
|
|
163
|
-
new Uint32Array(image.data.buffer)
|
|
270
|
+
fill(new Uint32Array(image.data.buffer))
|
|
164
271
|
made.ctx.putImageData(image, 0, 0)
|
|
165
272
|
return made.canvas
|
|
166
273
|
}
|
|
@@ -223,8 +330,29 @@ function getCache(model: MsaViewModel, theme: Theme) {
|
|
|
223
330
|
return next
|
|
224
331
|
}
|
|
225
332
|
|
|
226
|
-
|
|
227
|
-
|
|
333
|
+
// Cells averaged into one tile pixel along an axis: the smallest power of two
|
|
334
|
+
// that brings a pixel back to at least one device pixel. Powers of two keep the
|
|
335
|
+
// number of distinct tile sets a zoom sweep builds to a handful.
|
|
336
|
+
function cellsPerPixel(devicePixelsPerCell: number) {
|
|
337
|
+
return devicePixelsPerCell >= 1
|
|
338
|
+
? 1
|
|
339
|
+
: 2 ** Math.ceil(Math.log2(1 / devicePixelsPerCell))
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function getTile({
|
|
343
|
+
cache,
|
|
344
|
+
tileRow,
|
|
345
|
+
tileCol,
|
|
346
|
+
colSpan,
|
|
347
|
+
rowSpan,
|
|
348
|
+
}: {
|
|
349
|
+
cache: RasterCache
|
|
350
|
+
tileRow: number
|
|
351
|
+
tileCol: number
|
|
352
|
+
colSpan: number
|
|
353
|
+
rowSpan: number
|
|
354
|
+
}) {
|
|
355
|
+
const key = `${colSpan}_${rowSpan}_${tileRow}_${tileCol}`
|
|
228
356
|
const hit = cache.tiles.get(key)
|
|
229
357
|
if (hit) {
|
|
230
358
|
// re-inserting makes this the newest entry: a Map iterates in insertion
|
|
@@ -233,18 +361,31 @@ function getTile(cache: RasterCache, tileRow: number, tileCol: number) {
|
|
|
233
361
|
cache.tiles.set(key, hit)
|
|
234
362
|
return hit
|
|
235
363
|
}
|
|
236
|
-
const col0 = tileCol * rasterTileSize
|
|
237
|
-
const row0 = tileRow * rasterTileSize
|
|
238
|
-
const width = Math.min(
|
|
239
|
-
|
|
364
|
+
const col0 = tileCol * rasterTileSize * colSpan
|
|
365
|
+
const row0 = tileRow * rasterTileSize * rowSpan
|
|
366
|
+
const width = Math.min(
|
|
367
|
+
rasterTileSize,
|
|
368
|
+
Math.ceil((cache.numColumns - col0) / colSpan),
|
|
369
|
+
)
|
|
370
|
+
const height = Math.min(
|
|
371
|
+
rasterTileSize,
|
|
372
|
+
Math.ceil((cache.numRows - row0) / rowSpan),
|
|
373
|
+
)
|
|
240
374
|
if (width <= 0 || height <= 0) {
|
|
241
375
|
return undefined
|
|
242
376
|
}
|
|
243
|
-
const canvas = paint(
|
|
244
|
-
rasterPixels({
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
377
|
+
const canvas = paint(width, height, out => {
|
|
378
|
+
rasterPixels({
|
|
379
|
+
spec: cache.spec,
|
|
380
|
+
col0,
|
|
381
|
+
row0,
|
|
382
|
+
width,
|
|
383
|
+
height,
|
|
384
|
+
colSpan,
|
|
385
|
+
rowSpan,
|
|
386
|
+
out,
|
|
387
|
+
})
|
|
388
|
+
})
|
|
248
389
|
if (!canvas) {
|
|
249
390
|
return undefined
|
|
250
391
|
}
|
|
@@ -263,6 +404,11 @@ function getTile(cache: RasterCache, tileRow: number, tileCol: number) {
|
|
|
263
404
|
* one-pixel-per-cell tiles. The tiles do not depend on the zoom level, so a zoom
|
|
264
405
|
* frame costs a handful of drawImage calls instead of a fillRect per visible
|
|
265
406
|
* cell -- which ran to 885ms per block at the minimum column width.
|
|
407
|
+
*
|
|
408
|
+
* Below a device pixel per cell the tiles average cells together instead, and
|
|
409
|
+
* only along the axis that needs it. Leaving the downsampling to the browser's
|
|
410
|
+
* image smoothing blurred both axes at once: at fit-to-width, where only the
|
|
411
|
+
* columns are narrow, every row boundary turned into a gradient.
|
|
266
412
|
*/
|
|
267
413
|
export function drawMsaRaster({
|
|
268
414
|
ctx,
|
|
@@ -279,17 +425,18 @@ export function drawMsaRaster({
|
|
|
279
425
|
}) {
|
|
280
426
|
const { colWidth, rowHeight, blockSize, highResScaleFactor } = model
|
|
281
427
|
const cache = getCache(model, theme)
|
|
428
|
+
const colSpan = cellsPerPixel(colWidth * highResScaleFactor)
|
|
429
|
+
const rowSpan = cellsPerPixel(rowHeight * highResScaleFactor)
|
|
282
430
|
|
|
283
431
|
ctx.resetTransform()
|
|
284
432
|
ctx.scale(highResScaleFactor, highResScaleFactor)
|
|
285
433
|
ctx.translate(-offsetX, -offsetY)
|
|
286
|
-
|
|
287
|
-
// for, so let the browser downsample it rather than letting whichever cell
|
|
288
|
-
// landed last win
|
|
289
|
-
ctx.imageSmoothingEnabled = colWidth < 1 || rowHeight < 1
|
|
434
|
+
ctx.imageSmoothingEnabled = false
|
|
290
435
|
|
|
291
|
-
const
|
|
292
|
-
const
|
|
436
|
+
const pixelWidth = colWidth * colSpan
|
|
437
|
+
const pixelHeight = rowHeight * rowSpan
|
|
438
|
+
const tileWidth = rasterTileSize * pixelWidth
|
|
439
|
+
const tileHeight = rasterTileSize * pixelHeight
|
|
293
440
|
const firstCol = Math.max(0, Math.floor(offsetX / tileWidth))
|
|
294
441
|
const lastCol = Math.floor((offsetX + blockSize) / tileWidth)
|
|
295
442
|
const firstRow = Math.max(0, Math.floor(offsetY / tileHeight))
|
|
@@ -297,7 +444,7 @@ export function drawMsaRaster({
|
|
|
297
444
|
|
|
298
445
|
for (let tileRow = firstRow; tileRow <= lastRow; tileRow++) {
|
|
299
446
|
for (let tileCol = firstCol; tileCol <= lastCol; tileCol++) {
|
|
300
|
-
const tile = getTile(cache, tileRow, tileCol)
|
|
447
|
+
const tile = getTile({ cache, tileRow, tileCol, colSpan, rowSpan })
|
|
301
448
|
if (tile) {
|
|
302
449
|
ctx.drawImage(
|
|
303
450
|
tile,
|
|
@@ -307,8 +454,8 @@ export function drawMsaRaster({
|
|
|
307
454
|
tile.height,
|
|
308
455
|
tileCol * tileWidth,
|
|
309
456
|
tileRow * tileHeight,
|
|
310
|
-
tile.width *
|
|
311
|
-
tile.height *
|
|
457
|
+
tile.width * pixelWidth,
|
|
458
|
+
tile.height * pixelHeight,
|
|
312
459
|
)
|
|
313
460
|
}
|
|
314
461
|
}
|
|
@@ -353,7 +500,9 @@ export function canvasHref(canvas: RasterCanvas | undefined) {
|
|
|
353
500
|
}
|
|
354
501
|
|
|
355
502
|
// A canvas much past this many pixels fails to allocate, and the browser's own
|
|
356
|
-
// per-side limit is lower still, so a raster bigger than either
|
|
503
|
+
// per-side limit is lower still, so a raster bigger than either averages down,
|
|
504
|
+
// per axis: an alignment wide enough to hit the limit on columns alone keeps
|
|
505
|
+
// every row.
|
|
357
506
|
// The result is still drawn across the same rectangle -- it loses cell-exact
|
|
358
507
|
// detail at a size where no figure could show it anyway.
|
|
359
508
|
const maxImagePixels = 64e6
|
|
@@ -391,31 +540,40 @@ export function rasterImageHref({
|
|
|
391
540
|
return undefined
|
|
392
541
|
}
|
|
393
542
|
const cache = getCache(model, theme)
|
|
394
|
-
let
|
|
543
|
+
let colSpan = 1
|
|
544
|
+
let rowSpan = 1
|
|
545
|
+
while (Math.ceil(numCols / colSpan) > maxImageSide) {
|
|
546
|
+
colSpan *= 2
|
|
547
|
+
}
|
|
548
|
+
while (Math.ceil(numRows / rowSpan) > maxImageSide) {
|
|
549
|
+
rowSpan *= 2
|
|
550
|
+
}
|
|
395
551
|
while (
|
|
396
|
-
Math.ceil(numCols /
|
|
397
|
-
|
|
398
|
-
Math.ceil(numRows / step) > maxImageSide
|
|
552
|
+
Math.ceil(numCols / colSpan) * Math.ceil(numRows / rowSpan) >
|
|
553
|
+
maxImagePixels
|
|
399
554
|
) {
|
|
400
|
-
|
|
555
|
+
if (numCols / colSpan >= numRows / rowSpan) {
|
|
556
|
+
colSpan *= 2
|
|
557
|
+
} else {
|
|
558
|
+
rowSpan *= 2
|
|
559
|
+
}
|
|
401
560
|
}
|
|
402
|
-
const width = Math.ceil(numCols /
|
|
403
|
-
const height = Math.ceil(numRows /
|
|
561
|
+
const width = Math.ceil(numCols / colSpan)
|
|
562
|
+
const height = Math.ceil(numRows / rowSpan)
|
|
404
563
|
|
|
405
564
|
return canvasHref(
|
|
406
|
-
paint(
|
|
565
|
+
paint(width, height, out => {
|
|
407
566
|
rasterPixels({
|
|
408
567
|
spec: cache.spec,
|
|
409
568
|
col0,
|
|
410
569
|
row0,
|
|
411
570
|
width,
|
|
412
571
|
height,
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
),
|
|
572
|
+
colSpan,
|
|
573
|
+
rowSpan,
|
|
574
|
+
out,
|
|
575
|
+
})
|
|
576
|
+
}),
|
|
419
577
|
)
|
|
420
578
|
}
|
|
421
579
|
|
|
@@ -447,7 +605,7 @@ export function msaThumbnail({
|
|
|
447
605
|
const h = Math.ceil(numRows / rowStep)
|
|
448
606
|
cache.thumbnail = {
|
|
449
607
|
height,
|
|
450
|
-
canvas: paint(
|
|
608
|
+
canvas: paint(w, h, out => {
|
|
451
609
|
rasterPixels({
|
|
452
610
|
spec: cache.spec,
|
|
453
611
|
col0: 0,
|
|
@@ -456,10 +614,9 @@ export function msaThumbnail({
|
|
|
456
614
|
height: h,
|
|
457
615
|
colStep,
|
|
458
616
|
rowStep,
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
),
|
|
617
|
+
out,
|
|
618
|
+
})
|
|
619
|
+
}),
|
|
463
620
|
}
|
|
464
621
|
}
|
|
465
622
|
return cache.thumbnail.canvas
|
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
import { setFontSize } from '../../setFontSize.ts'
|
|
2
|
+
import { referenceColor } from '../overlayColors.ts'
|
|
2
3
|
|
|
3
4
|
import type { MsaViewModel } from '../../model.ts'
|
|
4
5
|
import type { RenderCtx } from '../renderCtx.ts'
|
|
5
6
|
import type { Theme } from '@mui/material'
|
|
6
7
|
|
|
8
|
+
// a stronger fill plus a solid border, so a domain/motif band reads clearly over
|
|
9
|
+
// the colored alignment cells (the faint hover-style wash alone is invisible
|
|
10
|
+
// against clustalx coloring)
|
|
7
11
|
export const highlightFill = 'rgba(255,140,0,0.28)'
|
|
8
12
|
export const highlightBorder = 'rgba(210,90,0,0.95)'
|
|
9
13
|
export const highlightRowFill = 'rgba(255,140,0,0.18)'
|
|
10
14
|
|
|
11
15
|
export const labelFontSize = 11
|
|
12
16
|
const labelPad = 3
|
|
17
|
+
// the label box a highlight draws above its band, and the reason a tree block
|
|
18
|
+
// has to consider rows a little outside itself
|
|
19
|
+
export const highlightLabelHeight = labelFontSize + labelPad * 2
|
|
13
20
|
|
|
21
|
+
export interface LabelBox {
|
|
22
|
+
left: number
|
|
23
|
+
right: number
|
|
24
|
+
top: number
|
|
25
|
+
bottom: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A highlight's label, over its band where it fits and beside it where it does
|
|
30
|
+
* not, kept inside `bounds` and out of the way of the labels already placed.
|
|
31
|
+
*
|
|
32
|
+
* Two single-residue highlights a column apart both want the same few pixels to
|
|
33
|
+
* the right of their bands, and a band scrolled off the left edge wants its
|
|
34
|
+
* label off-frame with it: the first overdrew two labels into an unreadable
|
|
35
|
+
* smudge, the second left the visible band untitled. Pass `placed` and each
|
|
36
|
+
* later label steps down a row instead of over the one before.
|
|
37
|
+
*/
|
|
14
38
|
export function drawHighlightLabel({
|
|
15
39
|
ctx,
|
|
16
40
|
theme,
|
|
@@ -18,6 +42,8 @@ export function drawHighlightLabel({
|
|
|
18
42
|
x,
|
|
19
43
|
y,
|
|
20
44
|
spanWidth,
|
|
45
|
+
bounds,
|
|
46
|
+
placed,
|
|
21
47
|
}: {
|
|
22
48
|
ctx: RenderCtx
|
|
23
49
|
theme: Theme
|
|
@@ -25,16 +51,33 @@ export function drawHighlightLabel({
|
|
|
25
51
|
x: number
|
|
26
52
|
y: number
|
|
27
53
|
spanWidth: number
|
|
54
|
+
bounds?: { min: number; max: number }
|
|
55
|
+
placed?: LabelBox[]
|
|
28
56
|
}) {
|
|
29
57
|
setFontSize(ctx, labelFontSize)
|
|
30
58
|
ctx.textAlign = 'start'
|
|
31
59
|
const boxWidth = ctx.measureText(label).width + labelPad * 2
|
|
32
|
-
const boxHeight =
|
|
33
|
-
|
|
60
|
+
const boxHeight = highlightLabelHeight
|
|
61
|
+
let left = boxWidth <= spanWidth ? x : x + spanWidth + 2
|
|
62
|
+
if (bounds) {
|
|
63
|
+
left = Math.max(bounds.min, Math.min(left, bounds.max - boxWidth))
|
|
64
|
+
}
|
|
65
|
+
let top = y
|
|
66
|
+
if (placed) {
|
|
67
|
+
const overlaps = (box: LabelBox) =>
|
|
68
|
+
box.left < left + boxWidth &&
|
|
69
|
+
box.right > left &&
|
|
70
|
+
box.top < top + boxHeight &&
|
|
71
|
+
box.bottom > top
|
|
72
|
+
while (placed.some(overlaps)) {
|
|
73
|
+
top += boxHeight
|
|
74
|
+
}
|
|
75
|
+
placed.push({ left, right: left + boxWidth, top, bottom: top + boxHeight })
|
|
76
|
+
}
|
|
34
77
|
ctx.fillStyle = theme.palette.background.paper
|
|
35
|
-
ctx.fillRect(left,
|
|
78
|
+
ctx.fillRect(left, top, boxWidth, boxHeight)
|
|
36
79
|
ctx.fillStyle = theme.palette.text.primary
|
|
37
|
-
ctx.fillText(label, left + labelPad,
|
|
80
|
+
ctx.fillText(label, left + labelPad, top + labelPad + labelFontSize - 2)
|
|
38
81
|
}
|
|
39
82
|
|
|
40
83
|
/**
|
|
@@ -62,6 +105,7 @@ export function renderHighlights({
|
|
|
62
105
|
height: number
|
|
63
106
|
}) {
|
|
64
107
|
const { resolvedHighlights, colWidth, rowHeight } = model
|
|
108
|
+
const placed: LabelBox[] = []
|
|
65
109
|
ctx.lineWidth = 2
|
|
66
110
|
for (const { rowIndices, color } of resolvedHighlights) {
|
|
67
111
|
ctx.fillStyle = color ?? highlightRowFill
|
|
@@ -80,7 +124,61 @@ export function renderHighlights({
|
|
|
80
124
|
ctx.strokeStyle = color ?? highlightBorder
|
|
81
125
|
ctx.strokeRect(x, 0, spanWidth, height)
|
|
82
126
|
if (label) {
|
|
83
|
-
drawHighlightLabel({
|
|
127
|
+
drawHighlightLabel({
|
|
128
|
+
ctx,
|
|
129
|
+
theme,
|
|
130
|
+
label,
|
|
131
|
+
x,
|
|
132
|
+
y: 0,
|
|
133
|
+
spanWidth,
|
|
134
|
+
bounds: { min: 0, max: width },
|
|
135
|
+
placed,
|
|
136
|
+
})
|
|
84
137
|
}
|
|
85
138
|
}
|
|
86
139
|
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Everything the alignment overlay owes to the document rather than to the
|
|
143
|
+
* mouse: the reference row's tint, a bordered band per run of highlighted
|
|
144
|
+
* columns, and the `highlights` layer. The live overlay canvas draws these
|
|
145
|
+
* under its hover and click bands, and the SVG export draws them and stops --
|
|
146
|
+
* a shared link that opens with columns highlighted exports with them too.
|
|
147
|
+
*/
|
|
148
|
+
export function renderPersistentHighlights({
|
|
149
|
+
ctx,
|
|
150
|
+
model,
|
|
151
|
+
theme,
|
|
152
|
+
offsetX,
|
|
153
|
+
offsetY,
|
|
154
|
+
width,
|
|
155
|
+
height,
|
|
156
|
+
}: {
|
|
157
|
+
ctx: RenderCtx
|
|
158
|
+
model: MsaViewModel
|
|
159
|
+
theme: Theme
|
|
160
|
+
offsetX: number
|
|
161
|
+
offsetY: number
|
|
162
|
+
width: number
|
|
163
|
+
height: number
|
|
164
|
+
}) {
|
|
165
|
+
const { colWidth, rowHeight, referenceRowIndex, highlightedColumnRuns } =
|
|
166
|
+
model
|
|
167
|
+
|
|
168
|
+
if (referenceRowIndex !== undefined) {
|
|
169
|
+
ctx.fillStyle = referenceColor
|
|
170
|
+
ctx.fillRect(0, referenceRowIndex * rowHeight - offsetY, width, rowHeight)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
ctx.lineWidth = 2
|
|
174
|
+
for (const { start, end } of highlightedColumnRuns) {
|
|
175
|
+
const x = start * colWidth - offsetX
|
|
176
|
+
const w = (end - start + 1) * colWidth
|
|
177
|
+
ctx.fillStyle = highlightFill
|
|
178
|
+
ctx.fillRect(x, 0, w, height)
|
|
179
|
+
ctx.strokeStyle = highlightBorder
|
|
180
|
+
ctx.strokeRect(x, 0, w, height)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
renderHighlights({ ctx, model, theme, offsetX, offsetY, width, height })
|
|
184
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { setFontSize } from '../../setFontSize.ts'
|
|
2
|
-
import { adjustColorForContrast } from '../../util.ts'
|
|
2
|
+
import { adjustColorForContrast, contrastTextFn } from '../../util.ts'
|
|
3
3
|
import { getVisibleLeaves } from '../getVisibleLeaves.ts'
|
|
4
4
|
import { domainBandCursor } from './domainBandCursor.ts'
|
|
5
5
|
import { tileColorFn } from './tileColor.ts'
|
|
@@ -15,7 +15,6 @@ export function renderMSABlock({
|
|
|
15
15
|
model,
|
|
16
16
|
offsetX,
|
|
17
17
|
offsetY,
|
|
18
|
-
contrastScheme,
|
|
19
18
|
ctx,
|
|
20
19
|
theme,
|
|
21
20
|
highResScaleFactorOverride,
|
|
@@ -27,7 +26,6 @@ export function renderMSABlock({
|
|
|
27
26
|
offsetY: number
|
|
28
27
|
theme: Theme
|
|
29
28
|
model: MsaViewModel
|
|
30
|
-
contrastScheme: Record<string, string>
|
|
31
29
|
ctx: RenderCtx
|
|
32
30
|
highResScaleFactorOverride?: number
|
|
33
31
|
blockSizeXOverride?: number
|
|
@@ -68,7 +66,6 @@ export function renderMSABlock({
|
|
|
68
66
|
model,
|
|
69
67
|
ctx,
|
|
70
68
|
theme,
|
|
71
|
-
contrastScheme,
|
|
72
69
|
xStart,
|
|
73
70
|
xEnd,
|
|
74
71
|
visibleLeaves,
|
|
@@ -90,23 +87,10 @@ export function renderMSABlock({
|
|
|
90
87
|
ctx.resetTransform()
|
|
91
88
|
}
|
|
92
89
|
|
|
93
|
-
// Letter color to use over a domain box, keyed by accession. Domain fills come
|
|
94
|
-
// from a categorical palette that has no fixed lightness, so ask the theme for a
|
|
95
|
-
// readable text color per fill instead of assuming dark-on-light.
|
|
96
|
-
function domainLetterColors(model: MsaViewModel, theme: Theme) {
|
|
97
|
-
return new Map(
|
|
98
|
-
Object.entries(model.fillPalette).map(([accession, fill]) => [
|
|
99
|
-
accession,
|
|
100
|
-
theme.palette.getContrastText(fill),
|
|
101
|
-
]),
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
90
|
function drawTilesAndText({
|
|
106
91
|
model,
|
|
107
92
|
ctx,
|
|
108
93
|
theme,
|
|
109
|
-
contrastScheme,
|
|
110
94
|
visibleLeaves,
|
|
111
95
|
xStart,
|
|
112
96
|
xEnd,
|
|
@@ -117,7 +101,6 @@ function drawTilesAndText({
|
|
|
117
101
|
model: MsaViewModel
|
|
118
102
|
theme: Theme
|
|
119
103
|
ctx: RenderCtx
|
|
120
|
-
contrastScheme: Record<string, string>
|
|
121
104
|
visibleLeaves: HierarchyNode<NodeWithIdsAndLength>[]
|
|
122
105
|
xStart: number
|
|
123
106
|
xEnd: number
|
|
@@ -139,10 +122,14 @@ function drawTilesAndText({
|
|
|
139
122
|
const tiles = drawTiles && bgColor
|
|
140
123
|
const paintTiles = tiles && !rasterTiles
|
|
141
124
|
if (paintTiles || showMsaLetters) {
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
const needsColor = paintTiles ||
|
|
125
|
+
// a letter takes its color from the cell it lands on, so the scheme lookup
|
|
126
|
+
// is only waste when nothing draws letters
|
|
127
|
+
const needsColor = paintTiles || showMsaLetters
|
|
145
128
|
const tileColor = tileColorFn(model)
|
|
129
|
+
// Domain fills come from a categorical palette with no fixed lightness, and
|
|
130
|
+
// a dynamic scheme colors a cell from its column, so a readable letter color
|
|
131
|
+
// is a question about the cell's own color rather than about its letter.
|
|
132
|
+
const contrastText = contrastTextFn(theme)
|
|
146
133
|
const offsetXAligned = xStart * colWidth
|
|
147
134
|
const halfColWidth = colWidth / 2
|
|
148
135
|
// note: -rowHeight/4 matches +rowHeight/4 in tree
|
|
@@ -151,9 +138,6 @@ function drawTilesAndText({
|
|
|
151
138
|
// on a domain box or on the plain background; sub-row layout stacks the boxes
|
|
152
139
|
// above the letters, so those rows are all plain background
|
|
153
140
|
const overDomains = !drawTiles && !subFeatureRows
|
|
154
|
-
const domainColors = overDomains
|
|
155
|
-
? domainLetterColors(model, theme)
|
|
156
|
-
: undefined
|
|
157
141
|
|
|
158
142
|
for (let i = 0, l1 = visibleLeaves.length; i < l1; i++) {
|
|
159
143
|
const node = visibleLeaves[i]!
|
|
@@ -186,18 +170,23 @@ function drawTilesAndText({
|
|
|
186
170
|
const covering = bandAt(col)
|
|
187
171
|
ctx.fillStyle = covering
|
|
188
172
|
? // on top of a domain box: contrast against the box fill
|
|
189
|
-
|
|
190
|
-
:
|
|
191
|
-
? // on
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
173
|
+
contrastText(model.fillPalette[covering.annotation.accession])
|
|
174
|
+
: isMatchingReference
|
|
175
|
+
? // the dot sits on the faint reference-match wash, which is
|
|
176
|
+
// the theme's background with a little of its text color in it
|
|
177
|
+
theme.palette.text.primary
|
|
178
|
+
: tiles
|
|
179
|
+
? // on top of a colored tile, or on the background where the
|
|
180
|
+
// scheme gives that cell no color
|
|
181
|
+
contrastText(color)
|
|
182
|
+
: !drawTiles || !color
|
|
183
|
+
? // plain background, uncolored letters
|
|
184
|
+
theme.palette.text.primary
|
|
185
|
+
: // letter-color mode: darken/lighten to stay readable
|
|
186
|
+
adjustColorForContrast(
|
|
187
|
+
color,
|
|
188
|
+
theme.palette.background.default,
|
|
189
|
+
)
|
|
201
190
|
ctx.fillText(
|
|
202
191
|
isMatchingReference ? '.' : letter,
|
|
203
192
|
x + halfColWidth,
|