@jbrowse/plugin-circular-view 2.3.4 → 2.4.1

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 (107) hide show
  1. package/dist/BaseChordDisplay/components/Loading.js.map +1 -1
  2. package/dist/BaseChordDisplay/index.d.ts +2 -2
  3. package/dist/BaseChordDisplay/index.js +4 -4
  4. package/dist/BaseChordDisplay/index.js.map +1 -1
  5. package/dist/BaseChordDisplay/models/{baseChordDisplayConfig.js → configSchema.js} +1 -1
  6. package/dist/BaseChordDisplay/models/configSchema.js.map +1 -0
  7. package/dist/BaseChordDisplay/models/{BaseChordDisplayModel.d.ts → model.d.ts} +12 -0
  8. package/dist/BaseChordDisplay/models/{BaseChordDisplayModel.js → model.js} +33 -23
  9. package/dist/BaseChordDisplay/models/model.js.map +1 -0
  10. package/dist/BaseChordDisplay/models/renderReaction.d.ts +1 -1
  11. package/dist/BaseChordDisplay/models/renderReaction.js +4 -5
  12. package/dist/BaseChordDisplay/models/renderReaction.js.map +1 -1
  13. package/dist/CircularView/components/CircularView.js +9 -20
  14. package/dist/CircularView/components/CircularView.js.map +1 -1
  15. package/dist/CircularView/components/Controls.d.ts +2 -2
  16. package/dist/CircularView/components/Controls.js +60 -24
  17. package/dist/CircularView/components/Controls.js.map +1 -1
  18. package/dist/CircularView/components/ExportSvgDialog.d.ts +8 -0
  19. package/dist/CircularView/components/ExportSvgDialog.js +76 -0
  20. package/dist/CircularView/components/ExportSvgDialog.js.map +1 -0
  21. package/dist/CircularView/components/Ruler.d.ts +2 -2
  22. package/dist/CircularView/components/Ruler.js +12 -20
  23. package/dist/CircularView/components/Ruler.js.map +1 -1
  24. package/dist/CircularView/models/CircularView.d.ts +30 -26
  25. package/dist/CircularView/models/CircularView.js +66 -22
  26. package/dist/CircularView/models/CircularView.js.map +1 -1
  27. package/dist/CircularView/models/slices.d.ts +23 -10
  28. package/dist/CircularView/models/slices.js +10 -7
  29. package/dist/CircularView/models/slices.js.map +1 -1
  30. package/dist/CircularView/models/viewportVisibleRegion.js +2 -4
  31. package/dist/CircularView/models/viewportVisibleRegion.js.map +1 -1
  32. package/dist/CircularView/svgcomponents/SVGBackground.d.ts +6 -0
  33. package/dist/CircularView/svgcomponents/SVGBackground.js +13 -0
  34. package/dist/CircularView/svgcomponents/SVGBackground.js.map +1 -0
  35. package/dist/CircularView/svgcomponents/SVGCircularView.d.ts +4 -0
  36. package/dist/CircularView/svgcomponents/SVGCircularView.js +40 -0
  37. package/dist/CircularView/svgcomponents/SVGCircularView.js.map +1 -0
  38. package/dist/LaunchCircularView/index.js +1 -1
  39. package/dist/LaunchCircularView/index.js.map +1 -1
  40. package/dist/index.d.ts +1 -2
  41. package/dist/index.js +1 -0
  42. package/dist/index.js.map +1 -1
  43. package/esm/BaseChordDisplay/components/Loading.js.map +1 -1
  44. package/esm/BaseChordDisplay/index.d.ts +2 -2
  45. package/esm/BaseChordDisplay/index.js +2 -2
  46. package/esm/BaseChordDisplay/index.js.map +1 -1
  47. package/esm/BaseChordDisplay/models/{baseChordDisplayConfig.js → configSchema.js} +1 -1
  48. package/esm/BaseChordDisplay/models/configSchema.js.map +1 -0
  49. package/esm/BaseChordDisplay/models/{BaseChordDisplayModel.d.ts → model.d.ts} +12 -0
  50. package/esm/BaseChordDisplay/models/{BaseChordDisplayModel.js → model.js} +34 -24
  51. package/esm/BaseChordDisplay/models/model.js.map +1 -0
  52. package/esm/BaseChordDisplay/models/renderReaction.d.ts +1 -1
  53. package/esm/BaseChordDisplay/models/renderReaction.js +4 -5
  54. package/esm/BaseChordDisplay/models/renderReaction.js.map +1 -1
  55. package/esm/CircularView/components/CircularView.js +9 -20
  56. package/esm/CircularView/components/CircularView.js.map +1 -1
  57. package/esm/CircularView/components/Controls.d.ts +2 -2
  58. package/esm/CircularView/components/Controls.js +37 -24
  59. package/esm/CircularView/components/Controls.js.map +1 -1
  60. package/esm/CircularView/components/ExportSvgDialog.d.ts +8 -0
  61. package/esm/CircularView/components/ExportSvgDialog.js +50 -0
  62. package/esm/CircularView/components/ExportSvgDialog.js.map +1 -0
  63. package/esm/CircularView/components/Ruler.d.ts +2 -2
  64. package/esm/CircularView/components/Ruler.js +12 -20
  65. package/esm/CircularView/components/Ruler.js.map +1 -1
  66. package/esm/CircularView/models/CircularView.d.ts +30 -26
  67. package/esm/CircularView/models/CircularView.js +63 -22
  68. package/esm/CircularView/models/CircularView.js.map +1 -1
  69. package/esm/CircularView/models/slices.d.ts +23 -10
  70. package/esm/CircularView/models/slices.js +10 -7
  71. package/esm/CircularView/models/slices.js.map +1 -1
  72. package/esm/CircularView/models/viewportVisibleRegion.js +2 -4
  73. package/esm/CircularView/models/viewportVisibleRegion.js.map +1 -1
  74. package/esm/CircularView/svgcomponents/SVGBackground.d.ts +6 -0
  75. package/esm/CircularView/svgcomponents/SVGBackground.js +7 -0
  76. package/esm/CircularView/svgcomponents/SVGBackground.js.map +1 -0
  77. package/esm/CircularView/svgcomponents/SVGCircularView.d.ts +4 -0
  78. package/esm/CircularView/svgcomponents/SVGCircularView.js +33 -0
  79. package/esm/CircularView/svgcomponents/SVGCircularView.js.map +1 -0
  80. package/esm/LaunchCircularView/index.js +1 -1
  81. package/esm/LaunchCircularView/index.js.map +1 -1
  82. package/esm/index.d.ts +1 -2
  83. package/esm/index.js +1 -0
  84. package/esm/index.js.map +1 -1
  85. package/package.json +5 -3
  86. package/src/BaseChordDisplay/components/Loading.tsx +1 -0
  87. package/src/BaseChordDisplay/index.ts +2 -2
  88. package/src/BaseChordDisplay/models/{BaseChordDisplayModel.ts → model.tsx} +47 -25
  89. package/src/BaseChordDisplay/models/renderReaction.ts +4 -7
  90. package/src/CircularView/components/CircularView.tsx +11 -26
  91. package/src/CircularView/components/Controls.tsx +41 -28
  92. package/src/CircularView/components/ExportSvgDialog.tsx +132 -0
  93. package/src/CircularView/components/Ruler.tsx +187 -182
  94. package/src/CircularView/models/CircularView.ts +87 -45
  95. package/src/CircularView/models/slices.ts +32 -12
  96. package/src/CircularView/models/viewportVisibleRegion.ts +2 -4
  97. package/src/CircularView/svgcomponents/SVGBackground.tsx +21 -0
  98. package/src/CircularView/svgcomponents/SVGCircularView.tsx +58 -0
  99. package/src/LaunchCircularView/index.ts +1 -1
  100. package/src/index.ts +5 -5
  101. package/dist/BaseChordDisplay/models/BaseChordDisplayModel.js.map +0 -1
  102. package/dist/BaseChordDisplay/models/baseChordDisplayConfig.js.map +0 -1
  103. package/esm/BaseChordDisplay/models/BaseChordDisplayModel.js.map +0 -1
  104. package/esm/BaseChordDisplay/models/baseChordDisplayConfig.js.map +0 -1
  105. /package/dist/BaseChordDisplay/models/{baseChordDisplayConfig.d.ts → configSchema.d.ts} +0 -0
  106. /package/esm/BaseChordDisplay/models/{baseChordDisplayConfig.d.ts → configSchema.d.ts} +0 -0
  107. /package/src/BaseChordDisplay/models/{baseChordDisplayConfig.ts → configSchema.ts} +0 -0
