react-msaview 3.1.0 → 3.1.2
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 +23 -23
- package/dist/calculateBlocks.d.ts +12 -0
- package/dist/calculateBlocks.js +23 -0
- package/dist/calculateBlocks.js.map +1 -0
- package/dist/components/ExportSVGDialog.d.ts +6 -0
- package/dist/components/ExportSVGDialog.js +26 -0
- package/dist/components/ExportSVGDialog.js.map +1 -0
- package/dist/components/MSAPanel/MSABlock.js +2 -3
- package/dist/components/MSAPanel/MSABlock.js.map +1 -1
- package/dist/components/MSAPanel/renderMSABlock.d.ts +1 -1
- package/dist/components/MSAPanel/renderMSABlock.js +1 -1
- package/dist/components/MSAPanel/renderMSABlock.js.map +1 -1
- package/dist/components/MinimapSVG.d.ts +6 -0
- package/dist/components/MinimapSVG.js +25 -0
- package/dist/components/MinimapSVG.js.map +1 -0
- package/dist/components/TreePanel/renderTreeCanvas.js +7 -7
- package/dist/components/TreePanel/renderTreeCanvas.js.map +1 -1
- package/dist/components/ZoomControls.js +9 -7
- package/dist/components/ZoomControls.js.map +1 -1
- package/dist/model.d.ts +53 -42
- package/dist/model.js +71 -71
- package/dist/model.js.map +1 -1
- package/dist/renderToSvg.d.ts +1 -0
- package/dist/renderToSvg.js +25 -12
- package/dist/renderToSvg.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/calculateBlocks.ts +58 -0
- package/src/components/ExportSVGDialog.tsx +61 -0
- package/src/components/MSAPanel/MSABlock.tsx +2 -3
- package/src/components/MSAPanel/renderMSABlock.ts +1 -1
- package/src/components/MinimapSVG.tsx +57 -0
- package/src/components/TreePanel/renderTreeCanvas.ts +8 -5
- package/src/components/ZoomControls.tsx +15 -6
- package/src/model.ts +76 -90
- package/src/renderToSvg.tsx +50 -19
- package/src/version.ts +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { IconButton
|
|
1
|
+
import React, { Suspense, lazy, useState } from 'react'
|
|
2
|
+
import { IconButton } from '@mui/material'
|
|
3
3
|
import { observer } from 'mobx-react'
|
|
4
4
|
import CascadingMenuButton from '@jbrowse/core/ui/CascadingMenuButton'
|
|
5
5
|
|
|
@@ -11,13 +11,16 @@ import MoreVert from '@mui/icons-material/MoreVert'
|
|
|
11
11
|
import ZoomIn from '@mui/icons-material/ZoomIn'
|
|
12
12
|
import ZoomOut from '@mui/icons-material/ZoomOut'
|
|
13
13
|
|
|
14
|
+
// lazies
|
|
15
|
+
const ExportSVGDialog = lazy(() => import('./ExportSVGDialog'))
|
|
16
|
+
|
|
14
17
|
const ZoomControls = observer(function ZoomControls({
|
|
15
18
|
model,
|
|
16
19
|
}: {
|
|
17
20
|
model: MsaViewModel
|
|
18
21
|
}) {
|
|
19
22
|
const { colWidth, rowHeight } = model
|
|
20
|
-
const
|
|
23
|
+
const [exportSvgDialogOpen, setExportSvgDialogOpen] = useState(false)
|
|
21
24
|
return (
|
|
22
25
|
<>
|
|
23
26
|
<IconButton
|
|
@@ -63,14 +66,20 @@ const ZoomControls = observer(function ZoomControls({
|
|
|
63
66
|
},
|
|
64
67
|
{
|
|
65
68
|
label: 'Export SVG',
|
|
66
|
-
onClick: () =>
|
|
67
|
-
model.exportSVG({ theme })
|
|
68
|
-
},
|
|
69
|
+
onClick: () => setExportSvgDialogOpen(true),
|
|
69
70
|
},
|
|
70
71
|
]}
|
|
71
72
|
>
|
|
72
73
|
<MoreVert />
|
|
73
74
|
</CascadingMenuButton>
|
|
75
|
+
{exportSvgDialogOpen ? (
|
|
76
|
+
<Suspense fallback={null}>
|
|
77
|
+
<ExportSVGDialog
|
|
78
|
+
model={model}
|
|
79
|
+
onClose={() => setExportSvgDialogOpen(false)}
|
|
80
|
+
/>
|
|
81
|
+
</Suspense>
|
|
82
|
+
) : null}
|
|
74
83
|
</>
|
|
75
84
|
)
|
|
76
85
|
})
|
package/src/model.ts
CHANGED
|
@@ -36,6 +36,7 @@ import { StructureModel } from './StructureModel'
|
|
|
36
36
|
import { DialogQueueSessionMixin } from './DialogQueue'
|
|
37
37
|
import { renderToSvg } from './renderToSvg'
|
|
38
38
|
import { Theme } from '@mui/material'
|
|
39
|
+
import { blocksX, blocksY } from './calculateBlocks'
|
|
39
40
|
|
|
40
41
|
export interface RowDetails {
|
|
41
42
|
[key: string]: unknown
|
|
@@ -594,85 +595,7 @@ const model = types
|
|
|
594
595
|
},
|
|
595
596
|
}))
|
|
596
597
|
|
|
597
|
-
.views(self => {
|
|
598
|
-
let oldBlocksX: number[] = []
|
|
599
|
-
let oldBlocksY: number[] = []
|
|
600
|
-
let oldValX = 0
|
|
601
|
-
let oldValY = 0
|
|
602
|
-
return {
|
|
603
|
-
/**
|
|
604
|
-
* #getter
|
|
605
|
-
*/
|
|
606
|
-
get initialized() {
|
|
607
|
-
return (
|
|
608
|
-
(self.data.msa ||
|
|
609
|
-
self.data.tree ||
|
|
610
|
-
self.msaFilehandle ||
|
|
611
|
-
self.treeFilehandle) &&
|
|
612
|
-
!self.error
|
|
613
|
-
)
|
|
614
|
-
},
|
|
615
|
-
/**
|
|
616
|
-
* #getter
|
|
617
|
-
*/
|
|
618
|
-
get blocksX() {
|
|
619
|
-
const { scrollX, blockSize: size, colWidth } = self
|
|
620
|
-
const ret = -(size * Math.floor(scrollX / size)) - size
|
|
621
|
-
|
|
622
|
-
const b = []
|
|
623
|
-
for (let i = ret; i < ret + size * 3; i += size) {
|
|
624
|
-
if (i + size > 0) {
|
|
625
|
-
b.push(i)
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
if (
|
|
629
|
-
JSON.stringify(b) !== JSON.stringify(oldBlocksX) ||
|
|
630
|
-
colWidth !== oldValX
|
|
631
|
-
) {
|
|
632
|
-
oldBlocksX = b
|
|
633
|
-
oldValX = colWidth
|
|
634
|
-
}
|
|
635
|
-
return oldBlocksX
|
|
636
|
-
},
|
|
637
|
-
/**
|
|
638
|
-
* #getter
|
|
639
|
-
*/
|
|
640
|
-
get blocksY() {
|
|
641
|
-
const { scrollY, blockSize: size, rowHeight } = self
|
|
642
|
-
const ret = -(size * Math.floor(scrollY / size)) - 2 * size
|
|
643
|
-
|
|
644
|
-
const b = []
|
|
645
|
-
for (let i = ret; i < ret + size * 3; i += size) {
|
|
646
|
-
if (i + size > 0) {
|
|
647
|
-
b.push(i)
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
if (
|
|
651
|
-
JSON.stringify(b) !== JSON.stringify(oldBlocksY) ||
|
|
652
|
-
rowHeight !== oldValY
|
|
653
|
-
) {
|
|
654
|
-
oldBlocksY = b
|
|
655
|
-
oldValY = rowHeight
|
|
656
|
-
}
|
|
657
|
-
return oldBlocksY
|
|
658
|
-
},
|
|
659
|
-
}
|
|
660
|
-
})
|
|
661
598
|
.views(self => ({
|
|
662
|
-
/**
|
|
663
|
-
* #getter
|
|
664
|
-
*/
|
|
665
|
-
get blocks2d() {
|
|
666
|
-
return self.blocksY.flatMap(by => self.blocksX.map(bx => [bx, by]))
|
|
667
|
-
},
|
|
668
|
-
|
|
669
|
-
/**
|
|
670
|
-
* #getter
|
|
671
|
-
*/
|
|
672
|
-
get done() {
|
|
673
|
-
return self.initialized && (self.data.msa || self.data.tree)
|
|
674
|
-
},
|
|
675
|
-
|
|
676
599
|
/**
|
|
677
600
|
* #getter
|
|
678
601
|
*/
|
|
@@ -933,6 +856,78 @@ const model = types
|
|
|
933
856
|
return this.root.leaves().length * self.rowHeight
|
|
934
857
|
},
|
|
935
858
|
}))
|
|
859
|
+
.views(self => ({
|
|
860
|
+
/**
|
|
861
|
+
* #getter
|
|
862
|
+
*/
|
|
863
|
+
get totalWidth() {
|
|
864
|
+
return self.numColumns * self.colWidth
|
|
865
|
+
},
|
|
866
|
+
}))
|
|
867
|
+
|
|
868
|
+
.views(self => ({
|
|
869
|
+
/**
|
|
870
|
+
* #getter
|
|
871
|
+
*/
|
|
872
|
+
get initialized() {
|
|
873
|
+
return (
|
|
874
|
+
(self.data.msa ||
|
|
875
|
+
self.data.tree ||
|
|
876
|
+
self.msaFilehandle ||
|
|
877
|
+
self.treeFilehandle) &&
|
|
878
|
+
!self.error
|
|
879
|
+
)
|
|
880
|
+
},
|
|
881
|
+
/**
|
|
882
|
+
* #getter
|
|
883
|
+
*/
|
|
884
|
+
get blocksX() {
|
|
885
|
+
return blocksX({
|
|
886
|
+
viewportWidth: self.msaAreaWidth,
|
|
887
|
+
viewportX: -self.scrollX,
|
|
888
|
+
blockSize: self.blockSize,
|
|
889
|
+
mapWidth: self.totalWidth,
|
|
890
|
+
})
|
|
891
|
+
},
|
|
892
|
+
/**
|
|
893
|
+
* #getter
|
|
894
|
+
*/
|
|
895
|
+
get blocksY() {
|
|
896
|
+
return blocksY({
|
|
897
|
+
viewportHeight: self.height,
|
|
898
|
+
viewportY: -self.scrollY,
|
|
899
|
+
blockSize: self.blockSize,
|
|
900
|
+
mapHeight: self.totalHeight,
|
|
901
|
+
})
|
|
902
|
+
},
|
|
903
|
+
}))
|
|
904
|
+
.views(self => ({
|
|
905
|
+
/**
|
|
906
|
+
* #getter
|
|
907
|
+
*/
|
|
908
|
+
get blocks2d() {
|
|
909
|
+
const ret = []
|
|
910
|
+
for (const by of self.blocksY) {
|
|
911
|
+
for (const bx of self.blocksX) {
|
|
912
|
+
ret.push([bx, by])
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return ret
|
|
916
|
+
},
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* #getter
|
|
920
|
+
*/
|
|
921
|
+
get done() {
|
|
922
|
+
return self.initialized && (self.data.msa || self.data.tree)
|
|
923
|
+
},
|
|
924
|
+
/**
|
|
925
|
+
* #getter
|
|
926
|
+
*/
|
|
927
|
+
get maxScrollX() {
|
|
928
|
+
return -self.totalWidth + (self.msaAreaWidth - 100)
|
|
929
|
+
},
|
|
930
|
+
}))
|
|
936
931
|
.actions(self => ({
|
|
937
932
|
/**
|
|
938
933
|
* #action
|
|
@@ -962,23 +957,14 @@ const model = types
|
|
|
962
957
|
* #action
|
|
963
958
|
*/
|
|
964
959
|
doScrollX(deltaX: number) {
|
|
965
|
-
self.scrollX = clamp(
|
|
966
|
-
-(self.numColumns * self.colWidth) + (self.msaAreaWidth - 100),
|
|
967
|
-
self.scrollX + deltaX,
|
|
968
|
-
0,
|
|
969
|
-
)
|
|
960
|
+
self.scrollX = clamp(self.maxScrollX, self.scrollX + deltaX, 0)
|
|
970
961
|
},
|
|
971
962
|
|
|
972
963
|
/**
|
|
973
964
|
* #action
|
|
974
965
|
*/
|
|
975
966
|
setScrollX(n: number) {
|
|
976
|
-
|
|
977
|
-
self.scrollX = clamp(
|
|
978
|
-
-(self.numColumns * self.colWidth) + (self.msaAreaWidth - 100),
|
|
979
|
-
n,
|
|
980
|
-
0,
|
|
981
|
-
)
|
|
967
|
+
self.scrollX = clamp(self.maxScrollX, n, 0)
|
|
982
968
|
},
|
|
983
969
|
|
|
984
970
|
/**
|
|
@@ -1234,7 +1220,7 @@ const model = types
|
|
|
1234
1220
|
/**
|
|
1235
1221
|
* #action
|
|
1236
1222
|
*/
|
|
1237
|
-
async exportSVG(opts: { theme: Theme }) {
|
|
1223
|
+
async exportSVG(opts: { theme: Theme; includeMinimap?: boolean }) {
|
|
1238
1224
|
const html = await renderToSvg(self as MsaViewModel, opts)
|
|
1239
1225
|
const blob = new Blob([html], { type: 'image/svg+xml' })
|
|
1240
1226
|
saveAs(blob, 'image.svg')
|
package/src/renderToSvg.tsx
CHANGED
|
@@ -7,15 +7,19 @@ import { Theme } from '@mui/material'
|
|
|
7
7
|
// locals
|
|
8
8
|
import { MsaViewModel } from './model'
|
|
9
9
|
import { renderTreeCanvas } from './components/TreePanel/renderTreeCanvas'
|
|
10
|
-
import {
|
|
10
|
+
import { renderMSABlock } from './components/MSAPanel/renderMSABlock'
|
|
11
11
|
import { colorContrast } from './util'
|
|
12
|
+
import MinimapSVG from './components/MinimapSVG'
|
|
12
13
|
|
|
13
14
|
// render LGV to SVG
|
|
14
|
-
export async function renderToSvg(
|
|
15
|
+
export async function renderToSvg(
|
|
16
|
+
model: MsaViewModel,
|
|
17
|
+
opts: { theme: Theme; includeMinimap?: boolean },
|
|
18
|
+
) {
|
|
15
19
|
await when(() => !!model.initialized)
|
|
16
20
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
21
|
const { width, height, scrollX, scrollY, colorScheme, treeAreaWidth } = model
|
|
18
|
-
const { theme } = opts
|
|
22
|
+
const { theme, includeMinimap } = opts
|
|
19
23
|
|
|
20
24
|
const { Context } = await import('svgcanvas')
|
|
21
25
|
const ctx1 = Context(width, height)
|
|
@@ -29,7 +33,7 @@ export async function renderToSvg(model: MsaViewModel, opts: { theme: Theme }) {
|
|
|
29
33
|
blockSizeYOverride: height,
|
|
30
34
|
highResScaleFactorOverride: 1,
|
|
31
35
|
})
|
|
32
|
-
|
|
36
|
+
renderMSABlock({
|
|
33
37
|
model,
|
|
34
38
|
offsetY: scrollY,
|
|
35
39
|
offsetX: -scrollX,
|
|
@@ -40,6 +44,7 @@ export async function renderToSvg(model: MsaViewModel, opts: { theme: Theme }) {
|
|
|
40
44
|
highResScaleFactorOverride: 1,
|
|
41
45
|
})
|
|
42
46
|
|
|
47
|
+
const Wrapper = includeMinimap ? MinimapWrapper : NullWrapper
|
|
43
48
|
const clipId = 'tree'
|
|
44
49
|
// the xlink namespace is used for rendering <image> tag
|
|
45
50
|
return renderToStaticMarkup(
|
|
@@ -50,22 +55,48 @@ export async function renderToSvg(model: MsaViewModel, opts: { theme: Theme }) {
|
|
|
50
55
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
|
51
56
|
viewBox={[0, 0, width, height].toString()}
|
|
52
57
|
>
|
|
53
|
-
<
|
|
54
|
-
<
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
58
|
+
<Wrapper model={model}>
|
|
59
|
+
<defs>
|
|
60
|
+
<clipPath id={clipId}>
|
|
61
|
+
<rect x={0} y={0} width={treeAreaWidth} height={height} />
|
|
62
|
+
</clipPath>
|
|
63
|
+
</defs>
|
|
64
|
+
<g
|
|
65
|
+
clipPath={`url(#${clipId})`}
|
|
66
|
+
/* eslint-disable-next-line react/no-danger */
|
|
67
|
+
dangerouslySetInnerHTML={{ __html: ctx1.getSvg().innerHTML }}
|
|
68
|
+
/>
|
|
69
|
+
<g
|
|
70
|
+
transform={`translate(${treeAreaWidth} 0)`}
|
|
71
|
+
/* eslint-disable-next-line react/no-danger */
|
|
72
|
+
dangerouslySetInnerHTML={{ __html: ctx2.getSvg().innerHTML }}
|
|
73
|
+
/>
|
|
74
|
+
</Wrapper>
|
|
68
75
|
</svg>,
|
|
69
76
|
createRoot,
|
|
70
77
|
)
|
|
71
78
|
}
|
|
79
|
+
|
|
80
|
+
function MinimapWrapper({
|
|
81
|
+
model,
|
|
82
|
+
children,
|
|
83
|
+
}: {
|
|
84
|
+
model: MsaViewModel
|
|
85
|
+
children: React.ReactNode
|
|
86
|
+
}) {
|
|
87
|
+
const { minimapHeight, treeAreaWidth } = model
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<>
|
|
91
|
+
<g transform={`translate(${treeAreaWidth} 0)`}>
|
|
92
|
+
<MinimapSVG model={model} />
|
|
93
|
+
</g>
|
|
94
|
+
|
|
95
|
+
<g transform={`translate(0 ${minimapHeight})`}>{children}</g>
|
|
96
|
+
</>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function NullWrapper({ children }: { children: React.ReactNode }) {
|
|
101
|
+
return <>{children}</>
|
|
102
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '3.1.
|
|
1
|
+
export const version = '3.1.2'
|