@jbrowse/plugin-linear-genome-view 1.4.1 → 1.5.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/dist/BaseLinearDisplay/components/Block.d.ts +7 -10
- package/dist/BaseLinearDisplay/models/BaseLinearDisplayModel.d.ts +16 -9
- package/dist/BaseLinearDisplay/models/serverSideRenderedBlock.d.ts +2 -2
- package/dist/LinearBareDisplay/model.d.ts +8 -8
- package/dist/LinearBasicDisplay/model.d.ts +11 -8
- package/dist/LinearGenomeView/components/HelpDialog.d.ts +5 -0
- package/dist/LinearGenomeView/components/LinearGenomeView.d.ts +3 -5
- package/dist/LinearGenomeView/components/LinearGenomeViewSvg.d.ts +4 -0
- package/dist/LinearGenomeView/components/OverviewRubberBand.d.ts +2 -3
- package/dist/LinearGenomeView/components/OverviewScaleBar.d.ts +116 -2
- package/dist/LinearGenomeView/components/RefNameAutocomplete.d.ts +3 -11
- package/dist/LinearGenomeView/components/ScaleBar.d.ts +36 -2
- package/dist/LinearGenomeView/components/util.d.ts +2 -0
- package/dist/LinearGenomeView/index.d.ts +22 -4
- package/dist/index.d.ts +26 -26
- package/dist/plugin-linear-genome-view.cjs.development.js +3178 -2884
- package/dist/plugin-linear-genome-view.cjs.development.js.map +1 -1
- package/dist/plugin-linear-genome-view.cjs.production.min.js +1 -1
- package/dist/plugin-linear-genome-view.cjs.production.min.js.map +1 -1
- package/dist/plugin-linear-genome-view.esm.js +3191 -2898
- package/dist/plugin-linear-genome-view.esm.js.map +1 -1
- package/package.json +2 -2
- package/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx +3 -0
- package/src/BaseLinearDisplay/components/Block.tsx +20 -33
- package/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx +3 -7
- package/src/BaseLinearDisplay/models/serverSideRenderedBlock.ts +15 -13
- package/src/LinearBasicDisplay/model.ts +25 -3
- package/src/LinearGenomeView/components/ExportSvgDialog.tsx +6 -6
- package/src/LinearGenomeView/components/Header.tsx +56 -78
- package/src/LinearGenomeView/components/HelpDialog.tsx +81 -0
- package/src/LinearGenomeView/components/ImportForm.tsx +139 -158
- package/src/LinearGenomeView/components/LinearGenomeView.test.js +6 -6
- package/src/LinearGenomeView/components/LinearGenomeView.tsx +30 -245
- package/src/LinearGenomeView/components/LinearGenomeViewSvg.tsx +317 -0
- package/src/LinearGenomeView/components/OverviewRubberBand.tsx +74 -34
- package/src/LinearGenomeView/components/OverviewScaleBar.tsx +326 -177
- package/src/LinearGenomeView/components/RefNameAutocomplete.tsx +152 -157
- package/src/LinearGenomeView/components/SearchResultsDialog.tsx +12 -34
- package/src/LinearGenomeView/components/SequenceDialog.tsx +10 -9
- package/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.js.snap +127 -254
- package/src/LinearGenomeView/components/util.ts +10 -0
- package/src/LinearGenomeView/index.tsx +69 -27
- package/src/index.ts +3 -1
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
-
|
|
4
|
-
// material ui things
|
|
5
2
|
import { Button, Paper, Typography, makeStyles } from '@material-ui/core'
|
|
6
3
|
import { TrackSelector as TrackSelectorIcon } from '@jbrowse/core/ui/Icons'
|
|
7
|
-
|
|
8
|
-
// misc
|
|
9
|
-
import { when } from 'mobx'
|
|
10
4
|
import { observer } from 'mobx-react'
|
|
11
|
-
import { getParent, Instance } from 'mobx-state-tree'
|
|
12
|
-
import { getConf, readConfObject } from '@jbrowse/core/configuration'
|
|
13
|
-
import { AnyConfigurationModel } from '@jbrowse/core/configuration/configurationSchema'
|
|
14
5
|
|
|
15
6
|
// locals
|
|
16
|
-
import {
|
|
7
|
+
import { LinearGenomeViewModel } from '..'
|
|
17
8
|
import Header from './Header'
|
|
18
9
|
import TrackContainer from './TrackContainer'
|
|
19
10
|
import TracksContainer from './TracksContainer'
|
|
@@ -21,18 +12,34 @@ import ImportForm from './ImportForm'
|
|
|
21
12
|
import MiniControls from './MiniControls'
|
|
22
13
|
import SequenceDialog from './SequenceDialog'
|
|
23
14
|
import SearchResultsDialog from './SearchResultsDialog'
|
|
24
|
-
import Ruler from './Ruler'
|
|
25
15
|
|
|
26
|
-
type LGV =
|
|
16
|
+
type LGV = LinearGenomeViewModel
|
|
27
17
|
|
|
28
18
|
const useStyles = makeStyles(theme => ({
|
|
29
|
-
|
|
19
|
+
note: {
|
|
30
20
|
textAlign: 'center',
|
|
31
21
|
paddingTop: theme.spacing(1),
|
|
32
22
|
paddingBottom: theme.spacing(1),
|
|
33
23
|
},
|
|
34
|
-
|
|
35
|
-
|
|
24
|
+
dots: {
|
|
25
|
+
'&::after': {
|
|
26
|
+
display: 'inline-block',
|
|
27
|
+
animation: '$ellipsis 1.5s infinite',
|
|
28
|
+
content: '"."',
|
|
29
|
+
width: '1em',
|
|
30
|
+
textAlign: 'left',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
'@keyframes ellipsis': {
|
|
34
|
+
'0%': {
|
|
35
|
+
content: '"."',
|
|
36
|
+
},
|
|
37
|
+
'33%': {
|
|
38
|
+
content: '".."',
|
|
39
|
+
},
|
|
40
|
+
'66%': {
|
|
41
|
+
content: '"..."',
|
|
42
|
+
},
|
|
36
43
|
},
|
|
37
44
|
}))
|
|
38
45
|
|
|
@@ -41,11 +48,16 @@ const LinearGenomeView = observer(({ model }: { model: LGV }) => {
|
|
|
41
48
|
const classes = useStyles()
|
|
42
49
|
|
|
43
50
|
if (!initialized && !error) {
|
|
44
|
-
return
|
|
51
|
+
return (
|
|
52
|
+
<Typography className={classes.dots} variant="h5">
|
|
53
|
+
Loading
|
|
54
|
+
</Typography>
|
|
55
|
+
)
|
|
45
56
|
}
|
|
46
57
|
if (!hasDisplayedRegions || error) {
|
|
47
58
|
return <ImportForm model={model} />
|
|
48
59
|
}
|
|
60
|
+
|
|
49
61
|
return (
|
|
50
62
|
<div style={{ position: 'relative' }}>
|
|
51
63
|
{model.seqDialogDisplayed ? (
|
|
@@ -79,15 +91,15 @@ const LinearGenomeView = observer(({ model }: { model: LGV }) => {
|
|
|
79
91
|
)}
|
|
80
92
|
<TracksContainer model={model}>
|
|
81
93
|
{!tracks.length ? (
|
|
82
|
-
<Paper variant="outlined" className={classes.
|
|
94
|
+
<Paper variant="outlined" className={classes.note}>
|
|
83
95
|
<Typography>No tracks active.</Typography>
|
|
84
96
|
<Button
|
|
85
97
|
variant="contained"
|
|
86
98
|
color="primary"
|
|
87
99
|
onClick={model.activateTrackSelector}
|
|
88
100
|
style={{ zIndex: 1000 }}
|
|
101
|
+
startIcon={<TrackSelectorIcon />}
|
|
89
102
|
>
|
|
90
|
-
<TrackSelectorIcon className={classes.spacer} />
|
|
91
103
|
Open track selector
|
|
92
104
|
</Button>
|
|
93
105
|
</Paper>
|
|
@@ -102,230 +114,3 @@ const LinearGenomeView = observer(({ model }: { model: LGV }) => {
|
|
|
102
114
|
})
|
|
103
115
|
|
|
104
116
|
export default LinearGenomeView
|
|
105
|
-
|
|
106
|
-
function ScaleBar({ model, fontSize }: { model: LGV; fontSize: number }) {
|
|
107
|
-
const {
|
|
108
|
-
offsetPx,
|
|
109
|
-
dynamicBlocks: { totalWidthPxWithoutBorders: totalWidthPx, totalBp },
|
|
110
|
-
} = model
|
|
111
|
-
let displayBp
|
|
112
|
-
if (Math.floor(totalBp / 1000000) > 0) {
|
|
113
|
-
displayBp = `${parseFloat((totalBp / 1000000).toPrecision(3))}Mbp`
|
|
114
|
-
} else if (Math.floor(totalBp / 1000) > 0) {
|
|
115
|
-
displayBp = `${parseFloat((totalBp / 1000).toPrecision(3))}Kbp`
|
|
116
|
-
} else {
|
|
117
|
-
displayBp = `${Math.floor(totalBp)}bp`
|
|
118
|
-
}
|
|
119
|
-
const x0 = Math.max(-offsetPx, 0)
|
|
120
|
-
const x1 = x0 + totalWidthPx
|
|
121
|
-
return (
|
|
122
|
-
<>
|
|
123
|
-
<line x1={x0} x2={x1} y1={10} y2={10} stroke="black" />
|
|
124
|
-
<line x1={x0} x2={x0} y1={5} y2={15} stroke="black" />
|
|
125
|
-
<line x1={x1} x2={x1} y1={5} y2={15} stroke="black" />
|
|
126
|
-
<text
|
|
127
|
-
x={x0 + (x1 - x0) / 2}
|
|
128
|
-
y={fontSize * 2}
|
|
129
|
-
textAnchor="middle"
|
|
130
|
-
fontSize={fontSize}
|
|
131
|
-
>
|
|
132
|
-
{displayBp}
|
|
133
|
-
</text>
|
|
134
|
-
</>
|
|
135
|
-
)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function SVGRuler({
|
|
139
|
-
model,
|
|
140
|
-
fontSize,
|
|
141
|
-
width,
|
|
142
|
-
}: {
|
|
143
|
-
model: LGV
|
|
144
|
-
fontSize: number
|
|
145
|
-
width: number
|
|
146
|
-
}) {
|
|
147
|
-
const {
|
|
148
|
-
dynamicBlocks: { contentBlocks },
|
|
149
|
-
offsetPx: viewOffsetPx,
|
|
150
|
-
bpPerPx,
|
|
151
|
-
} = model
|
|
152
|
-
const renderRuler = contentBlocks.length < 5
|
|
153
|
-
return (
|
|
154
|
-
<>
|
|
155
|
-
<defs>
|
|
156
|
-
<clipPath id="clip-ruler">
|
|
157
|
-
<rect x={0} y={0} width={width} height={20} />
|
|
158
|
-
</clipPath>
|
|
159
|
-
</defs>
|
|
160
|
-
{contentBlocks.map(block => {
|
|
161
|
-
const offsetLeft = block.offsetPx - viewOffsetPx
|
|
162
|
-
return (
|
|
163
|
-
<g key={`${block.key}`} transform={`translate(${offsetLeft} 0)`}>
|
|
164
|
-
<text x={offsetLeft / bpPerPx} y={fontSize} fontSize={fontSize}>
|
|
165
|
-
{block.refName}
|
|
166
|
-
</text>
|
|
167
|
-
{renderRuler ? (
|
|
168
|
-
<g transform="translate(0 20)" clipPath="url(#clip-ruler)">
|
|
169
|
-
<Ruler
|
|
170
|
-
start={block.start}
|
|
171
|
-
end={block.end}
|
|
172
|
-
bpPerPx={bpPerPx}
|
|
173
|
-
reversed={block.reversed}
|
|
174
|
-
/>
|
|
175
|
-
</g>
|
|
176
|
-
) : (
|
|
177
|
-
<line
|
|
178
|
-
strokeWidth={1}
|
|
179
|
-
stroke="black"
|
|
180
|
-
x1={block.start / bpPerPx}
|
|
181
|
-
x2={block.end / bpPerPx}
|
|
182
|
-
y1={20}
|
|
183
|
-
y2={20}
|
|
184
|
-
/>
|
|
185
|
-
)}
|
|
186
|
-
</g>
|
|
187
|
-
)
|
|
188
|
-
})}
|
|
189
|
-
</>
|
|
190
|
-
)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const fontSize = 15
|
|
194
|
-
const rulerHeight = 50
|
|
195
|
-
const textHeight = fontSize + 5
|
|
196
|
-
const paddingHeight = 20
|
|
197
|
-
const headerHeight = textHeight + 20
|
|
198
|
-
|
|
199
|
-
const totalHeight = (tracks: { displays: { height: number }[] }[]) => {
|
|
200
|
-
return tracks.reduce((accum, track) => {
|
|
201
|
-
const display = track.displays[0]
|
|
202
|
-
return accum + display.height + paddingHeight + textHeight
|
|
203
|
-
}, 0)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// SVG component, ruler and assembly name
|
|
207
|
-
const SVGHeader = ({ model }: { model: LGV }) => {
|
|
208
|
-
const { width, assemblyNames } = model
|
|
209
|
-
const assemblyName = assemblyNames.length > 1 ? '' : assemblyNames[0]
|
|
210
|
-
return (
|
|
211
|
-
<g id="header">
|
|
212
|
-
<text x={0} y={fontSize} fontSize={fontSize}>
|
|
213
|
-
{assemblyName}
|
|
214
|
-
</text>
|
|
215
|
-
<g transform={`translate(0 ${fontSize})`}>
|
|
216
|
-
<ScaleBar model={model} fontSize={fontSize} />
|
|
217
|
-
</g>
|
|
218
|
-
<g transform={`translate(0 ${rulerHeight})`}>
|
|
219
|
-
<SVGRuler model={model} fontSize={fontSize} width={width} />
|
|
220
|
-
</g>
|
|
221
|
-
</g>
|
|
222
|
-
)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// SVG component, region separator
|
|
226
|
-
const SVGRegionSeparators = ({ model }: { model: LGV }) => {
|
|
227
|
-
const { dynamicBlocks, tracks } = model
|
|
228
|
-
const initialOffset = headerHeight + rulerHeight + 20
|
|
229
|
-
const height = totalHeight(tracks)
|
|
230
|
-
|
|
231
|
-
return (
|
|
232
|
-
<>
|
|
233
|
-
{dynamicBlocks.contentBlocks.slice(1).map(block => (
|
|
234
|
-
<line
|
|
235
|
-
key={block.key}
|
|
236
|
-
x1={block.offsetPx - model.offsetPx}
|
|
237
|
-
x2={block.offsetPx - model.offsetPx}
|
|
238
|
-
y1={initialOffset}
|
|
239
|
-
y2={height}
|
|
240
|
-
stroke="black"
|
|
241
|
-
strokeOpacity={0.3}
|
|
242
|
-
/>
|
|
243
|
-
))}
|
|
244
|
-
</>
|
|
245
|
-
)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// SVG component, tracks
|
|
249
|
-
function SVGTracks({
|
|
250
|
-
displayResults,
|
|
251
|
-
model,
|
|
252
|
-
offset,
|
|
253
|
-
}: {
|
|
254
|
-
displayResults: {
|
|
255
|
-
track: {
|
|
256
|
-
configuration: AnyConfigurationModel
|
|
257
|
-
displays: { height: number }[]
|
|
258
|
-
}
|
|
259
|
-
result: string
|
|
260
|
-
}[]
|
|
261
|
-
model: LGV
|
|
262
|
-
offset: number
|
|
263
|
-
}) {
|
|
264
|
-
return (
|
|
265
|
-
<>
|
|
266
|
-
{displayResults.map(({ track, result }) => {
|
|
267
|
-
const current = offset
|
|
268
|
-
const trackName =
|
|
269
|
-
getConf(track, 'name') ||
|
|
270
|
-
`Reference sequence (${readConfObject(
|
|
271
|
-
getParent(track.configuration),
|
|
272
|
-
'name',
|
|
273
|
-
)})`
|
|
274
|
-
const display = track.displays[0]
|
|
275
|
-
offset += display.height + paddingHeight + textHeight
|
|
276
|
-
return (
|
|
277
|
-
<g
|
|
278
|
-
key={track.configuration.trackId}
|
|
279
|
-
transform={`translate(0 ${current})`}
|
|
280
|
-
>
|
|
281
|
-
<text fontSize={fontSize} x={Math.max(-model.offsetPx, 0)}>
|
|
282
|
-
{trackName}
|
|
283
|
-
</text>
|
|
284
|
-
<g transform={`translate(0 ${textHeight})`}>{result}</g>
|
|
285
|
-
</g>
|
|
286
|
-
)
|
|
287
|
-
})}
|
|
288
|
-
</>
|
|
289
|
-
)
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// render LGV to SVG
|
|
293
|
-
export async function renderToSvg(model: LGV, opts: ExportSvgOptions) {
|
|
294
|
-
await when(() => model.initialized)
|
|
295
|
-
const { width, tracks } = model
|
|
296
|
-
const shift = 50
|
|
297
|
-
const offset = headerHeight + rulerHeight + 20
|
|
298
|
-
const height = totalHeight(tracks) + offset
|
|
299
|
-
const displayResults = await Promise.all(
|
|
300
|
-
tracks.map(async track => {
|
|
301
|
-
const display = track.displays[0]
|
|
302
|
-
await when(() => (display.ready !== undefined ? display.ready : true))
|
|
303
|
-
const result = await display.renderSvg(opts)
|
|
304
|
-
return { track, result }
|
|
305
|
-
}),
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
// the xlink namespace is used for rendering <image> tag
|
|
309
|
-
return renderToStaticMarkup(
|
|
310
|
-
<svg
|
|
311
|
-
width={width}
|
|
312
|
-
height={height}
|
|
313
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
314
|
-
xmlnsXlink="http://www.w3.org/1999/xlink"
|
|
315
|
-
viewBox={[0, 0, width + shift * 2, height].toString()}
|
|
316
|
-
>
|
|
317
|
-
{/* background white */}
|
|
318
|
-
<rect width={width + shift * 2} height={height} fill="white" />
|
|
319
|
-
|
|
320
|
-
<g stroke="none" transform={`translate(${shift} ${fontSize})`}>
|
|
321
|
-
<SVGHeader model={model} />
|
|
322
|
-
<SVGTracks
|
|
323
|
-
model={model}
|
|
324
|
-
displayResults={displayResults}
|
|
325
|
-
offset={offset}
|
|
326
|
-
/>
|
|
327
|
-
<SVGRegionSeparators model={model} />
|
|
328
|
-
</g>
|
|
329
|
-
</svg>,
|
|
330
|
-
)
|
|
331
|
-
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
import { when } from 'mobx'
|
|
4
|
+
import { getParent } from 'mobx-state-tree'
|
|
5
|
+
import { getConf, readConfObject } from '@jbrowse/core/configuration'
|
|
6
|
+
import { getSession } from '@jbrowse/core/util'
|
|
7
|
+
import { AnyConfigurationModel } from '@jbrowse/core/configuration/configurationSchema'
|
|
8
|
+
import Base1DView from '@jbrowse/core/util/Base1DViewModel'
|
|
9
|
+
|
|
10
|
+
// locals
|
|
11
|
+
import Ruler from './Ruler'
|
|
12
|
+
import {
|
|
13
|
+
LinearGenomeViewModel,
|
|
14
|
+
ExportSvgOptions,
|
|
15
|
+
HEADER_OVERVIEW_HEIGHT,
|
|
16
|
+
} from '..'
|
|
17
|
+
import { Polygon, Cytobands } from './OverviewScaleBar'
|
|
18
|
+
|
|
19
|
+
type LGV = LinearGenomeViewModel
|
|
20
|
+
|
|
21
|
+
function getBpDisplayStr(totalBp: number) {
|
|
22
|
+
let displayBp
|
|
23
|
+
if (Math.floor(totalBp / 1000000) > 0) {
|
|
24
|
+
displayBp = `${parseFloat((totalBp / 1000000).toPrecision(3))}Mbp`
|
|
25
|
+
} else if (Math.floor(totalBp / 1000) > 0) {
|
|
26
|
+
displayBp = `${parseFloat((totalBp / 1000).toPrecision(3))}Kbp`
|
|
27
|
+
} else {
|
|
28
|
+
displayBp = `${Math.floor(totalBp)}bp`
|
|
29
|
+
}
|
|
30
|
+
return displayBp
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ScaleBar({ model, fontSize }: { model: LGV; fontSize: number }) {
|
|
34
|
+
const {
|
|
35
|
+
offsetPx,
|
|
36
|
+
dynamicBlocks: { totalWidthPxWithoutBorders: totalWidthPx, totalBp },
|
|
37
|
+
} = model
|
|
38
|
+
const displayBp = getBpDisplayStr(totalBp)
|
|
39
|
+
const x0 = Math.max(-offsetPx, 0)
|
|
40
|
+
const x1 = x0 + totalWidthPx
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
<line x1={x0} x2={x1} y1={10} y2={10} stroke="black" />
|
|
44
|
+
<line x1={x0} x2={x0} y1={5} y2={15} stroke="black" />
|
|
45
|
+
<line x1={x1} x2={x1} y1={5} y2={15} stroke="black" />
|
|
46
|
+
<text
|
|
47
|
+
x={x0 + (x1 - x0) / 2}
|
|
48
|
+
y={fontSize * 2}
|
|
49
|
+
textAnchor="middle"
|
|
50
|
+
fontSize={fontSize}
|
|
51
|
+
>
|
|
52
|
+
{displayBp}
|
|
53
|
+
</text>
|
|
54
|
+
</>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function SVGRuler({
|
|
59
|
+
model,
|
|
60
|
+
fontSize,
|
|
61
|
+
width,
|
|
62
|
+
}: {
|
|
63
|
+
model: LGV
|
|
64
|
+
fontSize: number
|
|
65
|
+
width: number
|
|
66
|
+
}) {
|
|
67
|
+
const {
|
|
68
|
+
dynamicBlocks: { contentBlocks },
|
|
69
|
+
offsetPx: viewOffsetPx,
|
|
70
|
+
bpPerPx,
|
|
71
|
+
} = model
|
|
72
|
+
const renderRuler = contentBlocks.length < 5
|
|
73
|
+
return (
|
|
74
|
+
<>
|
|
75
|
+
<defs>
|
|
76
|
+
<clipPath id="clip-ruler">
|
|
77
|
+
<rect x={0} y={0} width={width} height={20} />
|
|
78
|
+
</clipPath>
|
|
79
|
+
</defs>
|
|
80
|
+
{contentBlocks.map(block => {
|
|
81
|
+
const { key, start, end, reversed, offsetPx, refName } = block
|
|
82
|
+
const offsetLeft = offsetPx - viewOffsetPx
|
|
83
|
+
return (
|
|
84
|
+
<g key={`${key}`} transform={`translate(${offsetLeft} 0)`}>
|
|
85
|
+
<text x={offsetLeft / bpPerPx} y={fontSize} fontSize={fontSize}>
|
|
86
|
+
{refName}
|
|
87
|
+
</text>
|
|
88
|
+
{renderRuler ? (
|
|
89
|
+
<g transform="translate(0 20)" clipPath="url(#clip-ruler)">
|
|
90
|
+
<Ruler
|
|
91
|
+
start={start}
|
|
92
|
+
end={end}
|
|
93
|
+
bpPerPx={bpPerPx}
|
|
94
|
+
reversed={reversed}
|
|
95
|
+
/>
|
|
96
|
+
</g>
|
|
97
|
+
) : (
|
|
98
|
+
<line
|
|
99
|
+
strokeWidth={1}
|
|
100
|
+
stroke="black"
|
|
101
|
+
x1={start / bpPerPx}
|
|
102
|
+
x2={end / bpPerPx}
|
|
103
|
+
y1={20}
|
|
104
|
+
y2={20}
|
|
105
|
+
/>
|
|
106
|
+
)}
|
|
107
|
+
</g>
|
|
108
|
+
)
|
|
109
|
+
})}
|
|
110
|
+
</>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const fontSize = 15
|
|
115
|
+
const rulerHeight = 50
|
|
116
|
+
const textHeight = fontSize + 5
|
|
117
|
+
const paddingHeight = 20
|
|
118
|
+
const headerHeight = textHeight + 20
|
|
119
|
+
const cytobandHeightIfExists = 100
|
|
120
|
+
|
|
121
|
+
interface Display {
|
|
122
|
+
height: number
|
|
123
|
+
}
|
|
124
|
+
interface Track {
|
|
125
|
+
displays: Display[]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const totalHeight = (tracks: Track[]) => {
|
|
129
|
+
return tracks.reduce((accum, track) => {
|
|
130
|
+
const display = track.displays[0]
|
|
131
|
+
return accum + display.height + paddingHeight + textHeight
|
|
132
|
+
}, 0)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// SVG component, ruler and assembly name
|
|
136
|
+
const SVGHeader = ({ model }: { model: LGV }) => {
|
|
137
|
+
const { width, assemblyNames, showCytobands, displayedRegions } = model
|
|
138
|
+
const { assemblyManager } = getSession(model)
|
|
139
|
+
const assemblyName = assemblyNames.length > 1 ? '' : assemblyNames[0]
|
|
140
|
+
const assembly = assemblyManager.get(assemblyName)
|
|
141
|
+
|
|
142
|
+
const overview = Base1DView.create({
|
|
143
|
+
displayedRegions: JSON.parse(JSON.stringify(displayedRegions)),
|
|
144
|
+
interRegionPaddingWidth: 0,
|
|
145
|
+
minimumBlockWidth: model.minimumBlockWidth,
|
|
146
|
+
})
|
|
147
|
+
const visibleRegions = model.dynamicBlocks.contentBlocks
|
|
148
|
+
|
|
149
|
+
overview.setVolatileWidth(width)
|
|
150
|
+
overview.showAllRegions()
|
|
151
|
+
const block = overview.dynamicBlocks.contentBlocks[0]
|
|
152
|
+
|
|
153
|
+
const first = visibleRegions[0]
|
|
154
|
+
const firstOverviewPx =
|
|
155
|
+
overview.bpToPx({
|
|
156
|
+
...first,
|
|
157
|
+
coord: first.reversed ? first.end : first.start,
|
|
158
|
+
}) || 0
|
|
159
|
+
|
|
160
|
+
const last = visibleRegions[visibleRegions.length - 1]
|
|
161
|
+
const lastOverviewPx =
|
|
162
|
+
overview.bpToPx({
|
|
163
|
+
...last,
|
|
164
|
+
coord: last.reversed ? last.start : last.end,
|
|
165
|
+
}) || 0
|
|
166
|
+
|
|
167
|
+
const cytobandHeight = showCytobands ? cytobandHeightIfExists : 0
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<g id="header">
|
|
171
|
+
<text x={0} y={fontSize} fontSize={fontSize}>
|
|
172
|
+
{assemblyName}
|
|
173
|
+
</text>
|
|
174
|
+
|
|
175
|
+
{showCytobands ? (
|
|
176
|
+
<g transform={`translate(0 ${rulerHeight})`}>
|
|
177
|
+
<Cytobands overview={overview} assembly={assembly} block={block} />
|
|
178
|
+
<rect
|
|
179
|
+
stroke="red"
|
|
180
|
+
fill="rgb(255,0,0,0.1)"
|
|
181
|
+
width={Math.max(lastOverviewPx - firstOverviewPx, 0.5)}
|
|
182
|
+
height={HEADER_OVERVIEW_HEIGHT - 1}
|
|
183
|
+
x={firstOverviewPx}
|
|
184
|
+
y={0.5}
|
|
185
|
+
/>
|
|
186
|
+
<g transform={`translate(0,${HEADER_OVERVIEW_HEIGHT})`}>
|
|
187
|
+
<Polygon overview={overview} model={model} useOffset={false} />
|
|
188
|
+
</g>
|
|
189
|
+
</g>
|
|
190
|
+
) : null}
|
|
191
|
+
|
|
192
|
+
<g transform={`translate(0 ${fontSize + cytobandHeight})`}>
|
|
193
|
+
<ScaleBar model={model} fontSize={fontSize} />
|
|
194
|
+
</g>
|
|
195
|
+
<g transform={`translate(0 ${rulerHeight + cytobandHeight})`}>
|
|
196
|
+
<SVGRuler model={model} fontSize={fontSize} width={width} />
|
|
197
|
+
</g>
|
|
198
|
+
</g>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// SVG component, region separator
|
|
203
|
+
const SVGRegionSeparators = ({
|
|
204
|
+
model,
|
|
205
|
+
height,
|
|
206
|
+
}: {
|
|
207
|
+
height: number
|
|
208
|
+
model: LGV
|
|
209
|
+
}) => {
|
|
210
|
+
const { dynamicBlocks, offsetPx, interRegionPaddingWidth } = model
|
|
211
|
+
return (
|
|
212
|
+
<>
|
|
213
|
+
{dynamicBlocks.contentBlocks.slice(1).map(block => (
|
|
214
|
+
<rect
|
|
215
|
+
key={block.key}
|
|
216
|
+
x={block.offsetPx - offsetPx - interRegionPaddingWidth}
|
|
217
|
+
width={interRegionPaddingWidth}
|
|
218
|
+
y={0}
|
|
219
|
+
height={height}
|
|
220
|
+
stroke="none"
|
|
221
|
+
fill="grey"
|
|
222
|
+
/>
|
|
223
|
+
))}
|
|
224
|
+
</>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// SVG component, tracks
|
|
229
|
+
function SVGTracks({
|
|
230
|
+
displayResults,
|
|
231
|
+
model,
|
|
232
|
+
offset,
|
|
233
|
+
}: {
|
|
234
|
+
displayResults: {
|
|
235
|
+
track: {
|
|
236
|
+
configuration: AnyConfigurationModel
|
|
237
|
+
displays: { height: number }[]
|
|
238
|
+
}
|
|
239
|
+
result: string
|
|
240
|
+
}[]
|
|
241
|
+
model: LGV
|
|
242
|
+
offset: number
|
|
243
|
+
}) {
|
|
244
|
+
return (
|
|
245
|
+
<>
|
|
246
|
+
{displayResults.map(({ track, result }) => {
|
|
247
|
+
const current = offset
|
|
248
|
+
const trackName =
|
|
249
|
+
getConf(track, 'name') ||
|
|
250
|
+
`Reference sequence (${readConfObject(
|
|
251
|
+
getParent(track.configuration),
|
|
252
|
+
'name',
|
|
253
|
+
)})`
|
|
254
|
+
const display = track.displays[0]
|
|
255
|
+
offset += display.height + paddingHeight + textHeight
|
|
256
|
+
return (
|
|
257
|
+
<g
|
|
258
|
+
key={track.configuration.trackId}
|
|
259
|
+
transform={`translate(0 ${current})`}
|
|
260
|
+
>
|
|
261
|
+
<text fontSize={fontSize} x={Math.max(-model.offsetPx, 0)}>
|
|
262
|
+
{trackName}
|
|
263
|
+
</text>
|
|
264
|
+
<g transform={`translate(0 ${textHeight})`}>
|
|
265
|
+
{result}
|
|
266
|
+
<SVGRegionSeparators model={model} height={display.height} />
|
|
267
|
+
</g>
|
|
268
|
+
</g>
|
|
269
|
+
)
|
|
270
|
+
})}
|
|
271
|
+
</>
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// render LGV to SVG
|
|
276
|
+
export async function renderToSvg(model: LGV, opts: ExportSvgOptions) {
|
|
277
|
+
await when(() => model.initialized)
|
|
278
|
+
const { width, tracks, showCytobands } = model
|
|
279
|
+
const shift = 50
|
|
280
|
+
const offset =
|
|
281
|
+
headerHeight +
|
|
282
|
+
rulerHeight +
|
|
283
|
+
(showCytobands ? cytobandHeightIfExists : 0) +
|
|
284
|
+
20
|
|
285
|
+
const height = totalHeight(tracks) + offset
|
|
286
|
+
const displayResults = await Promise.all(
|
|
287
|
+
tracks.map(async track => {
|
|
288
|
+
const display = track.displays[0]
|
|
289
|
+
await when(() => (display.ready !== undefined ? display.ready : true))
|
|
290
|
+
const result = await display.renderSvg(opts)
|
|
291
|
+
return { track, result }
|
|
292
|
+
}),
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
// the xlink namespace is used for rendering <image> tag
|
|
296
|
+
return renderToStaticMarkup(
|
|
297
|
+
<svg
|
|
298
|
+
width={width}
|
|
299
|
+
height={height}
|
|
300
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
301
|
+
xmlnsXlink="http://www.w3.org/1999/xlink"
|
|
302
|
+
viewBox={[0, 0, width + shift * 2, height].toString()}
|
|
303
|
+
>
|
|
304
|
+
{/* background white */}
|
|
305
|
+
<rect width={width + shift * 2} height={height} fill="white" />
|
|
306
|
+
|
|
307
|
+
<g stroke="none" transform={`translate(${shift} ${fontSize})`}>
|
|
308
|
+
<SVGHeader model={model} />
|
|
309
|
+
<SVGTracks
|
|
310
|
+
model={model}
|
|
311
|
+
displayResults={displayResults}
|
|
312
|
+
offset={offset}
|
|
313
|
+
/>
|
|
314
|
+
</g>
|
|
315
|
+
</svg>,
|
|
316
|
+
)
|
|
317
|
+
}
|