@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.
Files changed (43) hide show
  1. package/dist/BaseLinearDisplay/components/Block.d.ts +7 -10
  2. package/dist/BaseLinearDisplay/models/BaseLinearDisplayModel.d.ts +16 -9
  3. package/dist/BaseLinearDisplay/models/serverSideRenderedBlock.d.ts +2 -2
  4. package/dist/LinearBareDisplay/model.d.ts +8 -8
  5. package/dist/LinearBasicDisplay/model.d.ts +11 -8
  6. package/dist/LinearGenomeView/components/HelpDialog.d.ts +5 -0
  7. package/dist/LinearGenomeView/components/LinearGenomeView.d.ts +3 -5
  8. package/dist/LinearGenomeView/components/LinearGenomeViewSvg.d.ts +4 -0
  9. package/dist/LinearGenomeView/components/OverviewRubberBand.d.ts +2 -3
  10. package/dist/LinearGenomeView/components/OverviewScaleBar.d.ts +116 -2
  11. package/dist/LinearGenomeView/components/RefNameAutocomplete.d.ts +3 -11
  12. package/dist/LinearGenomeView/components/ScaleBar.d.ts +36 -2
  13. package/dist/LinearGenomeView/components/util.d.ts +2 -0
  14. package/dist/LinearGenomeView/index.d.ts +22 -4
  15. package/dist/index.d.ts +26 -26
  16. package/dist/plugin-linear-genome-view.cjs.development.js +3178 -2884
  17. package/dist/plugin-linear-genome-view.cjs.development.js.map +1 -1
  18. package/dist/plugin-linear-genome-view.cjs.production.min.js +1 -1
  19. package/dist/plugin-linear-genome-view.cjs.production.min.js.map +1 -1
  20. package/dist/plugin-linear-genome-view.esm.js +3191 -2898
  21. package/dist/plugin-linear-genome-view.esm.js.map +1 -1
  22. package/package.json +2 -2
  23. package/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx +3 -0
  24. package/src/BaseLinearDisplay/components/Block.tsx +20 -33
  25. package/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx +3 -7
  26. package/src/BaseLinearDisplay/models/serverSideRenderedBlock.ts +15 -13
  27. package/src/LinearBasicDisplay/model.ts +25 -3
  28. package/src/LinearGenomeView/components/ExportSvgDialog.tsx +6 -6
  29. package/src/LinearGenomeView/components/Header.tsx +56 -78
  30. package/src/LinearGenomeView/components/HelpDialog.tsx +81 -0
  31. package/src/LinearGenomeView/components/ImportForm.tsx +139 -158
  32. package/src/LinearGenomeView/components/LinearGenomeView.test.js +6 -6
  33. package/src/LinearGenomeView/components/LinearGenomeView.tsx +30 -245
  34. package/src/LinearGenomeView/components/LinearGenomeViewSvg.tsx +317 -0
  35. package/src/LinearGenomeView/components/OverviewRubberBand.tsx +74 -34
  36. package/src/LinearGenomeView/components/OverviewScaleBar.tsx +326 -177
  37. package/src/LinearGenomeView/components/RefNameAutocomplete.tsx +152 -157
  38. package/src/LinearGenomeView/components/SearchResultsDialog.tsx +12 -34
  39. package/src/LinearGenomeView/components/SequenceDialog.tsx +10 -9
  40. package/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.js.snap +127 -254
  41. package/src/LinearGenomeView/components/util.ts +10 -0
  42. package/src/LinearGenomeView/index.tsx +69 -27
  43. 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 { LinearGenomeViewStateModel, ExportSvgOptions } from '..'
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 = Instance<LinearGenomeViewStateModel>
16
+ type LGV = LinearGenomeViewModel
27
17
 
28
18
  const useStyles = makeStyles(theme => ({
29
- errorMessage: {
19
+ note: {
30
20
  textAlign: 'center',
31
21
  paddingTop: theme.spacing(1),
32
22
  paddingBottom: theme.spacing(1),
33
23
  },
34
- spacer: {
35
- marginRight: theme.spacing(2),
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 null
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.errorMessage}>
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
+ }