react-msaview 3.1.2 → 3.1.3
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 +30 -30
- package/dist/components/ExportSVGDialog.js +17 -4
- package/dist/components/ExportSVGDialog.js.map +1 -1
- package/dist/model.d.ts +47 -40
- package/dist/model.js.map +1 -1
- package/dist/renderToSvg.d.ts +1 -0
- package/dist/renderToSvg.js +77 -22
- package/dist/renderToSvg.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -23
- package/src/components/ExportSVGDialog.tsx +48 -13
- package/src/model.ts +10 -5
- package/src/renderToSvg.tsx +155 -40
- package/src/version.ts +1 -1
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
2
|
import { Dialog, ErrorMessage } from '@jbrowse/core/ui'
|
|
3
3
|
import {
|
|
4
|
-
DialogContent,
|
|
5
|
-
DialogActions,
|
|
6
4
|
Button,
|
|
7
5
|
Checkbox,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogActions,
|
|
8
8
|
FormControlLabel,
|
|
9
|
-
|
|
9
|
+
FormControl,
|
|
10
|
+
FormLabel,
|
|
11
|
+
RadioGroup,
|
|
12
|
+
Radio,
|
|
10
13
|
Typography,
|
|
14
|
+
useTheme,
|
|
11
15
|
} from '@mui/material'
|
|
12
16
|
import { MsaViewModel } from '../model'
|
|
13
17
|
|
|
@@ -19,22 +23,48 @@ export default function ExportSVGDialog({
|
|
|
19
23
|
onClose: () => void
|
|
20
24
|
}) {
|
|
21
25
|
const [includeMinimap, setIncludeMinimap] = useState(true)
|
|
26
|
+
const [exportType, setExportType] = useState('viewport')
|
|
22
27
|
const [error, setError] = useState<unknown>()
|
|
23
28
|
const theme = useTheme()
|
|
24
29
|
return (
|
|
25
30
|
<Dialog onClose={() => onClose()} open title="Export SVG">
|
|
26
31
|
<DialogContent>
|
|
27
|
-
<Typography>Export SVG of current view</Typography>
|
|
28
32
|
{error ? <ErrorMessage error={error} /> : null}
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
<Typography>Settings:</Typography>
|
|
34
|
+
<div>
|
|
35
|
+
<FormControl>
|
|
36
|
+
<FormControlLabel
|
|
37
|
+
control={
|
|
38
|
+
<Checkbox
|
|
39
|
+
checked={includeMinimap}
|
|
40
|
+
onChange={event => setIncludeMinimap(event.target.checked)}
|
|
41
|
+
/>
|
|
42
|
+
}
|
|
43
|
+
disabled={exportType === 'entire'}
|
|
44
|
+
label="Include minimap?"
|
|
34
45
|
/>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
</FormControl>
|
|
47
|
+
</div>
|
|
48
|
+
<div>
|
|
49
|
+
<FormControl>
|
|
50
|
+
<FormLabel>Export type</FormLabel>
|
|
51
|
+
<RadioGroup
|
|
52
|
+
value={exportType}
|
|
53
|
+
onChange={event => setExportType(event.target.value)}
|
|
54
|
+
>
|
|
55
|
+
<FormControlLabel
|
|
56
|
+
value="entire"
|
|
57
|
+
control={<Radio />}
|
|
58
|
+
label="Entire MSA"
|
|
59
|
+
/>
|
|
60
|
+
<FormControlLabel
|
|
61
|
+
value="viewport"
|
|
62
|
+
control={<Radio />}
|
|
63
|
+
label="Current viewport only"
|
|
64
|
+
/>
|
|
65
|
+
</RadioGroup>
|
|
66
|
+
</FormControl>
|
|
67
|
+
</div>
|
|
38
68
|
</DialogContent>
|
|
39
69
|
<DialogActions>
|
|
40
70
|
<Button
|
|
@@ -42,7 +72,12 @@ export default function ExportSVGDialog({
|
|
|
42
72
|
color="primary"
|
|
43
73
|
onClick={async () => {
|
|
44
74
|
try {
|
|
45
|
-
await model.exportSVG({
|
|
75
|
+
await model.exportSVG({
|
|
76
|
+
theme,
|
|
77
|
+
includeMinimap:
|
|
78
|
+
exportType === 'entire' ? false : includeMinimap,
|
|
79
|
+
exportType,
|
|
80
|
+
})
|
|
46
81
|
} catch (e) {
|
|
47
82
|
console.error(e)
|
|
48
83
|
setError(e)
|
package/src/model.ts
CHANGED
|
@@ -44,13 +44,13 @@ export interface RowDetails {
|
|
|
44
44
|
range?: { start: number; end: number }
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
interface BasicTrackModel {
|
|
47
|
+
export interface BasicTrackModel {
|
|
48
48
|
id: string
|
|
49
49
|
name: string
|
|
50
50
|
associatedRowName?: string
|
|
51
51
|
height: number
|
|
52
52
|
}
|
|
53
|
-
interface Structure {
|
|
53
|
+
export interface Structure {
|
|
54
54
|
pdb: string
|
|
55
55
|
startPos: number
|
|
56
56
|
endPos: number
|
|
@@ -79,8 +79,9 @@ export interface IBoxTrack {
|
|
|
79
79
|
model: BoxTrackModel
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
type BasicTrack = IBoxTrack | ITextTrack
|
|
83
|
-
|
|
82
|
+
export type BasicTrack = IBoxTrack | ITextTrack
|
|
83
|
+
|
|
84
|
+
export type StructureSnap = SnapshotIn<typeof StructureModel>
|
|
84
85
|
|
|
85
86
|
/**
|
|
86
87
|
* #stateModel MsaView
|
|
@@ -1220,7 +1221,11 @@ const model = types
|
|
|
1220
1221
|
/**
|
|
1221
1222
|
* #action
|
|
1222
1223
|
*/
|
|
1223
|
-
async exportSVG(opts: {
|
|
1224
|
+
async exportSVG(opts: {
|
|
1225
|
+
theme: Theme
|
|
1226
|
+
includeMinimap?: boolean
|
|
1227
|
+
exportType: string
|
|
1228
|
+
}) {
|
|
1224
1229
|
const html = await renderToSvg(self as MsaViewModel, opts)
|
|
1225
1230
|
const blob = new Blob([html], { type: 'image/svg+xml' })
|
|
1226
1231
|
saveAs(blob, 'image.svg')
|
package/src/renderToSvg.tsx
CHANGED
|
@@ -11,23 +11,129 @@ import { renderMSABlock } from './components/MSAPanel/renderMSABlock'
|
|
|
11
11
|
import { colorContrast } from './util'
|
|
12
12
|
import MinimapSVG from './components/MinimapSVG'
|
|
13
13
|
|
|
14
|
-
// render LGV to SVG
|
|
15
14
|
export async function renderToSvg(
|
|
16
15
|
model: MsaViewModel,
|
|
17
|
-
opts: { theme: Theme; includeMinimap?: boolean },
|
|
16
|
+
opts: { theme: Theme; includeMinimap?: boolean; exportType: string },
|
|
18
17
|
) {
|
|
19
18
|
await when(() => !!model.initialized)
|
|
20
|
-
|
|
21
|
-
const {
|
|
22
|
-
const { theme, includeMinimap } = opts
|
|
19
|
+
const { width, height, scrollX, scrollY } = model
|
|
20
|
+
const { exportType, theme, includeMinimap } = opts
|
|
23
21
|
|
|
22
|
+
if (exportType === 'entire') {
|
|
23
|
+
return render({
|
|
24
|
+
width: model.totalWidth + model.treeAreaWidth,
|
|
25
|
+
height: model.totalHeight,
|
|
26
|
+
theme,
|
|
27
|
+
model,
|
|
28
|
+
offsetY: 0,
|
|
29
|
+
offsetX: 0,
|
|
30
|
+
includeMinimap,
|
|
31
|
+
})
|
|
32
|
+
} else if (exportType === 'viewport') {
|
|
33
|
+
return render({
|
|
34
|
+
width,
|
|
35
|
+
height,
|
|
36
|
+
theme,
|
|
37
|
+
model,
|
|
38
|
+
offsetY: scrollY,
|
|
39
|
+
offsetX: -scrollX,
|
|
40
|
+
includeMinimap,
|
|
41
|
+
})
|
|
42
|
+
} else {
|
|
43
|
+
throw new Error('unknown export type')
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function render({
|
|
48
|
+
width,
|
|
49
|
+
height,
|
|
50
|
+
offsetX,
|
|
51
|
+
offsetY,
|
|
52
|
+
theme,
|
|
53
|
+
model,
|
|
54
|
+
includeMinimap,
|
|
55
|
+
}: {
|
|
56
|
+
width: number
|
|
57
|
+
height: number
|
|
58
|
+
offsetX: number
|
|
59
|
+
offsetY: number
|
|
60
|
+
theme: Theme
|
|
61
|
+
model: MsaViewModel
|
|
62
|
+
includeMinimap?: boolean
|
|
63
|
+
}) {
|
|
24
64
|
const { Context } = await import('svgcanvas')
|
|
65
|
+
const Wrapper = includeMinimap ? MinimapWrapper : NullWrapper
|
|
66
|
+
|
|
67
|
+
return renderToStaticMarkup(
|
|
68
|
+
<SvgWrapper width={width} height={height}>
|
|
69
|
+
<Wrapper model={model}>
|
|
70
|
+
<CoreRendering
|
|
71
|
+
Context={Context}
|
|
72
|
+
model={model}
|
|
73
|
+
theme={theme}
|
|
74
|
+
offsetX={offsetX}
|
|
75
|
+
offsetY={offsetY}
|
|
76
|
+
width={width}
|
|
77
|
+
height={height}
|
|
78
|
+
/>
|
|
79
|
+
</Wrapper>
|
|
80
|
+
</SvgWrapper>,
|
|
81
|
+
createRoot,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// function renderMultiline({ model }: { model: MsaViewModel }) {
|
|
86
|
+
// const { treeAreaWidth, height } = model
|
|
87
|
+
// const clipId = 'tree'
|
|
88
|
+
// return (
|
|
89
|
+
// <Wrapper model={model}>
|
|
90
|
+
// <defs>
|
|
91
|
+
// <clipPath id={clipId}>
|
|
92
|
+
// <rect x={0} y={0} width={treeAreaWidth} height={height} />
|
|
93
|
+
// </clipPath>
|
|
94
|
+
// </defs>
|
|
95
|
+
// <g
|
|
96
|
+
// clipPath={`url(#${clipId})`}
|
|
97
|
+
// /* eslint-disable-next-line react/no-danger */
|
|
98
|
+
// dangerouslySetInnerHTML={{ __html: ctx1.getSvg().innerHTML }}
|
|
99
|
+
// />
|
|
100
|
+
// <g
|
|
101
|
+
// transform={`translate(${treeAreaWidth} 0)`}
|
|
102
|
+
// /* eslint-disable-next-line react/no-danger */
|
|
103
|
+
// dangerouslySetInnerHTML={{ __html: ctx2.getSvg().innerHTML }}
|
|
104
|
+
// />
|
|
105
|
+
// </Wrapper>
|
|
106
|
+
// )
|
|
107
|
+
// }
|
|
108
|
+
|
|
109
|
+
function CoreRendering({
|
|
110
|
+
model,
|
|
111
|
+
theme,
|
|
112
|
+
width,
|
|
113
|
+
height,
|
|
114
|
+
offsetX,
|
|
115
|
+
offsetY,
|
|
116
|
+
Context,
|
|
117
|
+
}: {
|
|
118
|
+
model: MsaViewModel
|
|
119
|
+
theme: Theme
|
|
120
|
+
width: number
|
|
121
|
+
height: number
|
|
122
|
+
offsetX: number
|
|
123
|
+
offsetY: number
|
|
124
|
+
Context: (
|
|
125
|
+
width: number,
|
|
126
|
+
height: number,
|
|
127
|
+
) => CanvasRenderingContext2D & { getSvg: () => { innerHTML: string } }
|
|
128
|
+
}) {
|
|
129
|
+
const clipId = 'tree'
|
|
130
|
+
const { treeAreaWidth, colorScheme } = model
|
|
131
|
+
const contrastScheme = colorContrast(colorScheme, theme)
|
|
25
132
|
const ctx1 = Context(width, height)
|
|
26
133
|
const ctx2 = Context(width, height)
|
|
27
|
-
const contrastScheme = colorContrast(colorScheme, theme)
|
|
28
134
|
renderTreeCanvas({
|
|
29
135
|
model,
|
|
30
|
-
offsetY
|
|
136
|
+
offsetY,
|
|
31
137
|
ctx: ctx1,
|
|
32
138
|
theme,
|
|
33
139
|
blockSizeYOverride: height,
|
|
@@ -35,45 +141,32 @@ export async function renderToSvg(
|
|
|
35
141
|
})
|
|
36
142
|
renderMSABlock({
|
|
37
143
|
model,
|
|
38
|
-
offsetY
|
|
39
|
-
offsetX
|
|
144
|
+
offsetY,
|
|
145
|
+
offsetX,
|
|
40
146
|
contrastScheme,
|
|
41
147
|
ctx: ctx2,
|
|
42
148
|
blockSizeXOverride: width - treeAreaWidth,
|
|
43
149
|
blockSizeYOverride: height,
|
|
44
150
|
highResScaleFactorOverride: 1,
|
|
45
151
|
})
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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>
|
|
75
|
-
</svg>,
|
|
76
|
-
createRoot,
|
|
152
|
+
return (
|
|
153
|
+
<>
|
|
154
|
+
<defs>
|
|
155
|
+
<clipPath id={clipId}>
|
|
156
|
+
<rect x={0} y={0} width={treeAreaWidth} height={height} />
|
|
157
|
+
</clipPath>
|
|
158
|
+
</defs>
|
|
159
|
+
<g
|
|
160
|
+
clipPath={`url(#${clipId})`}
|
|
161
|
+
/* eslint-disable-next-line react/no-danger */
|
|
162
|
+
dangerouslySetInnerHTML={{ __html: ctx1.getSvg().innerHTML }}
|
|
163
|
+
/>
|
|
164
|
+
<g
|
|
165
|
+
transform={`translate(${treeAreaWidth} 0)`}
|
|
166
|
+
/* eslint-disable-next-line react/no-danger */
|
|
167
|
+
dangerouslySetInnerHTML={{ __html: ctx2.getSvg().innerHTML }}
|
|
168
|
+
/>
|
|
169
|
+
</>
|
|
77
170
|
)
|
|
78
171
|
}
|
|
79
172
|
|
|
@@ -97,6 +190,28 @@ function MinimapWrapper({
|
|
|
97
190
|
)
|
|
98
191
|
}
|
|
99
192
|
|
|
193
|
+
function SvgWrapper({
|
|
194
|
+
width,
|
|
195
|
+
height,
|
|
196
|
+
children,
|
|
197
|
+
}: {
|
|
198
|
+
width: number
|
|
199
|
+
height: number
|
|
200
|
+
children: React.ReactNode
|
|
201
|
+
}) {
|
|
202
|
+
return (
|
|
203
|
+
<svg
|
|
204
|
+
width={width}
|
|
205
|
+
height={height}
|
|
206
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
207
|
+
xmlnsXlink="http://www.w3.org/1999/xlink"
|
|
208
|
+
viewBox={[0, 0, width, height].toString()}
|
|
209
|
+
>
|
|
210
|
+
{children}
|
|
211
|
+
</svg>
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
100
215
|
function NullWrapper({ children }: { children: React.ReactNode }) {
|
|
101
216
|
return <>{children}</>
|
|
102
217
|
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '3.1.
|
|
1
|
+
export const version = '3.1.3'
|