@@ -11,7 +11,11 @@ import { useTheme } from '@mui/material/styles'
11
11
  import { makeStyles } from 'tss-react/mui'
12
12
 
13
13
  // locals
14
- import { Slice } from '../models/slices'
14
+ import {
15
+ Slice,
16
+ SliceElidedRegion,
17
+ SliceNonElidedRegion,
18
+ } from '../models/slices'
15
19
  import { CircularViewModel } from '../models/CircularView'
16
20
 
17
21
  const useStyles = makeStyles()({
@@ -51,128 +55,110 @@ function sliceArcPath(
51
55
  ].join(' ')
52
56
  }
53
57
 
54
- const ElisionRulerArc = observer(
55
- ({ model, slice }: { model: CircularViewModel; slice: Slice }) => {
56
- const theme = useTheme()
57
- const { radiusPx: modelRadiusPx } = model
58
- const radiusPx = modelRadiusPx + 1
59
- const { endRadians, startRadians, region } = slice
60
- const startXY = polarToCartesian(radiusPx, startRadians)
61
- const endXY = polarToCartesian(radiusPx, endRadians)
62
- const widthPx = (endRadians - startRadians) * radiusPx
63
- const largeArc = endRadians - startRadians > Math.PI ? '1' : '0'
64
- // TODO: draw the elision
65
- const centerRadians = (endRadians + startRadians) / 2
66
- const regionCountString = `[${Number(
67
- // @ts-ignore
68
- region.regions.length,
69
- ).toLocaleString()}]`
70
- return (
71
- <>
72
- <RulerLabel
73
- text={regionCountString}
74
- view={model}
75
- maxWidthPx={widthPx}
76
- radians={centerRadians}
77
- radiusPx={radiusPx}
78
- title={`${Number(
79
- // @ts-ignore
80
- region.regions.length,
81
- ).toLocaleString()} more regions`}
82
- color={theme.palette.text.primary}
83
- />
84
- <path
85
- d={[
86
- 'M',
87
- ...startXY,
88
- 'A',
89
- radiusPx,
90
- radiusPx,
91
- '0',
92
- largeArc,
93
- '1',
94
- ...endXY,
95
- ].join(' ')}
96
- stroke={theme.palette.text.secondary}
97
- strokeWidth={2}
98
- strokeDasharray="2,2"
99
- fill="none"
100
- />
101
- </>
102
- )
103
- },
104
- )
58
+ const ElisionRulerArc = observer(function ({
59
+ model,
60
+ slice,
61
+ region,
62
+ }: {
63
+ model: CircularViewModel
64
+ slice: Slice
65
+ region: SliceElidedRegion
66
+ }) {
67
+ const theme = useTheme()
68
+ const { radiusPx: modelRadiusPx } = model
69
+ const radiusPx = modelRadiusPx + 1
70
+ const { endRadians, startRadians } = slice
71
+ const startXY = polarToCartesian(radiusPx, startRadians)
72
+ const endXY = polarToCartesian(radiusPx, endRadians)
73
+ const widthPx = (endRadians - startRadians) * radiusPx
74
+ const largeArc = endRadians - startRadians > Math.PI ? '1' : '0'
75
+ // TODO: draw the elision
76
+ const centerRadians = (endRadians + startRadians) / 2
77
+ const regionCount = `[${Number(region.regions.length).toLocaleString()}]`
78
+ return (
79
+ <>
80
+ <RulerLabel
81
+ text={regionCount}
82
+ view={model}
83
+ maxWidthPx={widthPx}
84
+ radians={centerRadians}
85
+ radiusPx={radiusPx}
86
+ title={`${regionCount} more regions`}
87
+ color={theme.palette.text.primary}
88
+ />
89
+ <path
90
+ d={[
91
+ 'M',
92
+ ...startXY,
93
+ 'A',
94
+ radiusPx,
95
+ radiusPx,
96
+ '0',
97
+ largeArc,
98
+ '1',
99
+ ...endXY,
100
+ ].join(' ')}
101
+ stroke={theme.palette.text.secondary}
102
+ strokeWidth={2}
103
+ strokeDasharray="2,2"
104
+ fill="none"
105
+ />
106
+ </>
107
+ )
108
+ })
105
109
 
106
- const RulerLabel = observer(
107
- ({
108
- view,
109
- text,
110
- maxWidthPx,
111
- radians,
112
- radiusPx,
113
- title,
114
- color,
115
- }: {
116
- view: CircularViewModel
117
- text: string
118
- maxWidthPx: number
119
- radiusPx: number
120
- radians: number
121
- title?: string
122
- color: string
123
- }) => {
124
- const { classes } = useStyles()
125
- const textXY = polarToCartesian(radiusPx + 5, radians)
126
- if (!text) {
127
- return null
128
- }
110
+ const RulerLabel = observer(function ({
111
+ view,
112
+ text,
113
+ maxWidthPx,
114
+ radians,
115
+ radiusPx,
116
+ title,
117
+ color,
118
+ }: {
119
+ view: CircularViewModel
120
+ text: string
121
+ maxWidthPx: number
122
+ radiusPx: number
123
+ radians: number
124
+ title?: string
125
+ color: string
126
+ }) {
127
+ const { classes } = useStyles()
128
+ const textXY = polarToCartesian(radiusPx + 5, radians)
129
+ if (!text) {
130
+ return null
131
+ }
129
132
 
130
- if (text.length * 6.5 < maxWidthPx) {
131
- // text is rotated parallel to the ruler arc
132
- return (
133
- <text
134
- x={0}
135
- y={0}
136
- className={classes.rulerLabel}
137
- textAnchor="middle"
138
- dominantBaseline="baseline"
139
- transform={`translate(${textXY}) rotate(${radToDeg(radians) + 90})`}
140
- style={{ fill: color }}
141
- >
142
- {text}
143
- <title>{title || text}</title>
144
- </text>
145
- )
146
- }
147
- if (maxWidthPx > 4) {
148
- // text is rotated perpendicular to the ruler arc
149
- const overallRotation = radToDeg(
150
- radians + view.offsetRadians - Math.PI / 2,
151
- )
152
- if (overallRotation >= 180) {
153
- return (
154
- <text
155
- x={0}
156
- y={0}
157
- className={classes.rulerLabel}
158
- textAnchor="start"
159
- dominantBaseline="middle"
160
- transform={`translate(${textXY}) rotate(${radToDeg(radians)})`}
161
- style={{ fill: color }}
162
- >
163
- {text}
164
- <title>{title || text}</title>
165
- </text>
166
- )
167
- }
133
+ if (text.length * 6.5 < maxWidthPx) {
134
+ // text is rotated parallel to the ruler arc
135
+ return (
136
+ <text
137
+ x={0}
138
+ y={0}
139
+ className={classes.rulerLabel}
140
+ textAnchor="middle"
141
+ dominantBaseline="baseline"
142
+ transform={`translate(${textXY}) rotate(${radToDeg(radians) + 90})`}
143
+ style={{ fill: color }}
144
+ >
145
+ {text}
146
+ <title>{title || text}</title>
147
+ </text>
148
+ )
149
+ }
150
+ if (maxWidthPx > 4) {
151
+ // text is rotated perpendicular to the ruler arc
152
+ const overallRotation = radToDeg(radians + view.offsetRadians - Math.PI / 2)
153
+ if (overallRotation >= 180) {
168
154
  return (
169
155
  <text
170
156
  x={0}
171
157
  y={0}
172
158
  className={classes.rulerLabel}
173
- textAnchor="end"
159
+ textAnchor="start"
174
160
  dominantBaseline="middle"
175
- transform={`translate(${textXY}) rotate(${radToDeg(radians) + 180})`}
161
+ transform={`translate(${textXY}) rotate(${radToDeg(radians)})`}
176
162
  style={{ fill: color }}
177
163
  >
178
164
  {text}
@@ -180,81 +166,100 @@ const RulerLabel = observer(
180
166
  </text>
181
167
  )
182
168
  }
169
+ return (
170
+ <text
171
+ x={0}
172
+ y={0}
173
+ className={classes.rulerLabel}
174
+ textAnchor="end"
175
+ dominantBaseline="middle"
176
+ transform={`translate(${textXY}) rotate(${radToDeg(radians) + 180})`}
177
+ style={{ fill: color }}
178
+ >
179
+ {text}
180
+ <title>{title || text}</title>
181
+ </text>
182
+ )
183
+ }
183
184
 
184
- // if you get here there is no room for the text at all
185
- return null
186
- },
187
- )
185
+ // if you get here there is no room for the text at all
186
+ return null
187
+ })
188
188
 
189
- const RegionRulerArc = observer(
190
- ({ model, slice }: { model: CircularViewModel; slice: Slice }) => {
191
- const theme = useTheme()
192
- const { radiusPx } = model
193
- const { region, endRadians, startRadians } = slice
194
- const centerRadians = (endRadians + startRadians) / 2
195
- const widthPx = (endRadians - startRadians) * radiusPx
196
- const session = getSession(model)
197
- let color
198
- const assembly = session.assemblyManager.get(slice.region.assemblyName)
199
- if (assembly) {
200
- color = assembly.getRefNameColor(region.refName)
201
- }
202
- if (color) {
203
- try {
204
- color = makeContrasting(color, theme.palette.background.paper)
205
- } catch (error) {
206
- color = theme.palette.text.primary
207
- }
208
- } else {
189
+ const RegionRulerArc = observer(function ({
190
+ model,
191
+ slice,
192
+ region,
193
+ }: {
194
+ model: CircularViewModel
195
+ slice: Slice
196
+ region: SliceNonElidedRegion
197
+ }) {
198
+ const theme = useTheme()
199
+ const { radiusPx } = model
200
+ const { endRadians, startRadians } = slice
201
+ const centerRadians = (endRadians + startRadians) / 2
202
+ const widthPx = (endRadians - startRadians) * radiusPx
203
+ const session = getSession(model)
204
+ let color
205
+ const assembly = session.assemblyManager.get(region.assemblyName)
206
+ if (assembly) {
207
+ color = assembly.getRefNameColor(region.refName)
208
+ }
209
+ if (color) {
210
+ try {
211
+ color = makeContrasting(color, theme.palette.background.paper)
212
+ } catch (error) {
209
213
  color = theme.palette.text.primary
210
214
  }
215
+ } else {
216
+ color = theme.palette.text.primary
217
+ }
211
218
 
212
- // TODO: slice flipping
213
- return (
214
- <>
215
- <RulerLabel
216
- text={region.refName}
217
- view={model}
218
- maxWidthPx={widthPx}
219
- radians={centerRadians}
220
- radiusPx={radiusPx}
221
- color={color}
222
- />
223
- <path
224
- d={sliceArcPath(slice, radiusPx + 1, region.start, region.end)}
225
- stroke={color}
226
- strokeWidth={2}
227
- fill="none"
228
- >
229
- <title>{region.refName}</title>
230
- </path>
231
- </>
232
- )
233
- },
234
- )
219
+ // TODO: slice flipping
220
+ return (
221
+ <>
222
+ <RulerLabel
223
+ text={region.refName}
224
+ view={model}
225
+ maxWidthPx={widthPx}
226
+ radians={centerRadians}
227
+ radiusPx={radiusPx}
228
+ color={color}
229
+ />
230
+ <path
231
+ d={sliceArcPath(slice, radiusPx + 1, region.start, region.end)}
232
+ stroke={color}
233
+ strokeWidth={2}
234
+ fill="none"
235
+ />
236
+ </>
237
+ )
238
+ })
235
239
 
236
- const CircularRuler = observer(
237
- ({ model, slice }: { model: CircularViewModel; slice: Slice }) => {
238
- if (slice.region.elided) {
239
- return (
240
- <ElisionRulerArc
241
- key={
242
- /* @ts-ignore */
243
- assembleLocString(slice.region.regions[0])
244
- }
245
- model={model}
246
- slice={slice}
247
- />
248
- )
249
- }
240
+ export default observer(function ({
241
+ model,
242
+ slice,
243
+ }: {
244
+ model: CircularViewModel
245
+ slice: Slice
246
+ }) {
247
+ if (slice.region.elided) {
250
248
  return (
251
- <RegionRulerArc
252
- key={assembleLocString(slice.region)}
249
+ <ElisionRulerArc
250
+ key={assembleLocString(slice.region.regions[0])}
253
251
  model={model}
252
+ region={slice.region}
254
253
  slice={slice}
255
254
  />
256
255
  )
257
- },
258
- )
259
-
260
- export default CircularRuler
256
+ }
257
+ return (
258
+ <RegionRulerArc
259
+ key={assembleLocString(slice.region)}
260
+ region={slice.region}
261
+ model={model}
262
+ slice={slice}
263
+ />
264
+ )
265
+ })
@@ -1,3 +1,4 @@
1
+ import React from 'react'
1
2
  import PluginManager from '@jbrowse/core/PluginManager'
2
3
  import {
3
4
  cast,
@@ -10,22 +11,38 @@ import {
10
11
  } from 'mobx-state-tree'
11
12
  import { Region } from '@jbrowse/core/util/types/mst'
12
13
  import { transaction } from 'mobx'
14
+ import { saveAs } from 'file-saver'
15
+ import { renderToSvg } from '../svgcomponents/SVGCircularView'
13
16
  import {
14
17
  AnyConfigurationModel,
15
18
  readConfObject,
16
19
  } from '@jbrowse/core/configuration'
17
-
20
+ import { MenuItem } from '@jbrowse/core/ui'
18
21
  import {
19
22
  getSession,
20
23
  clamp,
21
24
  isSessionModelWithWidgets,
22
- Region as IRegion,
23
25
  } from '@jbrowse/core/util'
24
26
  import { BaseViewModel } from '@jbrowse/core/pluggableElementTypes/models'
25
- import { calculateStaticSlices, sliceIsVisible } from './slices'
26
27
 
28
+ // icons
29
+ import { TrackSelector as TrackSelectorIcon } from '@jbrowse/core/ui/Icons'
30
+ import FolderOpenIcon from '@mui/icons-material/FolderOpen'
31
+ import PhotoCameraIcon from '@mui/icons-material/PhotoCamera'
32
+
33
+ // locals
34
+ import ExportSvgDlg from '../components/ExportSvgDialog'
35
+ import { calculateStaticSlices, sliceIsVisible, SliceRegion } from './slices'
27
36
  import { viewportVisibleSection } from './viewportVisibleRegion'
28
37
 
38
+ export interface ExportSvgOptions {
39
+ rasterizeLayers?: boolean
40
+ filename?: string
41
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
+ Wrapper?: React.FC<any>
43
+ themeName?: string
44
+ }
45
+
29
46
  /**
30
47
  * #stateModel CircularView
31
48
  * extends `BaseViewModel`
@@ -121,12 +138,6 @@ function stateModelFactory(pluginManager: PluginManager) {
121
138
  }
122
139
  return self.volatileWidth
123
140
  },
124
- /**
125
- * #getter
126
- */
127
- get staticSlices() {
128
- return calculateStaticSlices(self)
129
- },
130
141
 
131
142
  /**
132
143
  * #getter
@@ -262,20 +273,7 @@ function stateModelFactory(pluginManager: PluginManager) {
262
273
  * elide regions that are too small to see reasonably
263
274
  */
264
275
  get elidedRegions() {
265
- const visible: (
266
- | {
267
- elided: true
268
- widthBp: number
269
- regions: IRegion[]
270
- }
271
- | {
272
- elided: false
273
- widthBp: number
274
- start: number
275
- end: number
276
- refName: string
277
- }
278
- )[] = []
276
+ const visible: SliceRegion[] = []
279
277
  self.displayedRegions.forEach(region => {
280
278
  const widthBp = region.end - region.start
281
279
  const widthPx = widthBp / self.bpPerPx
@@ -330,6 +328,14 @@ function stateModelFactory(pluginManager: PluginManager) {
330
328
  )
331
329
  },
332
330
  }))
331
+ .views(self => ({
332
+ /**
333
+ * #getter
334
+ */
335
+ get staticSlices() {
336
+ return calculateStaticSlices(self)
337
+ },
338
+ }))
333
339
  .views(self => ({
334
340
  /**
335
341
  * #getter
@@ -501,9 +507,11 @@ function stateModelFactory(pluginManager: PluginManager) {
501
507
  throw new Error(`unknown track type ${conf.type}`)
502
508
  }
503
509
  const viewType = pluginManager.getViewType(self.type)
504
- const supportedDisplays = viewType.displayTypes.map(d => d.name)
510
+ const supportedDisplays = new Set(
511
+ viewType.displayTypes.map(d => d.name),
512
+ )
505
513
  const displayConf = conf.displays.find((d: AnyConfigurationModel) =>
506
- supportedDisplays.includes(d.type),
514
+ supportedDisplays.has(d.type),
507
515
  )
508
516
  const track = trackType.stateModel.create({
509
517
  ...initialSnapshot,
@@ -522,21 +530,24 @@ function stateModelFactory(pluginManager: PluginManager) {
522
530
  const name = readConfObject(configuration, 'name')
523
531
  const trackType = pluginManager.getTrackType(type)
524
532
  if (!trackType) {
525
- throw new Error(`unknown track type ${configuration.type}`)
533
+ throw new Error(`unknown track type ${type}`)
526
534
  }
527
535
  const viewType = pluginManager.getViewType(self.type)
528
- const supportedDisplays = viewType.displayTypes.map(d => d.name)
536
+ const supportedDisplays = new Set(
537
+ viewType.displayTypes.map(d => d.name),
538
+ )
529
539
  const displayConf = configuration.displays.find(
530
- (d: AnyConfigurationModel) => supportedDisplays.includes(d.type),
540
+ (d: AnyConfigurationModel) => supportedDisplays.has(d.type),
541
+ )
542
+ self.tracks.push(
543
+ trackType.stateModel.create({
544
+ ...initialSnapshot,
545
+ name,
546
+ type,
547
+ configuration,
548
+ displays: [{ type: displayConf.type, configuration: displayConf }],
549
+ }),
531
550
  )
532
- const track = trackType.stateModel.create({
533
- ...initialSnapshot,
534
- name,
535
- type,
536
- configuration,
537
- displays: [{ type: displayConf.type, configuration: displayConf }],
538
- })
539
- self.tracks.push(track)
540
551
  },
541
552
 
542
553
  /**
@@ -560,19 +571,50 @@ function stateModelFactory(pluginManager: PluginManager) {
560
571
  this.setModelViewWhenAdjust(self.atMinBpPerPx)
561
572
  return self.lockedFitToWindow
562
573
  },
574
+ /**
575
+ * #action
576
+ * creates an svg export and save using FileSaver
577
+ */
578
+ async exportSvg(opts: ExportSvgOptions = {}) {
579
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
580
+ const html = await renderToSvg(self as any, opts)
581
+ const blob = new Blob([html], { type: 'image/svg+xml' })
582
+ saveAs(blob, opts.filename || 'image.svg')
583
+ },
584
+ }))
585
+ .views(self => ({
586
+ /**
587
+ * #method
588
+ * return the view menu items
589
+ */
590
+ menuItems(): MenuItem[] {
591
+ return [
592
+ {
593
+ label: 'Return to import form',
594
+ onClick: () => self.setDisplayedRegions([]),
595
+ icon: FolderOpenIcon,
596
+ },
597
+ {
598
+ label: 'Export SVG',
599
+ icon: PhotoCameraIcon,
600
+ onClick: () => {
601
+ getSession(self).queueDialog(handleClose => [
602
+ ExportSvgDlg,
603
+ { model: self, handleClose },
604
+ ])
605
+ },
606
+ },
607
+ {
608
+ label: 'Open track selector',
609
+ onClick: self.activateTrackSelector,
610
+ icon: TrackSelectorIcon,
611
+ },
612
+ ]
613
+ },
563
614
  }))
564
615
  }
565
616
 
566
617
  export type CircularViewStateModel = ReturnType<typeof stateModelFactory>
567
618
  export type CircularViewModel = Instance<CircularViewStateModel>
568
619
 
569
- /**
570
- PLANS
571
-
572
- - tracks
573
- - ruler tick marks
574
- - set viewport scroll from state snapshot
575
-
576
- */
577
-
578
620
  export default stateModelFactory
@@ -1,11 +1,25 @@
1
1
  import { polarToCartesian, assembleLocString, Region } from '@jbrowse/core/util'
2
2
  import { thetaRangesOverlap } from './viewportVisibleRegion'
3
3
 
4
+ export type SliceElidedRegion = {
5
+ elided: true
6
+ widthBp: number
7
+ regions: Region[]
8
+ }
9
+
10
+ export type SliceNonElidedRegion = {
11
+ elided: false
12
+ widthBp: number
13
+ start: number
14
+ end: number
15
+ refName: string
16
+ assemblyName: string
17
+ }
18
+ export type SliceRegion = SliceNonElidedRegion | SliceElidedRegion
19
+
4
20
  export class Slice {
5
21
  key: string
6
22
 
7
- offsetRadians: number
8
-
9
23
  startRadians: number
10
24
 
11
25
  endRadians: number
@@ -16,18 +30,20 @@ export class Slice {
16
30
 
17
31
  constructor(
18
32
  view: { bpPerRadian: number },
19
- public region: Region & { widthBp: number; elided: boolean },
20
- currentRadianOffset: number,
33
+ public region: SliceRegion,
34
+ public offsetRadians: number,
21
35
  public radianWidth: number,
22
36
  ) {
23
37
  const { bpPerRadian } = view
24
- this.key = assembleLocString(region)
25
- this.offsetRadians = currentRadianOffset
38
+ this.key =
39
+ 'regions' in region
40
+ ? JSON.stringify(region.regions)
41
+ : assembleLocString(region)
26
42
  this.bpPerRadian = bpPerRadian
27
43
  this.flipped = false
28
44
 
29
- this.startRadians = this.offsetRadians
30
- this.endRadians = region.widthBp / this.bpPerRadian + this.offsetRadians
45
+ this.startRadians = offsetRadians
46
+ this.endRadians = region.widthBp / this.bpPerRadian + offsetRadians
31
47
  Object.freeze(this)
32
48
  }
33
49
 
@@ -49,13 +65,17 @@ export class Slice {
49
65
  }
50
66
  }
51
67
 
52
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
- function calculateStaticSlices(self: any) {
68
+ function calculateStaticSlices(self: {
69
+ elidedRegions: SliceRegion[]
70
+ bpPerRadian: number
71
+ spacingPx: number
72
+ pxPerRadian: number
73
+ }) {
54
74
  const slices = []
55
75
  let currentRadianOffset = 0
76
+ const { bpPerRadian, spacingPx, pxPerRadian } = self
56
77
  for (const region of self.elidedRegions) {
57
- const radianWidth =
58
- region.widthBp / self.bpPerRadian + self.spacingPx / self.pxPerRadian
78
+ const radianWidth = region.widthBp / bpPerRadian + spacingPx / pxPerRadian
59
79
  slices.push(new Slice(self, region, currentRadianOffset, radianWidth))
60
80
  currentRadianOffset += radianWidth
61
81
  }