react-msaview 8.4.0 → 9.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 +503 -89
- package/bundle/index.js.map +4 -4
- package/dist/components/MSAView.js +2 -2
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/MSAViewer.d.ts +1 -3
- package/dist/components/MSAViewer.js +1 -2
- package/dist/components/MSAViewer.js.map +1 -1
- package/dist/components/ResizeHandles.js +1 -1
- package/dist/components/ResizeHandles.js.map +1 -1
- package/dist/components/Track.js +1 -1
- package/dist/components/Track.js.map +1 -1
- package/dist/components/dialogs/ExportSVGDialog.js +2 -2
- package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
- package/dist/components/header/GoToBox.d.ts +332 -0
- package/dist/components/header/GoToBox.js +14 -4
- package/dist/components/header/GoToBox.js.map +1 -1
- package/dist/components/header/settingsMenuItems.js +4 -1
- package/dist/components/header/settingsMenuItems.js.map +1 -1
- package/dist/components/tree/CladeLabels.js +3 -3
- package/dist/components/tree/CladeLabels.js.map +1 -1
- package/dist/components/tree/TreeCanvas.js +4 -4
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +4 -4
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/TreeOverview.js +10 -5
- package/dist/components/tree/TreeOverview.js.map +1 -1
- package/dist/components/tree/TreePanel.js +6 -2
- package/dist/components/tree/TreePanel.js.map +1 -1
- package/dist/components/tree/TreeRuler.js +2 -2
- package/dist/components/tree/TreeRuler.js.map +1 -1
- package/dist/components/tree/cladeBrackets.d.ts +8 -10
- package/dist/components/tree/cladeBrackets.js +7 -6
- package/dist/components/tree/cladeBrackets.js.map +1 -1
- package/dist/components/tree/clickMap.d.ts +2 -0
- package/dist/components/tree/clickMap.js.map +1 -1
- package/dist/components/tree/ellipsize.d.ts +8 -0
- package/dist/components/tree/ellipsize.js +20 -0
- package/dist/components/tree/ellipsize.js.map +1 -0
- package/dist/components/tree/renderTreeCanvas.js +30 -26
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/components/tree/renderTreeMouseover.js +1 -1
- package/dist/components/tree/renderTreeMouseover.js.map +1 -1
- package/dist/constants.d.ts +6 -3
- package/dist/constants.js +16 -6
- package/dist/constants.js.map +1 -1
- package/dist/measureTextCanvas.js +2 -2
- package/dist/measureTextCanvas.js.map +1 -1
- package/dist/model/treeModel.d.ts +14 -20
- package/dist/model/treeModel.js +16 -23
- package/dist/model/treeModel.js.map +1 -1
- package/dist/model.d.ts +192 -213
- package/dist/model.js +112 -104
- package/dist/model.js.map +1 -1
- package/dist/renderToSvg.js +24 -22
- package/dist/renderToSvg.js.map +1 -1
- package/dist/svgTestUtil.d.ts +252 -356
- package/dist/tipLabels.d.ts +8 -0
- package/dist/tipLabels.js +46 -0
- package/dist/tipLabels.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -6
- package/src/components/MSAView.tsx +2 -2
- package/src/components/MSAViewer.tsx +1 -5
- package/src/components/ResizeHandles.tsx +1 -1
- package/src/components/Track.tsx +1 -1
- package/src/components/dialogs/ExportSVGDialog.tsx +2 -2
- package/src/components/header/GoToBox.tsx +18 -4
- package/src/components/header/settingsMenuItems.ts +8 -0
- package/src/components/tree/CladeLabels.tsx +3 -3
- package/src/components/tree/TreeCanvas.tsx +10 -4
- package/src/components/tree/TreeCanvasBlock.tsx +4 -4
- package/src/components/tree/TreeOverview.tsx +10 -5
- package/src/components/tree/TreePanel.tsx +8 -2
- package/src/components/tree/TreeRuler.tsx +7 -3
- package/src/components/tree/cladeBrackets.ts +10 -17
- package/src/components/tree/clickMap.ts +2 -0
- package/src/components/tree/ellipsize.ts +24 -0
- package/src/components/tree/renderTreeCanvas.ts +42 -30
- package/src/components/tree/renderTreeMouseover.ts +1 -1
- package/src/constants.ts +17 -6
- package/src/measureTextCanvas.ts +2 -2
- package/src/model/treeModel.ts +17 -27
- package/src/model.ts +133 -128
- package/src/renderToSvg.tsx +24 -22
- package/src/tipLabels.ts +51 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NodeWithIds } from './types.ts';
|
|
2
|
+
/** the leaf names of a tree, and its depth in levels from the root */
|
|
3
|
+
export declare function treeTips(tree: NodeWithIds): {
|
|
4
|
+
names: string[];
|
|
5
|
+
depth: number;
|
|
6
|
+
};
|
|
7
|
+
/** the width of the widest label at `fontSize`, measuring the longest few */
|
|
8
|
+
export declare function widestLabel(labels: string[], fontSize: number): number;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { measureTextCanvas } from './measureTextCanvas.js';
|
|
2
|
+
// A label's width follows its length closely enough that the widest one is
|
|
3
|
+
// among the longest few, so a 230k-tip tree measures this many strings.
|
|
4
|
+
const measuredLabels = 100;
|
|
5
|
+
/** the leaf names of a tree, and its depth in levels from the root */
|
|
6
|
+
export function treeTips(tree) {
|
|
7
|
+
const names = [];
|
|
8
|
+
let depth = 0;
|
|
9
|
+
const stack = [[tree, 0]];
|
|
10
|
+
for (let entry = stack.pop(); entry; entry = stack.pop()) {
|
|
11
|
+
const [node, level] = entry;
|
|
12
|
+
const { children } = node;
|
|
13
|
+
if (children.length > 0) {
|
|
14
|
+
for (const child of children) {
|
|
15
|
+
stack.push([child, level + 1]);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
names.push(node.name);
|
|
20
|
+
depth = Math.max(depth, level);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return { names, depth };
|
|
24
|
+
}
|
|
25
|
+
/** the width of the widest label at `fontSize`, measuring the longest few */
|
|
26
|
+
export function widestLabel(labels, fontSize) {
|
|
27
|
+
const counts = [];
|
|
28
|
+
for (const label of labels) {
|
|
29
|
+
counts[label.length] = (counts[label.length] ?? 0) + 1;
|
|
30
|
+
}
|
|
31
|
+
let shortest = counts.length;
|
|
32
|
+
for (let seen = 0; shortest > 0 && seen < measuredLabels;) {
|
|
33
|
+
shortest--;
|
|
34
|
+
seen += counts[shortest] ?? 0;
|
|
35
|
+
}
|
|
36
|
+
let widest = 0;
|
|
37
|
+
let ties = 0;
|
|
38
|
+
for (const label of labels) {
|
|
39
|
+
if (label.length > shortest ||
|
|
40
|
+
(label.length === shortest && ties++ < measuredLabels)) {
|
|
41
|
+
widest = Math.max(widest, measureTextCanvas(label, fontSize));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return widest;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=tipLabels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tipLabels.js","sourceRoot":"","sources":["../src/tipLabels.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAI1D,2EAA2E;AAC3E,wEAAwE;AACxE,MAAM,cAAc,GAAG,GAAG,CAAA;AAE1B,sEAAsE;AACtE,MAAM,UAAU,QAAQ,CAAC,IAAiB;IACxC,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,MAAM,KAAK,GAA4B,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;IAClD,KAAK,IAAI,KAAK,GAAG,KAAK,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,KAAK,GAAG,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC;QACzD,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,KAAK,CAAA;QAC3B,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAA;QACzB,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;gBAC7B,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAA;YAChC,CAAC;QACH,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACrB,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;QAChC,CAAC;IACH,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAA;AACzB,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,WAAW,CAAC,MAAgB,EAAE,QAAgB;IAC5D,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAA;IACxD,CAAC;IACD,IAAI,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAA;IAC5B,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,QAAQ,GAAG,CAAC,IAAI,IAAI,GAAG,cAAc,GAAG,CAAC;QAC1D,QAAQ,EAAE,CAAA;QACV,IAAI,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;IAC/B,CAAC;IACD,IAAI,MAAM,GAAG,CAAC,CAAA;IACd,IAAI,IAAI,GAAG,CAAC,CAAA;IACZ,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IACE,KAAK,CAAC,MAAM,GAAG,QAAQ;YACvB,CAAC,KAAK,CAAC,MAAM,KAAK,QAAQ,IAAI,IAAI,EAAE,GAAG,cAAc,CAAC,EACtD,CAAC;YACD,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAA;QAC/D,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC"}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "
|
|
1
|
+
export declare const version = "9.0.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const version = '
|
|
1
|
+
export const version = '9.0.0';
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-msaview",
|
|
3
3
|
"author": "Colin",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "9.0.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
@@ -37,11 +37,11 @@
|
|
|
37
37
|
"react-dom": ">=16.8.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@jbrowse/core": "
|
|
41
|
-
"@jbrowse/mobx-state-tree": "^6.
|
|
40
|
+
"@jbrowse/core": "^5.0.0-beta.9",
|
|
41
|
+
"@jbrowse/mobx-state-tree": "^6.6.1",
|
|
42
42
|
"@mui/icons-material": "^9.4.0",
|
|
43
43
|
"@mui/material": "^9.4.0",
|
|
44
|
-
"@types/node": "^26.
|
|
44
|
+
"@types/node": "^26.6.2",
|
|
45
45
|
"esbuild": "^0.28.2",
|
|
46
46
|
"mobx": "^7.0.3",
|
|
47
47
|
"mobx-react": "^10.0.2",
|
|
@@ -50,10 +50,10 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@gmod/newick": "^1.0.3",
|
|
53
|
-
"@jbrowse/svgcanvas": "
|
|
53
|
+
"@jbrowse/svgcanvas": "9.0.0",
|
|
54
54
|
"colord": "^2.10.0",
|
|
55
55
|
"flatbush": "^4.6.2",
|
|
56
|
-
"msa-parsers": "
|
|
56
|
+
"msa-parsers": "9.0.0"
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
59
|
"clean": "rimraf dist bundle",
|
|
@@ -48,7 +48,7 @@ const TrackColumnIndicator = observer(function ({
|
|
|
48
48
|
mouseClickCol,
|
|
49
49
|
colWidth,
|
|
50
50
|
scrollX,
|
|
51
|
-
|
|
51
|
+
effectiveTreeAreaWidth,
|
|
52
52
|
rowPanelsWidth,
|
|
53
53
|
resizeHandleWidth,
|
|
54
54
|
totalTrackAreaHeight,
|
|
@@ -72,7 +72,7 @@ const TrackColumnIndicator = observer(function ({
|
|
|
72
72
|
<div
|
|
73
73
|
style={{
|
|
74
74
|
position: 'absolute',
|
|
75
|
-
left:
|
|
75
|
+
left: effectiveTreeAreaWidth + rowPanelsWidth + resizeHandleWidth,
|
|
76
76
|
top: 0,
|
|
77
77
|
width: msaCanvasWidth,
|
|
78
78
|
height: totalTrackAreaHeight,
|
|
@@ -105,10 +105,8 @@ export interface MSAViewerProps {
|
|
|
105
105
|
rowPanels?: RowPanelSpec[]
|
|
106
106
|
/** draw the phylogenetic tree (default true); false leaves a labels-only gutter */
|
|
107
107
|
drawTree?: boolean
|
|
108
|
-
/** fixed width (px) of the tree/label area */
|
|
108
|
+
/** fixed width (px) of the tree/label area; unset fits it to the labels and the tree */
|
|
109
109
|
treeAreaWidth?: number
|
|
110
|
-
/** auto-size the tree/label area to the labels (used when drawTree is false) */
|
|
111
|
-
autoTreeAreaWidth?: boolean
|
|
112
110
|
/** draw branch lengths (default true); false draws a cladogram */
|
|
113
111
|
showBranchLen?: boolean
|
|
114
112
|
/**
|
|
@@ -254,7 +252,6 @@ function Viewer({
|
|
|
254
252
|
drawTree,
|
|
255
253
|
residueEncoding,
|
|
256
254
|
treeAreaWidth,
|
|
257
|
-
autoTreeAreaWidth,
|
|
258
255
|
showBranchLen,
|
|
259
256
|
treeOrder,
|
|
260
257
|
treeRoot,
|
|
@@ -303,7 +300,6 @@ function Viewer({
|
|
|
303
300
|
...(rowPanels ? { rowPanels } : {}),
|
|
304
301
|
...(drawTree !== undefined ? { drawTree } : {}),
|
|
305
302
|
...(treeAreaWidth ? { treeAreaWidth } : {}),
|
|
306
|
-
...(autoTreeAreaWidth ? { autoTreeAreaWidth } : {}),
|
|
307
303
|
...(showBranchLen !== undefined ? { showBranchLen } : {}),
|
|
308
304
|
...(treeOrder ? { treeOrder } : {}),
|
|
309
305
|
...(treeRoot ? { treeRoot } : {}),
|
|
@@ -22,7 +22,7 @@ export const VerticalResizeHandle = observer(function ({
|
|
|
22
22
|
<DragHandle
|
|
23
23
|
axis="x"
|
|
24
24
|
variant="resizer"
|
|
25
|
-
getStart={() => model.
|
|
25
|
+
getStart={() => model.effectiveTreeAreaWidth}
|
|
26
26
|
onDrag={onDrag}
|
|
27
27
|
style={{ width: model.resizeHandleWidth }}
|
|
28
28
|
/>
|
package/src/components/Track.tsx
CHANGED
|
@@ -44,7 +44,7 @@ const TrackLabel = observer(function TrackLabel({
|
|
|
44
44
|
const closeMenu = () => {
|
|
45
45
|
setAnchorEl(undefined)
|
|
46
46
|
}
|
|
47
|
-
const { drawLabels, fontSize,
|
|
47
|
+
const { drawLabels, fontSize, effectiveTreeAreaWidth: width } = model
|
|
48
48
|
const {
|
|
49
49
|
model: { name, height },
|
|
50
50
|
} = track
|
|
@@ -49,7 +49,7 @@ const ExportSVGDialog = observer(function ({
|
|
|
49
49
|
const {
|
|
50
50
|
totalWidth,
|
|
51
51
|
totalHeight,
|
|
52
|
-
|
|
52
|
+
effectiveTreeAreaWidth,
|
|
53
53
|
rowPanelsWidth,
|
|
54
54
|
turnedOnTracks,
|
|
55
55
|
totalTrackAreaHeight,
|
|
@@ -59,7 +59,7 @@ const ExportSVGDialog = observer(function ({
|
|
|
59
59
|
treeOverviewHeight,
|
|
60
60
|
} = model
|
|
61
61
|
const hasTracks = turnedOnTracks.length > 0
|
|
62
|
-
const entireWidth = totalWidth +
|
|
62
|
+
const entireWidth = totalWidth + effectiveTreeAreaWidth + rowPanelsWidth
|
|
63
63
|
const entireHeight =
|
|
64
64
|
totalHeight +
|
|
65
65
|
(includeTracks ? totalTrackAreaHeight : 0) +
|
|
@@ -6,9 +6,26 @@ import { observer } from 'mobx-react'
|
|
|
6
6
|
import { findableRowNames, goTo, rowNameSuggestions } from './goTo.ts'
|
|
7
7
|
|
|
8
8
|
import type { MsaViewModel } from '../../model.ts'
|
|
9
|
+
import type { AutocompleteRenderInputParams } from '@mui/material'
|
|
9
10
|
|
|
10
11
|
const label = 'Go to row or column'
|
|
11
12
|
|
|
13
|
+
// MUI 9 hands renderInput `slotProps`; MUI 7, which every released JBrowse host
|
|
14
|
+
// serves, hands it `inputProps` and no `slotProps` at all
|
|
15
|
+
export function labelledInput(params: AutocompleteRenderInputParams) {
|
|
16
|
+
const { slotProps, inputProps } = params as Partial<
|
|
17
|
+
AutocompleteRenderInputParams & { inputProps: object }
|
|
18
|
+
>
|
|
19
|
+
return slotProps
|
|
20
|
+
? {
|
|
21
|
+
slotProps: {
|
|
22
|
+
...slotProps,
|
|
23
|
+
htmlInput: { ...slotProps.htmlInput, 'aria-label': label },
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
: { inputProps: { ...inputProps, 'aria-label': label } }
|
|
27
|
+
}
|
|
28
|
+
|
|
12
29
|
const GoToBox = observer(function ({ model }: { model: MsaViewModel }) {
|
|
13
30
|
const [missed, setMissed] = useState(false)
|
|
14
31
|
const names = findableRowNames(model)
|
|
@@ -37,10 +54,7 @@ const GoToBox = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
37
54
|
error={missed}
|
|
38
55
|
placeholder={label}
|
|
39
56
|
title="A row name, a column number, or name:residue"
|
|
40
|
-
|
|
41
|
-
...params.slotProps,
|
|
42
|
-
htmlInput: { ...params.slotProps.htmlInput, 'aria-label': label },
|
|
43
|
-
}}
|
|
57
|
+
{...labelledInput(params)}
|
|
44
58
|
/>
|
|
45
59
|
)}
|
|
46
60
|
/>
|
|
@@ -76,6 +76,7 @@ export function treeSettingsMenuItems(model: MsaViewModel): MenuItem[] {
|
|
|
76
76
|
drawNodeBubbles,
|
|
77
77
|
drawNodeLabels,
|
|
78
78
|
drawLabels,
|
|
79
|
+
treeAreaWidth,
|
|
79
80
|
showTreeOverview,
|
|
80
81
|
treeOrder,
|
|
81
82
|
treeRoot,
|
|
@@ -145,5 +146,12 @@ export function treeSettingsMenuItems(model: MsaViewModel): MenuItem[] {
|
|
|
145
146
|
toggle('Draw labels', drawLabels, arg => {
|
|
146
147
|
model.setDrawLabels(arg)
|
|
147
148
|
}),
|
|
149
|
+
toggle(
|
|
150
|
+
'Fit tree area to labels and tree',
|
|
151
|
+
treeAreaWidth === undefined,
|
|
152
|
+
arg => {
|
|
153
|
+
model.setTreeAreaWidth(arg ? undefined : model.effectiveTreeAreaWidth)
|
|
154
|
+
},
|
|
155
|
+
),
|
|
148
156
|
]
|
|
149
157
|
}
|
|
@@ -24,14 +24,14 @@ const CladeLabels = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
24
24
|
resolvedClades,
|
|
25
25
|
cladeGutterWidth,
|
|
26
26
|
rowHeight,
|
|
27
|
-
|
|
27
|
+
effectiveTreeAreaWidth,
|
|
28
28
|
fontSize,
|
|
29
29
|
} = model
|
|
30
30
|
const theme = useTheme()
|
|
31
31
|
if (cladeGutterWidth === 0) {
|
|
32
32
|
return null
|
|
33
33
|
}
|
|
34
|
-
const gutterLeft =
|
|
34
|
+
const gutterLeft = effectiveTreeAreaWidth - cladeGutterWidth
|
|
35
35
|
return (
|
|
36
36
|
<>
|
|
37
37
|
{resolvedClades.map((clade, index) => {
|
|
@@ -49,7 +49,7 @@ const CladeLabels = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
49
49
|
position: 'absolute',
|
|
50
50
|
left,
|
|
51
51
|
top: rows[0] * rowHeight,
|
|
52
|
-
width:
|
|
52
|
+
width: effectiveTreeAreaWidth - left - bracketGap,
|
|
53
53
|
height: cladeHeight(clade, rowHeight),
|
|
54
54
|
display: 'flex',
|
|
55
55
|
alignItems: 'center',
|
|
@@ -13,7 +13,13 @@ import type { MsaViewModel } from '../../model.ts'
|
|
|
13
13
|
|
|
14
14
|
const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
15
15
|
const ref = useRef<HTMLDivElement>(null)
|
|
16
|
-
const {
|
|
16
|
+
const {
|
|
17
|
+
height,
|
|
18
|
+
blocksY,
|
|
19
|
+
effectiveTreeAreaWidth,
|
|
20
|
+
scrollY,
|
|
21
|
+
highResScaleFactor,
|
|
22
|
+
} = model
|
|
17
23
|
const onScrollY = useCallback(
|
|
18
24
|
(d: number) => {
|
|
19
25
|
model.doScrollY(d)
|
|
@@ -22,7 +28,7 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
22
28
|
)
|
|
23
29
|
const { onMouseDown, onMouseUp } = useWheelScroll({ ref, onScrollY })
|
|
24
30
|
|
|
25
|
-
const canvasWidth =
|
|
31
|
+
const canvasWidth = effectiveTreeAreaWidth * highResScaleFactor
|
|
26
32
|
const canvasHeight = height * highResScaleFactor
|
|
27
33
|
const mouseoverRef = useCanvasAutorun({
|
|
28
34
|
draw: ctx => {
|
|
@@ -43,7 +49,7 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
43
49
|
style={{
|
|
44
50
|
height,
|
|
45
51
|
position: 'relative',
|
|
46
|
-
width:
|
|
52
|
+
width: effectiveTreeAreaWidth,
|
|
47
53
|
}}
|
|
48
54
|
>
|
|
49
55
|
{/* one transform for the whole block set, matching MSACanvas: the tree
|
|
@@ -70,7 +76,7 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
70
76
|
position: 'absolute',
|
|
71
77
|
top: 0,
|
|
72
78
|
left: 0,
|
|
73
|
-
width:
|
|
79
|
+
width: effectiveTreeAreaWidth,
|
|
74
80
|
height,
|
|
75
81
|
zIndex: 1000,
|
|
76
82
|
pointerEvents: 'none',
|
|
@@ -37,9 +37,9 @@ const TreeCanvasBlock = observer(function ({
|
|
|
37
37
|
)
|
|
38
38
|
const hovered = anchor?.value
|
|
39
39
|
|
|
40
|
-
const {
|
|
41
|
-
// TreePanel clips to
|
|
42
|
-
const width =
|
|
40
|
+
const { effectiveTreeAreaWidth, blockSize, highResScaleFactor } = model
|
|
41
|
+
// TreePanel clips to the tree area, so a wider canvas is wasted backing store
|
|
42
|
+
const width = effectiveTreeAreaWidth
|
|
43
43
|
const height = blockSize
|
|
44
44
|
|
|
45
45
|
const canvasWidth = width * highResScaleFactor
|
|
@@ -121,7 +121,7 @@ const TreeCanvasBlock = observer(function ({
|
|
|
121
121
|
|
|
122
122
|
{anchor ? (
|
|
123
123
|
<BaseTooltip clientPoint={anchor.clientPoint}>
|
|
124
|
-
{anchor.value.name}
|
|
124
|
+
{anchor.value.label ?? anchor.value.name}
|
|
125
125
|
</BaseTooltip>
|
|
126
126
|
) : null}
|
|
127
127
|
</>
|
|
@@ -21,10 +21,10 @@ import type { MsaViewModel } from '../../model.ts'
|
|
|
21
21
|
* moving across it costs are a blit and two boxes.
|
|
22
22
|
*/
|
|
23
23
|
const TreeOverview = observer(function ({ model }: { model: MsaViewModel }) {
|
|
24
|
-
const {
|
|
24
|
+
const { effectiveTreeAreaWidth, overviewHeight, highResScaleFactor } = model
|
|
25
25
|
const theme = useTheme()
|
|
26
26
|
const [hover, setHover] = useState<{ id: string; rows: [number, number] }>()
|
|
27
|
-
const width = Math.round(
|
|
27
|
+
const width = Math.round(effectiveTreeAreaWidth * highResScaleFactor)
|
|
28
28
|
const height = Math.round(overviewHeight * highResScaleFactor)
|
|
29
29
|
|
|
30
30
|
const ref = useCanvasAutorun({
|
|
@@ -34,12 +34,17 @@ const TreeOverview = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
34
34
|
const image = treeOverviewImage({
|
|
35
35
|
model,
|
|
36
36
|
theme,
|
|
37
|
-
width:
|
|
37
|
+
width: effectiveTreeAreaWidth,
|
|
38
38
|
height: overviewHeight,
|
|
39
39
|
scale: highResScaleFactor,
|
|
40
40
|
})
|
|
41
41
|
ctx.scale(highResScaleFactor, highResScaleFactor)
|
|
42
|
-
const box = {
|
|
42
|
+
const box = {
|
|
43
|
+
model,
|
|
44
|
+
ctx,
|
|
45
|
+
width: effectiveTreeAreaWidth,
|
|
46
|
+
height: overviewHeight,
|
|
47
|
+
}
|
|
43
48
|
if (image) {
|
|
44
49
|
ctx.drawImage(
|
|
45
50
|
image,
|
|
@@ -86,7 +91,7 @@ const TreeOverview = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
86
91
|
display: 'block',
|
|
87
92
|
flexShrink: 0,
|
|
88
93
|
cursor: 'pointer',
|
|
89
|
-
width:
|
|
94
|
+
width: effectiveTreeAreaWidth,
|
|
90
95
|
height: overviewHeight,
|
|
91
96
|
}}
|
|
92
97
|
onMouseMove={event => {
|
|
@@ -7,9 +7,15 @@ import TreeCanvas from './TreeCanvas.tsx'
|
|
|
7
7
|
import type { MsaViewModel } from '../../model.ts'
|
|
8
8
|
|
|
9
9
|
const TreePanel = observer(function ({ model }: { model: MsaViewModel }) {
|
|
10
|
-
const {
|
|
10
|
+
const { effectiveTreeAreaWidth } = model
|
|
11
11
|
return (
|
|
12
|
-
<div
|
|
12
|
+
<div
|
|
13
|
+
style={{
|
|
14
|
+
overflow: 'hidden',
|
|
15
|
+
flexShrink: 0,
|
|
16
|
+
width: effectiveTreeAreaWidth,
|
|
17
|
+
}}
|
|
18
|
+
>
|
|
13
19
|
<TreeCanvas model={model} />
|
|
14
20
|
</div>
|
|
15
21
|
)
|
|
@@ -13,13 +13,17 @@ import type { MsaViewModel } from '../../model.ts'
|
|
|
13
13
|
// scale bar: without one the branch lengths are drawn to a scale nothing
|
|
14
14
|
// states.
|
|
15
15
|
const TreeRuler = observer(({ model }: { model: MsaViewModel }) => {
|
|
16
|
-
const {
|
|
16
|
+
const { effectiveTreeAreaWidth, marginLeft, treeScaleBar: bar } = model
|
|
17
17
|
const theme = useTheme()
|
|
18
18
|
|
|
19
19
|
return (
|
|
20
|
-
<div style={{ flexShrink: 0, width:
|
|
20
|
+
<div style={{ flexShrink: 0, width: effectiveTreeAreaWidth }}>
|
|
21
21
|
{bar ? (
|
|
22
|
-
<svg
|
|
22
|
+
<svg
|
|
23
|
+
width={effectiveTreeAreaWidth}
|
|
24
|
+
height={h}
|
|
25
|
+
style={{ display: 'block' }}
|
|
26
|
+
>
|
|
23
27
|
<ScaleBarSVG
|
|
24
28
|
bar={bar}
|
|
25
29
|
marginLeft={marginLeft}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { labelFitFontSize } from '../../constants.ts'
|
|
1
2
|
import { measureTextCanvas } from '../../measureTextCanvas.ts'
|
|
2
3
|
|
|
3
|
-
import type { ResolvedClade } from '../../types.ts'
|
|
4
|
+
import type { Clade, ResolvedClade } from '../../types.ts'
|
|
4
5
|
|
|
5
6
|
// The bar of the bracket mark and the space around it, in px.
|
|
6
7
|
export const bracketBarWidth = 3
|
|
@@ -11,7 +12,7 @@ export const bracketGap = 4
|
|
|
11
12
|
export const maxCladeGutterWidth = 140
|
|
12
13
|
|
|
13
14
|
/** whether a clade draws in the gutter: a bracket, or a highlight with a label */
|
|
14
|
-
export function cladeInGutter(clade:
|
|
15
|
+
export function cladeInGutter(clade: Pick<Clade, 'mark' | 'label'>) {
|
|
15
16
|
return (
|
|
16
17
|
clade.mark === 'bracket' || (clade.mark === 'highlight' && !!clade.label)
|
|
17
18
|
)
|
|
@@ -35,7 +36,7 @@ export function cladeLabelHorizontal(
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
/** the px between the left edge of the gutter and the label's own column */
|
|
38
|
-
export function cladeLabelOffset(clade:
|
|
39
|
+
export function cladeLabelOffset(clade: Pick<Clade, 'mark'>) {
|
|
39
40
|
return clade.mark === 'bracket'
|
|
40
41
|
? bracketGap + bracketBarWidth + bracketGap
|
|
41
42
|
: bracketGap
|
|
@@ -43,27 +44,19 @@ export function cladeLabelOffset(clade: ResolvedClade) {
|
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* The gutter the bracket mark needs at the right of the tree area: the bar plus
|
|
46
|
-
* the widest label
|
|
47
|
-
* most maxCladeGutterWidth.
|
|
47
|
+
* the widest label read across at labelFitFontSize, zero where no clade draws
|
|
48
|
+
* there, and at most maxCladeGutterWidth. The records size it, so a zoom, a
|
|
49
|
+
* collapse or a focus leaves it as it is, and a renderer clips a label that
|
|
50
|
+
* outgrows it.
|
|
48
51
|
*/
|
|
49
|
-
export function cladeGutterWidth({
|
|
50
|
-
clades,
|
|
51
|
-
rowHeight,
|
|
52
|
-
fontSize,
|
|
53
|
-
}: {
|
|
54
|
-
clades: ResolvedClade[]
|
|
55
|
-
rowHeight: number
|
|
56
|
-
fontSize: number
|
|
57
|
-
}) {
|
|
52
|
+
export function cladeGutterWidth(clades: Pick<Clade, 'mark' | 'label'>[]) {
|
|
58
53
|
let widest = 0
|
|
59
54
|
for (const clade of clades) {
|
|
60
55
|
if (!cladeInGutter(clade)) {
|
|
61
56
|
continue
|
|
62
57
|
}
|
|
63
58
|
const text = clade.label
|
|
64
|
-
?
|
|
65
|
-
? measureTextCanvas(clade.label, fontSize)
|
|
66
|
-
: fontSize
|
|
59
|
+
? measureTextCanvas(clade.label, labelFitFontSize)
|
|
67
60
|
: 0
|
|
68
61
|
widest = Math.max(widest, cladeLabelOffset(clade) + text + bracketGap)
|
|
69
62
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const ellipsis = '…'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* the longest prefix of `text`, plus an ellipsis, that measures at most
|
|
5
|
+
* `room` px
|
|
6
|
+
*/
|
|
7
|
+
export function ellipsize(
|
|
8
|
+
measure: (text: string) => number,
|
|
9
|
+
text: string,
|
|
10
|
+
room: number,
|
|
11
|
+
) {
|
|
12
|
+
const width = measure(text)
|
|
13
|
+
if (width <= room) {
|
|
14
|
+
return { text, width }
|
|
15
|
+
}
|
|
16
|
+
for (let n = Math.floor((text.length * room) / width); n > 0; n--) {
|
|
17
|
+
const truncated = text.slice(0, n).trimEnd() + ellipsis
|
|
18
|
+
const truncatedWidth = measure(truncated)
|
|
19
|
+
if (truncatedWidth <= room) {
|
|
20
|
+
return { text: truncated, width: truncatedWidth }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return { text: '', width: 0 }
|
|
24
|
+
}
|