react-msaview 5.9.0 → 5.10.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 +76 -76
- package/bundle/index.js.map +3 -3
- package/dist/components/ConservationTrack.js +21 -16
- package/dist/components/ConservationTrack.js.map +1 -1
- package/dist/components/MSAView.js +17 -23
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/TextTrack.js +26 -21
- package/dist/components/TextTrack.js.map +1 -1
- package/dist/components/header/settingsMenuItems.js +39 -72
- package/dist/components/header/settingsMenuItems.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 +13 -6
- package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js +9 -6
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +31 -31
- 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/renderTreeCanvas.d.ts +0 -38
- package/dist/components/tree/renderTreeCanvas.js +17 -17
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/model.js +82 -113
- 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 +3 -0
- package/dist/renderToSvg.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/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/dist/version.js.map +1 -1
- package/package.json +3 -3
- package/src/components/ConservationTrack.tsx +7 -5
- package/src/components/MSAView.tsx +24 -30
- package/src/components/TextTrack.tsx +7 -12
- package/src/components/header/settingsMenuItems.ts +40 -72
- package/src/components/msa/MSACanvasBlock.tsx +16 -21
- package/src/components/msa/MSAMouseoverCanvas.tsx +10 -6
- package/src/components/msa/renderBoxFeatureCanvasBlock.test.ts +51 -0
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +13 -8
- package/src/components/msa/renderMSAMouseover.ts +37 -32
- 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/renderTreeCanvas.ts +20 -17
- package/src/model.ts +97 -130
- 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/renderToSvg.tsx +3 -0
- package/src/seqPosToGlobalCol.test.ts +39 -51
- package/src/seqPosToGlobalCol.ts +6 -41
- package/src/useCanvasAutorun.test.tsx +98 -0
- package/src/useCanvasAutorun.ts +30 -11
- package/src/version.ts +1 -1
|
@@ -1,24 +1,14 @@
|
|
|
1
|
-
import { isBlank } from './util.js';
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The global column of every ungapped sequence position in a row, i.e.
|
|
3
|
+
* index[seqPos] is the column holding the seqPos-th non-gap character.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* A row is indexed once and then answers seqPos -> column by lookup, which is
|
|
6
|
+
* what makes the domain overlay affordable: it resolves one position per feature
|
|
7
|
+
* per row on every redraw, and scanning the row for each would be quadratic.
|
|
9
8
|
*
|
|
10
9
|
* @example
|
|
11
10
|
* // Row: "A-TG-C" (A at 0, T at 2, G at 3, C at 5)
|
|
12
|
-
*
|
|
13
|
-
* seqPosToGlobalCol({ row: "A-TG-C", seqPos: 1 }) // → 2 (T)
|
|
14
|
-
* seqPosToGlobalCol({ row: "A-TG-C", seqPos: 2 }) // → 3 (G)
|
|
15
|
-
* seqPosToGlobalCol({ row: "A-TG-C", seqPos: 3 }) // → 5 (C)
|
|
16
|
-
*/
|
|
17
|
-
/**
|
|
18
|
-
* The global column of every ungapped sequence position in a row, i.e.
|
|
19
|
-
* index[seqPos] is the column holding the seqPos-th non-gap character. Amortizes
|
|
20
|
-
* repeated seqPosToGlobalCol lookups (the domain overlay does one per feature
|
|
21
|
-
* per row) into a single pass per row.
|
|
11
|
+
* buildSeqPosIndex('A-TG-C') // → [0, 2, 3, 5]
|
|
22
12
|
*/
|
|
23
13
|
export function buildSeqPosIndex(row) {
|
|
24
14
|
const index = new Int32Array(row.length);
|
|
@@ -31,21 +21,4 @@ export function buildSeqPosIndex(row) {
|
|
|
31
21
|
}
|
|
32
22
|
return index.subarray(0, n);
|
|
33
23
|
}
|
|
34
|
-
export function seqPosToGlobalCol({ row, seqPos, }) {
|
|
35
|
-
let nonGapCount = 0;
|
|
36
|
-
let globalCol = 0;
|
|
37
|
-
// Find the seqPos-th non-gap character
|
|
38
|
-
while (globalCol < row.length) {
|
|
39
|
-
if (!isBlank(row[globalCol])) {
|
|
40
|
-
if (nonGapCount === seqPos) {
|
|
41
|
-
return globalCol;
|
|
42
|
-
}
|
|
43
|
-
nonGapCount++;
|
|
44
|
-
}
|
|
45
|
-
globalCol++;
|
|
46
|
-
}
|
|
47
|
-
// If seqPos is 0 and we didn't find any non-gap character, return 0
|
|
48
|
-
// Otherwise return globalCol (which is row.length at this point)
|
|
49
|
-
return seqPos === 0 ? 0 : globalCol;
|
|
50
|
-
}
|
|
51
24
|
//# sourceMappingURL=seqPosToGlobalCol.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seqPosToGlobalCol.js","sourceRoot":"","sources":["../src/seqPosToGlobalCol.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"seqPosToGlobalCol.js","sourceRoot":"","sources":["../src/seqPosToGlobalCol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IACxC,IAAI,CAAC,GAAG,CAAC,CAAA;IACT,KAAK,IAAI,GAAG,GAAG,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC;QAC1C,oEAAoE;QACpE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YAC7C,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,CAAA;QAClB,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;AAC7B,CAAC"}
|
|
@@ -1,2 +1,24 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Wires a canvas ref to a mobx autorun: grabs the 2d context once the canvas
|
|
4
|
+
* mounts and re-runs `draw` whenever any observable it reads changes. Collapses
|
|
5
|
+
* the getContext/null-guard/autorun-dispose boilerplate repeated by every canvas
|
|
6
|
+
* block host.
|
|
7
|
+
*
|
|
8
|
+
* `width`/`height` are the canvas *backing store* size the caller renders onto
|
|
9
|
+
* the element (logical size times the high-res scale factor), and they are load
|
|
10
|
+
* bearing rather than informational. Assigning either resets the canvas bitmap,
|
|
11
|
+
* and React assigns them on every commit where they changed -- which lands
|
|
12
|
+
* *after* the mobx reaction that already redrew at the old size. Without a redraw
|
|
13
|
+
* on the far side of that commit the canvas is left blank: it is what blanked the
|
|
14
|
+
* tree on a tree-area resize and the consensus tracks on a vertical zoom.
|
|
15
|
+
*
|
|
16
|
+
* `deps` covers everything else the effect should re-subscribe on, matching a
|
|
17
|
+
* normal useEffect dep array.
|
|
18
|
+
*/
|
|
19
|
+
export declare function useCanvasAutorun({ draw, width, height, deps, }: {
|
|
20
|
+
draw: (ctx: CanvasRenderingContext2D) => void;
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
deps: React.DependencyList;
|
|
24
|
+
}): React.RefObject<HTMLCanvasElement | null>;
|
package/dist/useCanvasAutorun.js
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react';
|
|
2
2
|
import { autorun } from 'mobx';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Wires a canvas ref to a mobx autorun: grabs the 2d context once the canvas
|
|
5
|
+
* mounts and re-runs `draw` whenever any observable it reads changes. Collapses
|
|
6
|
+
* the getContext/null-guard/autorun-dispose boilerplate repeated by every canvas
|
|
7
|
+
* block host.
|
|
8
|
+
*
|
|
9
|
+
* `width`/`height` are the canvas *backing store* size the caller renders onto
|
|
10
|
+
* the element (logical size times the high-res scale factor), and they are load
|
|
11
|
+
* bearing rather than informational. Assigning either resets the canvas bitmap,
|
|
12
|
+
* and React assigns them on every commit where they changed -- which lands
|
|
13
|
+
* *after* the mobx reaction that already redrew at the old size. Without a redraw
|
|
14
|
+
* on the far side of that commit the canvas is left blank: it is what blanked the
|
|
15
|
+
* tree on a tree-area resize and the consensus tracks on a vertical zoom.
|
|
16
|
+
*
|
|
17
|
+
* `deps` covers everything else the effect should re-subscribe on, matching a
|
|
18
|
+
* normal useEffect dep array.
|
|
19
|
+
*/
|
|
20
|
+
export function useCanvasAutorun({ draw, width, height, deps, }) {
|
|
9
21
|
const ref = useRef(null);
|
|
10
22
|
// the autorun tracks its own observable reads, so `draw` is intentionally not
|
|
11
|
-
// a dependency; `deps`
|
|
23
|
+
// a dependency; `deps` plus the canvas size control when it re-subscribes
|
|
12
24
|
useEffect(() => {
|
|
13
25
|
const ctx = ref.current?.getContext('2d');
|
|
14
26
|
return ctx
|
|
@@ -17,7 +29,7 @@ export function useCanvasAutorun(draw, deps) {
|
|
|
17
29
|
})
|
|
18
30
|
: undefined;
|
|
19
31
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
20
|
-
}, deps);
|
|
32
|
+
}, [...deps, width, height]);
|
|
21
33
|
return ref;
|
|
22
34
|
}
|
|
23
35
|
//# sourceMappingURL=useCanvasAutorun.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useCanvasAutorun.js","sourceRoot":"","sources":["../src/useCanvasAutorun.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAEzC,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAI9B
|
|
1
|
+
{"version":3,"file":"useCanvasAutorun.js","sourceRoot":"","sources":["../src/useCanvasAutorun.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAEzC,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAI9B;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAC/B,IAAI,EACJ,KAAK,EACL,MAAM,EACN,IAAI,GAML;IACC,MAAM,GAAG,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAA;IAC3C,8EAA8E;IAC9E,0EAA0E;IAC1E,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,EAAE,UAAU,CAAC,IAAI,CAAC,CAAA;QACzC,OAAO,GAAG;YACR,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE;gBACX,IAAI,CAAC,GAAG,CAAC,CAAA;YACX,CAAC,CAAC;YACJ,CAAC,CAAC,SAAS,CAAA;QACb,uDAAuD;IACzD,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAA;IAC5B,OAAO,GAAG,CAAA;AACZ,CAAC"}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "5.
|
|
1
|
+
export declare const version = "5.10.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const version = '5.
|
|
1
|
+
export const version = '5.10.0';
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
package/dist/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,OAAO,GAAG,
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,OAAO,GAAG,QAAQ,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-msaview",
|
|
3
3
|
"author": "Colin",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.10.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"@gmod/newick": "^1.0.0",
|
|
44
44
|
"colord": "^2.9.3",
|
|
45
45
|
"flatbush": "^4.6.2",
|
|
46
|
-
"@jbrowse/svgcanvas": "5.
|
|
47
|
-
"msa-parsers": "5.
|
|
46
|
+
"@jbrowse/svgcanvas": "5.10.0",
|
|
47
|
+
"msa-parsers": "5.10.0"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"clean": "rimraf dist bundle",
|
|
@@ -25,9 +25,9 @@ const ConservationBlock = observer(function ({
|
|
|
25
25
|
}) {
|
|
26
26
|
const { blockSize, scrollX, highResScaleFactor } = model
|
|
27
27
|
|
|
28
|
-
const ref = useCanvasAutorun(
|
|
29
|
-
ctx => {
|
|
30
|
-
const {
|
|
28
|
+
const ref = useCanvasAutorun({
|
|
29
|
+
draw: ctx => {
|
|
30
|
+
const { colWidth } = model
|
|
31
31
|
ctx.resetTransform()
|
|
32
32
|
ctx.scale(highResScaleFactor, highResScaleFactor)
|
|
33
33
|
ctx.clearRect(0, 0, blockSize, trackHeight)
|
|
@@ -43,8 +43,10 @@ const ConservationBlock = observer(function ({
|
|
|
43
43
|
blockSize,
|
|
44
44
|
})
|
|
45
45
|
},
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
width: blockSize * highResScaleFactor,
|
|
47
|
+
height: trackHeight * highResScaleFactor,
|
|
48
|
+
deps: [model, track, offsetX, trackHeight],
|
|
49
|
+
})
|
|
48
50
|
|
|
49
51
|
return (
|
|
50
52
|
<canvas
|
|
@@ -11,6 +11,7 @@ import VerticalScrollbar from './VerticalScrollbar.tsx'
|
|
|
11
11
|
import Header from './header/Header.tsx'
|
|
12
12
|
import Minimap from './minimap/Minimap.tsx'
|
|
13
13
|
import MSAPanel from './msa/MSAPanel.tsx'
|
|
14
|
+
import { clickColor, hoverColor } from './overlayColors.ts'
|
|
14
15
|
import TreePanel from './tree/TreePanel.tsx'
|
|
15
16
|
import TreeRuler from './tree/TreeRuler.tsx'
|
|
16
17
|
|
|
@@ -43,47 +44,40 @@ const TrackColumnIndicator = observer(function ({
|
|
|
43
44
|
verticalScrollbarWidth,
|
|
44
45
|
} = model
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
const
|
|
47
|
+
// hovered column, then the pinned one, matching the alignment's own overlay
|
|
48
|
+
const bands = [
|
|
49
|
+
{ col: mouseCol, color: hoverColor },
|
|
50
|
+
{ col: mouseClickCol, color: clickColor },
|
|
51
|
+
]
|
|
48
52
|
|
|
49
53
|
return (
|
|
50
54
|
<div
|
|
51
55
|
style={{
|
|
52
56
|
position: 'absolute',
|
|
53
|
-
left,
|
|
57
|
+
left: treeAreaWidth + resizeHandleWidth,
|
|
54
58
|
top: 0,
|
|
55
|
-
width:
|
|
59
|
+
width: msaAreaWidth - verticalScrollbarWidth,
|
|
56
60
|
height: totalTrackAreaHeight,
|
|
57
61
|
overflow: 'hidden',
|
|
58
62
|
pointerEvents: 'none',
|
|
59
63
|
}}
|
|
60
64
|
>
|
|
61
|
-
{
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
position: 'absolute',
|
|
78
|
-
left: mouseClickCol * colWidth + scrollX,
|
|
79
|
-
top: 0,
|
|
80
|
-
width: colWidth,
|
|
81
|
-
height: totalTrackAreaHeight,
|
|
82
|
-
backgroundColor: 'rgba(128,128,0,0.2)',
|
|
83
|
-
zIndex: 100,
|
|
84
|
-
}}
|
|
85
|
-
/>
|
|
86
|
-
) : null}
|
|
65
|
+
{bands.map(({ col, color }) =>
|
|
66
|
+
col === undefined ? null : (
|
|
67
|
+
<div
|
|
68
|
+
key={color}
|
|
69
|
+
style={{
|
|
70
|
+
position: 'absolute',
|
|
71
|
+
left: col * colWidth + scrollX,
|
|
72
|
+
top: 0,
|
|
73
|
+
width: colWidth,
|
|
74
|
+
height: totalTrackAreaHeight,
|
|
75
|
+
backgroundColor: color,
|
|
76
|
+
zIndex: 100,
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
),
|
|
80
|
+
)}
|
|
87
81
|
</div>
|
|
88
82
|
)
|
|
89
83
|
})
|
|
@@ -32,16 +32,9 @@ const AnnotationBlock = observer(function ({
|
|
|
32
32
|
|
|
33
33
|
const colorScheme = customColorScheme ?? modelColorScheme
|
|
34
34
|
const { theme, contrastScheme } = useColorContrast(colorScheme)
|
|
35
|
-
const ref = useCanvasAutorun(
|
|
36
|
-
ctx => {
|
|
37
|
-
const {
|
|
38
|
-
blockSize,
|
|
39
|
-
bgColor,
|
|
40
|
-
colWidth,
|
|
41
|
-
fontSize,
|
|
42
|
-
rowHeight,
|
|
43
|
-
highResScaleFactor,
|
|
44
|
-
} = model
|
|
35
|
+
const ref = useCanvasAutorun({
|
|
36
|
+
draw: ctx => {
|
|
37
|
+
const { bgColor, colWidth, fontSize } = model
|
|
45
38
|
ctx.resetTransform()
|
|
46
39
|
ctx.scale(highResScaleFactor, highResScaleFactor)
|
|
47
40
|
ctx.clearRect(0, 0, blockSize, rowHeight)
|
|
@@ -62,8 +55,10 @@ const AnnotationBlock = observer(function ({
|
|
|
62
55
|
blockSize,
|
|
63
56
|
})
|
|
64
57
|
},
|
|
65
|
-
|
|
66
|
-
|
|
58
|
+
width: blockSize * highResScaleFactor,
|
|
59
|
+
height: rowHeight * highResScaleFactor,
|
|
60
|
+
deps: [model, offsetX, theme, contrastScheme, colorScheme, data],
|
|
61
|
+
})
|
|
67
62
|
return (
|
|
68
63
|
<canvas
|
|
69
64
|
ref={ref}
|
|
@@ -1,41 +1,34 @@
|
|
|
1
1
|
import type { MsaViewModel } from '../../model.ts'
|
|
2
2
|
import type { MenuItem } from '@jbrowse/core/ui/Menu'
|
|
3
3
|
|
|
4
|
+
function toggle(label: string, checked: boolean, set: (arg: boolean) => void) {
|
|
5
|
+
return {
|
|
6
|
+
label,
|
|
7
|
+
type: 'checkbox' as const,
|
|
8
|
+
checked,
|
|
9
|
+
onClick: () => {
|
|
10
|
+
set(!checked)
|
|
11
|
+
},
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
4
15
|
export function msaSettingsMenuItems(model: MsaViewModel): MenuItem[] {
|
|
5
16
|
const { drawMsaLetters, hideGaps, bgColor, showColumnStats } = model
|
|
6
17
|
return [
|
|
7
|
-
{
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
{
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
label: 'Color letters instead of background of tiles',
|
|
25
|
-
type: 'checkbox',
|
|
26
|
-
checked: !bgColor,
|
|
27
|
-
onClick: () => {
|
|
28
|
-
model.setBgColor(!bgColor)
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
label: 'Enable hiding gappy columns?',
|
|
33
|
-
type: 'checkbox',
|
|
34
|
-
checked: hideGaps,
|
|
35
|
-
onClick: () => {
|
|
36
|
-
model.setHideGaps(!hideGaps)
|
|
37
|
-
},
|
|
38
|
-
},
|
|
18
|
+
toggle('Show column statistics on hover', showColumnStats, arg => {
|
|
19
|
+
model.setShowColumnStats(arg)
|
|
20
|
+
}),
|
|
21
|
+
toggle('Draw letters', drawMsaLetters, arg => {
|
|
22
|
+
model.setDrawMsaLetters(arg)
|
|
23
|
+
}),
|
|
24
|
+
// the checkbox reads as the inverse of the property it sets: bgColor draws
|
|
25
|
+
// the tile, and turning it off is what leaves the letter itself colored
|
|
26
|
+
toggle('Color letters instead of background of tiles', !bgColor, arg => {
|
|
27
|
+
model.setBgColor(!arg)
|
|
28
|
+
}),
|
|
29
|
+
toggle('Enable hiding gappy columns?', hideGaps, arg => {
|
|
30
|
+
model.setHideGaps(arg)
|
|
31
|
+
}),
|
|
39
32
|
]
|
|
40
33
|
}
|
|
41
34
|
|
|
@@ -48,45 +41,20 @@ export function treeSettingsMenuItems(model: MsaViewModel): MenuItem[] {
|
|
|
48
41
|
drawLabels,
|
|
49
42
|
} = model
|
|
50
43
|
return [
|
|
51
|
-
{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
label: 'Draw clickable bubbles on tree branches',
|
|
69
|
-
type: 'checkbox',
|
|
70
|
-
checked: drawNodeBubbles,
|
|
71
|
-
onClick: () => {
|
|
72
|
-
model.setDrawNodeBubbles(!drawNodeBubbles)
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
label: 'Tree labels align right',
|
|
77
|
-
type: 'checkbox',
|
|
78
|
-
checked: labelsAlignRight,
|
|
79
|
-
onClick: () => {
|
|
80
|
-
model.setLabelsAlignRight(!labelsAlignRight)
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
label: 'Draw labels',
|
|
85
|
-
type: 'checkbox',
|
|
86
|
-
checked: drawLabels,
|
|
87
|
-
onClick: () => {
|
|
88
|
-
model.setDrawLabels(!drawLabels)
|
|
89
|
-
},
|
|
90
|
-
},
|
|
44
|
+
toggle('Show branch length', showBranchLen, arg => {
|
|
45
|
+
model.setShowBranchLen(arg)
|
|
46
|
+
}),
|
|
47
|
+
toggle('Show tree', drawTree, arg => {
|
|
48
|
+
model.setDrawTree(arg)
|
|
49
|
+
}),
|
|
50
|
+
toggle('Draw clickable bubbles on tree branches', drawNodeBubbles, arg => {
|
|
51
|
+
model.setDrawNodeBubbles(arg)
|
|
52
|
+
}),
|
|
53
|
+
toggle('Tree labels align right', labelsAlignRight, arg => {
|
|
54
|
+
model.setLabelsAlignRight(arg)
|
|
55
|
+
}),
|
|
56
|
+
toggle('Draw labels', drawLabels, arg => {
|
|
57
|
+
model.setDrawLabels(arg)
|
|
58
|
+
}),
|
|
91
59
|
]
|
|
92
60
|
}
|
|
@@ -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,
|
|
@@ -16,21 +16,25 @@ const MSAMouseoverCanvas = observer(function ({
|
|
|
16
16
|
const { height, msaAreaWidth, verticalScrollbarWidth, highResScaleFactor } =
|
|
17
17
|
model
|
|
18
18
|
const width = msaAreaWidth - verticalScrollbarWidth
|
|
19
|
-
const
|
|
20
|
-
|
|
19
|
+
const canvasWidth = width * highResScaleFactor
|
|
20
|
+
const canvasHeight = height * highResScaleFactor
|
|
21
|
+
const ref = useCanvasAutorun({
|
|
22
|
+
draw: ctx => {
|
|
21
23
|
if (isAlive(model)) {
|
|
22
24
|
renderMouseover({ ctx, model })
|
|
23
25
|
}
|
|
24
26
|
},
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
width: canvasWidth,
|
|
28
|
+
height: canvasHeight,
|
|
29
|
+
deps: [model],
|
|
30
|
+
})
|
|
27
31
|
|
|
28
32
|
return (
|
|
29
33
|
<canvas
|
|
30
34
|
ref={ref}
|
|
31
35
|
id="mouseover"
|
|
32
|
-
width={
|
|
33
|
-
height={
|
|
36
|
+
width={canvasWidth}
|
|
37
|
+
height={canvasHeight}
|
|
34
38
|
style={{
|
|
35
39
|
position: 'absolute',
|
|
36
40
|
top: 0,
|
|
@@ -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'
|