react-msaview 5.9.0 → 6.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/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/components/ConservationTrack.js +21 -16
- package/dist/components/ConservationTrack.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/Loading.js +2 -2
- package/dist/components/Loading.js.map +1 -1
- package/dist/components/MSAView.js +18 -24
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/ResizeHandles.js +24 -40
- package/dist/components/ResizeHandles.js.map +1 -1
- package/dist/components/ScrollbarThumb.d.ts +14 -0
- package/dist/components/ScrollbarThumb.js +28 -0
- package/dist/components/ScrollbarThumb.js.map +1 -0
- package/dist/components/SequenceTextArea.js +3 -10
- package/dist/components/SequenceTextArea.js.map +1 -1
- package/dist/components/TextTrack.js +26 -21
- package/dist/components/TextTrack.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/{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 +39 -72
- 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 +11 -22
- 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 +2 -2
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +19 -16
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/MSAMouseoverCanvas.js +14 -8
- 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/renderBoxFeatureCanvasBlock.js +9 -6
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSABlock.js +14 -20
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +32 -33
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/overlayColors.d.ts +2 -0
- package/dist/components/overlayColors.js +5 -0
- package/dist/components/overlayColors.js.map +1 -1
- package/dist/components/tree/TreeCanvas.js +29 -24
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +19 -12
- 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.d.ts +0 -38
- package/dist/components/tree/renderTreeCanvas.js +17 -17
- package/dist/components/tree/renderTreeCanvas.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.d.ts +50 -100
- package/dist/model.js +209 -219
- package/dist/model.js.map +1 -1
- package/dist/neighborJoining.js +12 -19
- package/dist/neighborJoining.js.map +1 -1
- package/dist/parseAsn1.js +32 -43
- package/dist/parseAsn1.js.map +1 -1
- package/dist/renderToSvg.js +5 -2
- 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/seqPosToGlobalCol.d.ts +6 -19
- package/dist/seqPosToGlobalCol.js +6 -33
- package/dist/seqPosToGlobalCol.js.map +1 -1
- package/dist/types.d.ts +4 -11
- package/dist/useCanvasAutorun.d.ts +23 -1
- package/dist/useCanvasAutorun.js +20 -8
- package/dist/useCanvasAutorun.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -5
- package/src/clustalX.ts +22 -22
- package/src/colSpace.test.ts +27 -0
- package/src/components/ConservationTrack.tsx +7 -5
- package/src/components/CopyButton.tsx +44 -0
- package/src/components/Loading.tsx +10 -1
- package/src/components/MSAView.tsx +25 -32
- package/src/components/ResizeHandles.tsx +60 -69
- package/src/components/ScrollbarThumb.tsx +48 -0
- package/src/components/SequenceTextArea.tsx +3 -16
- package/src/components/TextTrack.tsx +7 -12
- package/src/components/VerticalScrollbar.tsx +19 -33
- package/src/components/dialogs/FeatureDialog.tsx +3 -4
- 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 +40 -72
- package/src/components/import/ImportForm.tsx +1 -1
- package/src/components/import/ImportFormExamples.tsx +82 -133
- package/src/components/minimap/Minimap.tsx +23 -31
- 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 +3 -10
- package/src/components/msa/MSACanvasBlock.tsx +16 -21
- package/src/components/msa/MSAMouseoverCanvas.tsx +11 -9
- package/src/components/msa/MSAPanel.tsx +2 -2
- package/src/components/msa/domainBandCursor.ts +41 -0
- package/src/components/msa/renderBoxFeatureCanvasBlock.test.ts +51 -0
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +13 -8
- package/src/components/msa/renderMSABlock.ts +19 -23
- package/src/components/msa/renderMSAMouseover.ts +38 -35
- package/src/components/overlayColors.ts +6 -0
- package/src/components/tree/TreeCanvas.tsx +6 -4
- package/src/components/tree/TreeCanvasBlock.tsx +11 -12
- package/src/components/tree/TreeRuler.tsx +7 -2
- package/src/components/tree/renderTreeCanvas.test.ts +15 -7
- package/src/components/tree/renderTreeCanvas.ts +20 -17
- 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/index.ts +4 -1
- package/src/largeTree.test.ts +23 -2
- package/src/launchInterProScan.ts +3 -30
- package/src/loadErrorDisplay.test.tsx +102 -0
- package/src/measureTextCanvas.ts +22 -6
- package/src/model.ts +233 -244
- package/src/modelFilehandleLoaders.test.ts +137 -0
- package/src/neighborJoining.test.ts +12 -0
- package/src/neighborJoining.ts +12 -20
- package/src/parseAsn1.ts +32 -40
- package/src/{renderDomainLegendSvg.test.tsx → renderDomainsSvg.test.tsx} +44 -20
- package/src/renderMinimapSvg.test.tsx +109 -0
- package/src/renderToSvg.tsx +5 -2
- package/src/rowCoordinateCalculations.ts +48 -44
- package/src/seqPosToGlobalCol.test.ts +39 -51
- package/src/seqPosToGlobalCol.ts +6 -41
- package/src/types.ts +10 -15
- package/src/useCanvasAutorun.test.tsx +98 -0
- package/src/useCanvasAutorun.ts +30 -11
- package/src/version.ts +1 -1
- package/src/viewChrome.test.tsx +134 -0
- 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/src/components/dialogs/DomainDialog.tsx +0 -45
- package/src/components/header/SettingsMenu.tsx +0 -57
|
@@ -31,24 +31,17 @@ const MSACanvasBlock = observer(function ({
|
|
|
31
31
|
},
|
|
32
32
|
)
|
|
33
33
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
const canvasSize = blockSize * highResScaleFactor
|
|
35
|
+
const ref = useCanvasAutorun({
|
|
36
|
+
draw: ctx => {
|
|
37
37
|
ctx.resetTransform()
|
|
38
|
-
ctx.clearRect(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
renderBoxFeatureCanvasBlock({
|
|
46
|
-
ctx,
|
|
47
|
-
offsetX,
|
|
48
|
-
offsetY,
|
|
49
|
-
model,
|
|
50
|
-
})
|
|
51
|
-
}
|
|
38
|
+
ctx.clearRect(0, 0, canvasSize, canvasSize)
|
|
39
|
+
renderBoxFeatureCanvasBlock({
|
|
40
|
+
ctx,
|
|
41
|
+
offsetX,
|
|
42
|
+
offsetY,
|
|
43
|
+
model,
|
|
44
|
+
})
|
|
52
45
|
renderMSABlock({
|
|
53
46
|
ctx,
|
|
54
47
|
theme,
|
|
@@ -58,8 +51,10 @@ const MSACanvasBlock = observer(function ({
|
|
|
58
51
|
model,
|
|
59
52
|
})
|
|
60
53
|
},
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
width: canvasSize,
|
|
55
|
+
height: canvasSize,
|
|
56
|
+
deps: [model, offsetX, offsetY, theme, contrastScheme],
|
|
57
|
+
})
|
|
63
58
|
|
|
64
59
|
const { hoveredInsertion, mouseOverDomains, showColumnStats } = model
|
|
65
60
|
|
|
@@ -80,8 +75,8 @@ const MSACanvasBlock = observer(function ({
|
|
|
80
75
|
onMouseLeave={() => {
|
|
81
76
|
onMouseLeave()
|
|
82
77
|
}}
|
|
83
|
-
width={
|
|
84
|
-
height={
|
|
78
|
+
width={canvasSize}
|
|
79
|
+
height={canvasSize}
|
|
85
80
|
style={{
|
|
86
81
|
position: 'absolute',
|
|
87
82
|
top: scrollY + offsetY,
|
|
@@ -13,24 +13,26 @@ const MSAMouseoverCanvas = observer(function ({
|
|
|
13
13
|
}: {
|
|
14
14
|
model: MsaViewModel
|
|
15
15
|
}) {
|
|
16
|
-
const { height,
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const ref = useCanvasAutorun(
|
|
20
|
-
ctx => {
|
|
16
|
+
const { height, msaCanvasWidth: width, highResScaleFactor } = model
|
|
17
|
+
const canvasWidth = width * highResScaleFactor
|
|
18
|
+
const canvasHeight = height * highResScaleFactor
|
|
19
|
+
const ref = useCanvasAutorun({
|
|
20
|
+
draw: ctx => {
|
|
21
21
|
if (isAlive(model)) {
|
|
22
22
|
renderMouseover({ ctx, model })
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
width: canvasWidth,
|
|
26
|
+
height: canvasHeight,
|
|
27
|
+
deps: [model],
|
|
28
|
+
})
|
|
27
29
|
|
|
28
30
|
return (
|
|
29
31
|
<canvas
|
|
30
32
|
ref={ref}
|
|
31
33
|
id="mouseover"
|
|
32
|
-
width={
|
|
33
|
-
height={
|
|
34
|
+
width={canvasWidth}
|
|
35
|
+
height={canvasHeight}
|
|
34
36
|
style={{
|
|
35
37
|
position: 'absolute',
|
|
36
38
|
top: 0,
|
|
@@ -2,7 +2,7 @@ import React from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { observer } from 'mobx-react'
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import AnnotationLegend from './AnnotationLegend.tsx'
|
|
6
6
|
import MSACanvas from './MSACanvas.tsx'
|
|
7
7
|
import MSAMouseoverCanvas from './MSAMouseoverCanvas.tsx'
|
|
8
8
|
|
|
@@ -14,7 +14,7 @@ const MSAPanel = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
14
14
|
<div style={{ position: 'relative' }}>
|
|
15
15
|
<MSACanvas model={model} />
|
|
16
16
|
<MSAMouseoverCanvas model={model} />
|
|
17
|
-
<
|
|
17
|
+
<AnnotationLegend model={model} />
|
|
18
18
|
</div>
|
|
19
19
|
)
|
|
20
20
|
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { DomainBand } from '../../types.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Left-to-right cursor over one row's domain bands, answering "which band is
|
|
5
|
+
* drawn on top at this column" — the fill a letter there has to contrast
|
|
6
|
+
* against.
|
|
7
|
+
*
|
|
8
|
+
* Not simply the first band covering the column. The overlay paints in
|
|
9
|
+
* `domainBands` order, largest first, so where signatures nest — a family
|
|
10
|
+
* enclosing a domain, which InterProScan emits routinely — the box a reader
|
|
11
|
+
* actually sees is the last one painted over that column. `stackIndex` is that
|
|
12
|
+
* paint order.
|
|
13
|
+
*
|
|
14
|
+
* `bands` must be sorted by start column (`domainBandsByStart`), and columns
|
|
15
|
+
* must be visited in increasing order, which is what the block renderer does.
|
|
16
|
+
* Both keep this to one pass over the bands per row rather than a scan per cell.
|
|
17
|
+
*/
|
|
18
|
+
export function domainBandCursor(bands: DomainBand[] | undefined) {
|
|
19
|
+
let next = 0
|
|
20
|
+
let active: DomainBand[] = []
|
|
21
|
+
|
|
22
|
+
return (col: number) => {
|
|
23
|
+
if (!bands) {
|
|
24
|
+
return undefined
|
|
25
|
+
}
|
|
26
|
+
while (next < bands.length && bands[next]!.startCol <= col) {
|
|
27
|
+
active.push(bands[next]!)
|
|
28
|
+
next++
|
|
29
|
+
}
|
|
30
|
+
if (active.some(band => band.endCol <= col)) {
|
|
31
|
+
active = active.filter(band => band.endCol > col)
|
|
32
|
+
}
|
|
33
|
+
let top: DomainBand | undefined
|
|
34
|
+
for (const band of active) {
|
|
35
|
+
if (!top || band.stackIndex > top.stackIndex) {
|
|
36
|
+
top = band
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return top
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { expect, test } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import stateModelFactory from '../../model.ts'
|
|
4
|
+
import { renderBoxFeatureCanvasBlock } from './renderBoxFeatureCanvasBlock.ts'
|
|
5
|
+
|
|
6
|
+
import type { RenderCtx } from '../renderCtx.ts'
|
|
7
|
+
|
|
8
|
+
const row = 'ACDEFGHIKLMNPQRSTVWY'.repeat(3)
|
|
9
|
+
|
|
10
|
+
// only `human` carries the gene model; `mouse` is there to push it off the top
|
|
11
|
+
// of the block
|
|
12
|
+
const msa = `>mouse\n${row}\n>human\n${row}`
|
|
13
|
+
|
|
14
|
+
const gff = `##gff-version 3
|
|
15
|
+
human src exon 1 20 . . . ID=human.exon1;Name=exon-1
|
|
16
|
+
human src exon 21 40 . . . ID=human.exon2;Name=exon-2
|
|
17
|
+
human src exon 41 60 . . . ID=human.exon3;Name=exon-3`
|
|
18
|
+
|
|
19
|
+
function drawnLabels() {
|
|
20
|
+
const model = stateModelFactory().create({
|
|
21
|
+
type: 'MsaView',
|
|
22
|
+
data: { msa },
|
|
23
|
+
})
|
|
24
|
+
model.setWidth(1000)
|
|
25
|
+
model.setRowHeight(20)
|
|
26
|
+
// segment numbers are drawn in place of residue letters, never on top of them
|
|
27
|
+
model.setDrawMsaLetters(false)
|
|
28
|
+
model.applyGFFText(gff)
|
|
29
|
+
expect(model.rowNames).toEqual(['mouse', 'human'])
|
|
30
|
+
|
|
31
|
+
const labels: string[] = []
|
|
32
|
+
const ctx = {
|
|
33
|
+
font: '12px sans-serif',
|
|
34
|
+
resetTransform() {},
|
|
35
|
+
scale() {},
|
|
36
|
+
translate() {},
|
|
37
|
+
fillRect() {},
|
|
38
|
+
strokeRect() {},
|
|
39
|
+
measureText: (text: string) => ({ width: text.length * 6 }),
|
|
40
|
+
fillText(text: string) {
|
|
41
|
+
labels.push(text)
|
|
42
|
+
},
|
|
43
|
+
} as unknown as RenderCtx
|
|
44
|
+
|
|
45
|
+
renderBoxFeatureCanvasBlock({ model, ctx, offsetX: 0, offsetY: 0 })
|
|
46
|
+
return labels
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test('segment numbers are drawn once each, even when row 0 has no gene model', () => {
|
|
50
|
+
expect(drawnLabels()).toEqual(['1', '2', '3'])
|
|
51
|
+
})
|
|
@@ -77,8 +77,13 @@ function drawTiles({
|
|
|
77
77
|
const xMin = offsetX - cull
|
|
78
78
|
const xMax = offsetX + blockWidth + cull
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
// a segment (exon) number labels its band once per block, on the topmost
|
|
81
|
+
// visible row carrying that segment, so it reads as a column header for the
|
|
82
|
+
// whole band. Keyed by accession rather than drawn on row 0, because the rows
|
|
83
|
+
// carrying the gene model are often not the first ones in the alignment
|
|
84
|
+
const labelled = drawSegmentLabels ? new Set<string>() : undefined
|
|
85
|
+
|
|
86
|
+
for (const node of visibleLeaves) {
|
|
82
87
|
const y = node.x!
|
|
83
88
|
const bands = domainBands.get(node.data.name)
|
|
84
89
|
|
|
@@ -94,15 +99,15 @@ function drawTiles({
|
|
|
94
99
|
if (strand === undefined) {
|
|
95
100
|
ctx.fillRect(x, t, lw, h)
|
|
96
101
|
ctx.strokeRect(x, t, lw, h)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (label !== undefined && i === 0) {
|
|
102
|
+
const label =
|
|
103
|
+
labelled && !labelled.has(accession)
|
|
104
|
+
? segmentLabels.get(accession)
|
|
105
|
+
: undefined
|
|
106
|
+
if (label !== undefined) {
|
|
103
107
|
const fontSize = Math.min(h - 2, 11)
|
|
104
108
|
ctx.font = `${fontSize}px sans-serif`
|
|
105
109
|
if (ctx.measureText(label).width + 2 <= lw) {
|
|
110
|
+
labelled!.add(accession)
|
|
106
111
|
ctx.fillStyle = '#222'
|
|
107
112
|
ctx.textAlign = 'center'
|
|
108
113
|
ctx.textBaseline = 'middle'
|
|
@@ -1,6 +1,7 @@
|
|
|
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'
|
|
4
5
|
import { visibleColRange } from './visibleColRange.ts'
|
|
5
6
|
|
|
6
7
|
import type { HierarchyNode } from '../../hierarchy.ts'
|
|
@@ -167,8 +168,9 @@ function drawTilesAndText({
|
|
|
167
168
|
if (str) {
|
|
168
169
|
const tileY = y - rowHeight
|
|
169
170
|
const textY = y - quarterRowHeight
|
|
170
|
-
const
|
|
171
|
-
|
|
171
|
+
const bandAt = domainBandCursor(
|
|
172
|
+
overDomains ? domainBandsByStart.get(name) : undefined,
|
|
173
|
+
)
|
|
172
174
|
|
|
173
175
|
for (let j = 0, l2 = str.length; j < l2; j++) {
|
|
174
176
|
const col = xStart + j
|
|
@@ -186,27 +188,21 @@ function drawTilesAndText({
|
|
|
186
188
|
}
|
|
187
189
|
|
|
188
190
|
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
|
-
)
|
|
191
|
+
const covering = bandAt(col)
|
|
192
|
+
ctx.fillStyle = covering
|
|
193
|
+
? // on top of a domain box: contrast against the box fill
|
|
194
|
+
domainColors!.get(covering.annotation.accession)!
|
|
195
|
+
: tiles
|
|
196
|
+
? // on top of a colored tile
|
|
197
|
+
(contrastScheme[letter] ?? 'black')
|
|
198
|
+
: !drawTiles || !color
|
|
199
|
+
? // plain background, uncolored letters
|
|
200
|
+
theme.palette.text.primary
|
|
201
|
+
: // letter-color mode: darken/lighten to stay readable
|
|
202
|
+
adjustColorForContrast(
|
|
203
|
+
color,
|
|
204
|
+
theme.palette.background.default,
|
|
205
|
+
)
|
|
210
206
|
ctx.fillText(
|
|
211
207
|
isMatchingReference ? '.' : letter,
|
|
212
208
|
x + halfColWidth,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
clickColor,
|
|
3
|
+
hoverColor,
|
|
4
|
+
multiRowHoverColor,
|
|
5
|
+
referenceColor,
|
|
6
|
+
} from '../overlayColors.ts'
|
|
2
7
|
|
|
3
8
|
import type { MsaViewModel } from '../../model.ts'
|
|
4
9
|
|
|
5
|
-
const hoverColor = 'rgba(0,0,0,0.15)'
|
|
6
|
-
const highlightColor = 'rgba(128,128,0,0.2)'
|
|
7
10
|
// the persistent highlightColumns overlay: a stronger fill plus a solid border
|
|
8
11
|
// so a domain/motif band reads clearly over the colored alignment cells (the
|
|
9
12
|
// faint hover-style wash alone is invisible against clustalx coloring)
|
|
@@ -20,8 +23,7 @@ export function renderMouseover({
|
|
|
20
23
|
const {
|
|
21
24
|
mouseCol,
|
|
22
25
|
colWidth,
|
|
23
|
-
|
|
24
|
-
verticalScrollbarWidth,
|
|
26
|
+
msaCanvasWidth: width,
|
|
25
27
|
height,
|
|
26
28
|
rowHeight,
|
|
27
29
|
scrollX,
|
|
@@ -34,53 +36,54 @@ export function renderMouseover({
|
|
|
34
36
|
hoveredRowIndices,
|
|
35
37
|
highlightedColumnRuns,
|
|
36
38
|
} = model
|
|
37
|
-
const width = msaAreaWidth - verticalScrollbarWidth
|
|
38
39
|
ctx.resetTransform()
|
|
39
40
|
ctx.clearRect(0, 0, width * highResScaleFactor, height * highResScaleFactor)
|
|
40
41
|
ctx.scale(highResScaleFactor, highResScaleFactor)
|
|
41
42
|
|
|
42
|
-
//
|
|
43
|
+
// rows and columns sit at a fixed pitch, so an index is all it takes to place
|
|
44
|
+
// a full-width or full-height band (matching fillRow in TreeCanvas)
|
|
45
|
+
const rowBand = (index: number) => {
|
|
46
|
+
ctx.fillRect(0, index * rowHeight + scrollY, width, rowHeight)
|
|
47
|
+
}
|
|
48
|
+
const colBand = (index: number) => {
|
|
49
|
+
ctx.fillRect(index * colWidth + scrollX, 0, colWidth, height)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// the reference row (relativeTo) stays lit, as does every tip under a hovered
|
|
53
|
+
// tree node
|
|
43
54
|
if (referenceRowIndex !== undefined) {
|
|
44
55
|
ctx.fillStyle = referenceColor
|
|
45
|
-
|
|
56
|
+
rowBand(referenceRowIndex)
|
|
46
57
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
ctx.fillStyle = multiRowHoverColor
|
|
51
|
-
for (const rowIndex of hoveredRowIndices) {
|
|
52
|
-
ctx.fillRect(0, rowIndex * rowHeight + scrollY, width, rowHeight)
|
|
53
|
-
}
|
|
58
|
+
ctx.fillStyle = multiRowHoverColor
|
|
59
|
+
for (const rowIndex of hoveredRowIndices) {
|
|
60
|
+
rowBand(rowIndex)
|
|
54
61
|
}
|
|
55
62
|
|
|
56
|
-
//
|
|
57
|
-
// band so a domain/motif highlight
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
ctx.strokeRect(x, 0, w, height)
|
|
67
|
-
}
|
|
63
|
+
// each contiguous run of highlighted columns draws as one filled, bordered
|
|
64
|
+
// band, so a domain/motif highlight stays visible over the alignment
|
|
65
|
+
ctx.lineWidth = 2
|
|
66
|
+
for (const { start, end } of highlightedColumnRuns) {
|
|
67
|
+
const x = start * colWidth + scrollX
|
|
68
|
+
const w = (end - start + 1) * colWidth
|
|
69
|
+
ctx.fillStyle = highlightColumnsFill
|
|
70
|
+
ctx.fillRect(x, 0, w, height)
|
|
71
|
+
ctx.strokeStyle = highlightColumnsBorder
|
|
72
|
+
ctx.strokeRect(x, 0, w, height)
|
|
68
73
|
}
|
|
69
74
|
|
|
75
|
+
ctx.fillStyle = hoverColor
|
|
70
76
|
if (mouseCol !== undefined) {
|
|
71
|
-
|
|
72
|
-
ctx.fillRect(mouseCol * colWidth + scrollX, 0, colWidth, height)
|
|
77
|
+
colBand(mouseCol)
|
|
73
78
|
}
|
|
74
79
|
if (mouseRow !== undefined) {
|
|
75
|
-
|
|
76
|
-
ctx.fillRect(0, mouseRow * rowHeight + scrollY, width, rowHeight)
|
|
80
|
+
rowBand(mouseRow)
|
|
77
81
|
}
|
|
82
|
+
ctx.fillStyle = clickColor
|
|
78
83
|
if (mouseClickCol !== undefined) {
|
|
79
|
-
|
|
80
|
-
ctx.fillRect(mouseClickCol * colWidth + scrollX, 0, colWidth, height)
|
|
84
|
+
colBand(mouseClickCol)
|
|
81
85
|
}
|
|
82
86
|
if (mouseClickRow !== undefined) {
|
|
83
|
-
|
|
84
|
-
ctx.fillRect(0, mouseClickRow * rowHeight + scrollY, width, rowHeight)
|
|
87
|
+
rowBand(mouseClickRow)
|
|
85
88
|
}
|
|
86
89
|
}
|
|
@@ -10,3 +10,9 @@ export const referenceColor = 'rgba(0,128,255,0.3)'
|
|
|
10
10
|
// while the alignment cells are already colored so a lighter wash reads better.
|
|
11
11
|
export const treeHoverColor = 'rgba(255,165,0,0.2)'
|
|
12
12
|
export const multiRowHoverColor = 'rgba(255,165,0,0.15)'
|
|
13
|
+
|
|
14
|
+
// the hovered column/row, and the one pinned by a click. Drawn on the alignment
|
|
15
|
+
// overlay canvas and again as plain divs over the track area, which have to
|
|
16
|
+
// match or the crosshair appears to change color as it crosses the tracks.
|
|
17
|
+
export const hoverColor = 'rgba(0,0,0,0.15)'
|
|
18
|
+
export const clickColor = 'rgba(128,128,0,0.2)'
|
|
@@ -22,8 +22,8 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
22
22
|
)
|
|
23
23
|
const { onMouseDown, onMouseUp } = useWheelScroll({ ref, onScrollY })
|
|
24
24
|
|
|
25
|
-
const mouseoverRef = useCanvasAutorun(
|
|
26
|
-
ctx => {
|
|
25
|
+
const mouseoverRef = useCanvasAutorun({
|
|
26
|
+
draw: ctx => {
|
|
27
27
|
if (isAlive(model)) {
|
|
28
28
|
const {
|
|
29
29
|
rowHeight,
|
|
@@ -61,8 +61,10 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
width: treeAreaWidth,
|
|
65
|
+
height,
|
|
66
|
+
deps: [model],
|
|
67
|
+
})
|
|
66
68
|
|
|
67
69
|
return (
|
|
68
70
|
<div
|
|
@@ -60,15 +60,12 @@ const TreeCanvasBlock = observer(function ({
|
|
|
60
60
|
const width = treeAreaWidth + padding
|
|
61
61
|
const height = blockSize
|
|
62
62
|
|
|
63
|
-
const
|
|
64
|
-
|
|
63
|
+
const canvasWidth = width * highResScaleFactor
|
|
64
|
+
const canvasHeight = height * highResScaleFactor
|
|
65
|
+
const ref = useCanvasAutorun({
|
|
66
|
+
draw: ctx => {
|
|
65
67
|
ctx.resetTransform()
|
|
66
|
-
ctx.clearRect(
|
|
67
|
-
0,
|
|
68
|
-
0,
|
|
69
|
-
width * highResScaleFactor,
|
|
70
|
-
height * highResScaleFactor,
|
|
71
|
-
)
|
|
68
|
+
ctx.clearRect(0, 0, canvasWidth, canvasHeight)
|
|
72
69
|
renderTreeCanvas({
|
|
73
70
|
ctx,
|
|
74
71
|
model,
|
|
@@ -77,8 +74,10 @@ const TreeCanvasBlock = observer(function ({
|
|
|
77
74
|
theme,
|
|
78
75
|
})
|
|
79
76
|
},
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
width: canvasWidth,
|
|
78
|
+
height: canvasHeight,
|
|
79
|
+
deps: [model, clickMap, offsetY, theme],
|
|
80
|
+
})
|
|
82
81
|
|
|
83
82
|
const style = {
|
|
84
83
|
width,
|
|
@@ -112,8 +111,8 @@ const TreeCanvasBlock = observer(function ({
|
|
|
112
111
|
|
|
113
112
|
<canvas
|
|
114
113
|
ref={ref}
|
|
115
|
-
width={
|
|
116
|
-
height={
|
|
114
|
+
width={canvasWidth}
|
|
115
|
+
height={canvasHeight}
|
|
117
116
|
style={{ ...style, cursor: hovered ? 'pointer' : 'default' }}
|
|
118
117
|
onMouseMove={event => {
|
|
119
118
|
onMouseMove(event)
|
|
@@ -4,9 +4,14 @@ import { observer } from 'mobx-react'
|
|
|
4
4
|
|
|
5
5
|
import type { MsaViewModel } from '../../model.ts'
|
|
6
6
|
|
|
7
|
+
// The gutter above the tree panel. It spans the resize handle too, so whatever
|
|
8
|
+
// follows it in the top row starts exactly where the alignment canvas does --
|
|
9
|
+
// MainArea has TreePanel + VerticalResizeHandle before the alignment.
|
|
7
10
|
const TreeRuler = observer(({ model }: { model: MsaViewModel }) => {
|
|
8
|
-
const { treeAreaWidth } = model
|
|
9
|
-
return
|
|
11
|
+
const { treeAreaWidth, resizeHandleWidth } = model
|
|
12
|
+
return (
|
|
13
|
+
<div style={{ flexShrink: 0, width: treeAreaWidth + resizeHandleWidth }} />
|
|
14
|
+
)
|
|
10
15
|
})
|
|
11
16
|
|
|
12
17
|
export default TreeRuler
|
|
@@ -117,8 +117,9 @@ describe('getNodeX cladogram positioning', () => {
|
|
|
117
117
|
})
|
|
118
118
|
})
|
|
119
119
|
|
|
120
|
-
// the x values the tree pass actually strokes, for a model built from newick
|
|
121
|
-
|
|
120
|
+
// the x values the tree pass actually strokes, for a model built from newick,
|
|
121
|
+
// alongside the tree width they are expected to span
|
|
122
|
+
function drawnTree(newick: string) {
|
|
122
123
|
const model = stateModelFactory().create({
|
|
123
124
|
type: 'MsaView',
|
|
124
125
|
data: { msa: '>a\nA\n>b\nA\n>c\nA\n>d\nA', tree: newick },
|
|
@@ -150,19 +151,26 @@ function drawnTreeXs(newick: string) {
|
|
|
150
151
|
offsetY: 0,
|
|
151
152
|
theme: { palette: { text: { primary: '#000' } } } as Theme,
|
|
152
153
|
})
|
|
153
|
-
return xs
|
|
154
|
+
return { xs, model }
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
describe('renderTreeCanvas horizontal extent', () => {
|
|
157
158
|
it('spreads a cladogram across the tree area when branch lengths are absent', () => {
|
|
158
159
|
// a lengthless newick forces cladogram mode; scaling it by the (zero) max
|
|
159
160
|
// branch length would stack every node on x=0 as one vertical line
|
|
160
|
-
const xs =
|
|
161
|
-
expect(Math.max(...xs)).toBe(
|
|
161
|
+
const { xs, model } = drawnTree('((a,b),(c,d));')
|
|
162
|
+
expect(Math.max(...xs)).toBe(model.treeWidth)
|
|
162
163
|
})
|
|
163
164
|
|
|
164
165
|
it('scales a phylogram by branch length', () => {
|
|
165
|
-
const xs =
|
|
166
|
-
expect(Math.max(...xs)).toBeCloseTo(
|
|
166
|
+
const { xs, model } = drawnTree('((a:0.1,b:0.2):0.3,(c:0.4,d:0.5):0.6);')
|
|
167
|
+
expect(Math.max(...xs)).toBeCloseTo(model.treeWidth)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// labels off means no label gutter to reserve, so the tips reach the far edge
|
|
171
|
+
// of the tree area rather than stopping at the default treeWidth
|
|
172
|
+
it('hands the label gutter to the tree when labels are off', () => {
|
|
173
|
+
const { model } = drawnTree('((a,b),(c,d));')
|
|
174
|
+
expect(model.treeWidth).toBe(model.treeAreaWidth - 10 - model.marginLeft)
|
|
167
175
|
})
|
|
168
176
|
})
|