@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.
- package/dist/BaseChordDisplay/components/Loading.js.map +1 -1
- package/dist/BaseChordDisplay/index.d.ts +2 -2
- package/dist/BaseChordDisplay/index.js +4 -4
- package/dist/BaseChordDisplay/index.js.map +1 -1
- package/dist/BaseChordDisplay/models/{baseChordDisplayConfig.js → configSchema.js} +1 -1
- package/dist/BaseChordDisplay/models/configSchema.js.map +1 -0
- package/dist/BaseChordDisplay/models/{BaseChordDisplayModel.d.ts → model.d.ts} +12 -0
- package/dist/BaseChordDisplay/models/{BaseChordDisplayModel.js → model.js} +33 -23
- package/dist/BaseChordDisplay/models/model.js.map +1 -0
- package/dist/BaseChordDisplay/models/renderReaction.d.ts +1 -1
- package/dist/BaseChordDisplay/models/renderReaction.js +4 -5
- package/dist/BaseChordDisplay/models/renderReaction.js.map +1 -1
- package/dist/CircularView/components/CircularView.js +9 -20
- package/dist/CircularView/components/CircularView.js.map +1 -1
- package/dist/CircularView/components/Controls.d.ts +2 -2
- package/dist/CircularView/components/Controls.js +60 -24
- package/dist/CircularView/components/Controls.js.map +1 -1
- package/dist/CircularView/components/ExportSvgDialog.d.ts +8 -0
- package/dist/CircularView/components/ExportSvgDialog.js +76 -0
- package/dist/CircularView/components/ExportSvgDialog.js.map +1 -0
- package/dist/CircularView/components/Ruler.d.ts +2 -2
- package/dist/CircularView/components/Ruler.js +12 -20
- package/dist/CircularView/components/Ruler.js.map +1 -1
- package/dist/CircularView/models/CircularView.d.ts +30 -26
- package/dist/CircularView/models/CircularView.js +66 -22
- package/dist/CircularView/models/CircularView.js.map +1 -1
- package/dist/CircularView/models/slices.d.ts +23 -10
- package/dist/CircularView/models/slices.js +10 -7
- package/dist/CircularView/models/slices.js.map +1 -1
- package/dist/CircularView/models/viewportVisibleRegion.js +2 -4
- package/dist/CircularView/models/viewportVisibleRegion.js.map +1 -1
- package/dist/CircularView/svgcomponents/SVGBackground.d.ts +6 -0
- package/dist/CircularView/svgcomponents/SVGBackground.js +13 -0
- package/dist/CircularView/svgcomponents/SVGBackground.js.map +1 -0
- package/dist/CircularView/svgcomponents/SVGCircularView.d.ts +4 -0
- package/dist/CircularView/svgcomponents/SVGCircularView.js +40 -0
- package/dist/CircularView/svgcomponents/SVGCircularView.js.map +1 -0
- package/dist/LaunchCircularView/index.js +1 -1
- package/dist/LaunchCircularView/index.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/esm/BaseChordDisplay/components/Loading.js.map +1 -1
- package/esm/BaseChordDisplay/index.d.ts +2 -2
- package/esm/BaseChordDisplay/index.js +2 -2
- package/esm/BaseChordDisplay/index.js.map +1 -1
- package/esm/BaseChordDisplay/models/{baseChordDisplayConfig.js → configSchema.js} +1 -1
- package/esm/BaseChordDisplay/models/configSchema.js.map +1 -0
- package/esm/BaseChordDisplay/models/{BaseChordDisplayModel.d.ts → model.d.ts} +12 -0
- package/esm/BaseChordDisplay/models/{BaseChordDisplayModel.js → model.js} +34 -24
- package/esm/BaseChordDisplay/models/model.js.map +1 -0
- package/esm/BaseChordDisplay/models/renderReaction.d.ts +1 -1
- package/esm/BaseChordDisplay/models/renderReaction.js +4 -5
- package/esm/BaseChordDisplay/models/renderReaction.js.map +1 -1
- package/esm/CircularView/components/CircularView.js +9 -20
- package/esm/CircularView/components/CircularView.js.map +1 -1
- package/esm/CircularView/components/Controls.d.ts +2 -2
- package/esm/CircularView/components/Controls.js +37 -24
- package/esm/CircularView/components/Controls.js.map +1 -1
- package/esm/CircularView/components/ExportSvgDialog.d.ts +8 -0
- package/esm/CircularView/components/ExportSvgDialog.js +50 -0
- package/esm/CircularView/components/ExportSvgDialog.js.map +1 -0
- package/esm/CircularView/components/Ruler.d.ts +2 -2
- package/esm/CircularView/components/Ruler.js +12 -20
- package/esm/CircularView/components/Ruler.js.map +1 -1
- package/esm/CircularView/models/CircularView.d.ts +30 -26
- package/esm/CircularView/models/CircularView.js +63 -22
- package/esm/CircularView/models/CircularView.js.map +1 -1
- package/esm/CircularView/models/slices.d.ts +23 -10
- package/esm/CircularView/models/slices.js +10 -7
- package/esm/CircularView/models/slices.js.map +1 -1
- package/esm/CircularView/models/viewportVisibleRegion.js +2 -4
- package/esm/CircularView/models/viewportVisibleRegion.js.map +1 -1
- package/esm/CircularView/svgcomponents/SVGBackground.d.ts +6 -0
- package/esm/CircularView/svgcomponents/SVGBackground.js +7 -0
- package/esm/CircularView/svgcomponents/SVGBackground.js.map +1 -0
- package/esm/CircularView/svgcomponents/SVGCircularView.d.ts +4 -0
- package/esm/CircularView/svgcomponents/SVGCircularView.js +33 -0
- package/esm/CircularView/svgcomponents/SVGCircularView.js.map +1 -0
- package/esm/LaunchCircularView/index.js +1 -1
- package/esm/LaunchCircularView/index.js.map +1 -1
- package/esm/index.d.ts +1 -2
- package/esm/index.js +1 -0
- package/esm/index.js.map +1 -1
- package/package.json +5 -3
- package/src/BaseChordDisplay/components/Loading.tsx +1 -0
- package/src/BaseChordDisplay/index.ts +2 -2
- package/src/BaseChordDisplay/models/{BaseChordDisplayModel.ts → model.tsx} +47 -25
- package/src/BaseChordDisplay/models/renderReaction.ts +4 -7
- package/src/CircularView/components/CircularView.tsx +11 -26
- package/src/CircularView/components/Controls.tsx +41 -28
- package/src/CircularView/components/ExportSvgDialog.tsx +132 -0
- package/src/CircularView/components/Ruler.tsx +187 -182
- package/src/CircularView/models/CircularView.ts +87 -45
- package/src/CircularView/models/slices.ts +32 -12
- package/src/CircularView/models/viewportVisibleRegion.ts +2 -4
- package/src/CircularView/svgcomponents/SVGBackground.tsx +21 -0
- package/src/CircularView/svgcomponents/SVGCircularView.tsx +58 -0
- package/src/LaunchCircularView/index.ts +1 -1
- package/src/index.ts +5 -5
- package/dist/BaseChordDisplay/models/BaseChordDisplayModel.js.map +0 -1
- package/dist/BaseChordDisplay/models/baseChordDisplayConfig.js.map +0 -1
- package/esm/BaseChordDisplay/models/BaseChordDisplayModel.js.map +0 -1
- package/esm/BaseChordDisplay/models/baseChordDisplayConfig.js.map +0 -1
- /package/dist/BaseChordDisplay/models/{baseChordDisplayConfig.d.ts → configSchema.d.ts} +0 -0
- /package/esm/BaseChordDisplay/models/{baseChordDisplayConfig.d.ts → configSchema.d.ts} +0 -0
- /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 {
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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="
|
|
159
|
+
textAnchor="start"
|
|
174
160
|
dominantBaseline="middle"
|
|
175
|
-
transform={`translate(${textXY}) rotate(${radToDeg(radians)
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
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 =
|
|
510
|
+
const supportedDisplays = new Set(
|
|
511
|
+
viewType.displayTypes.map(d => d.name),
|
|
512
|
+
)
|
|
505
513
|
const displayConf = conf.displays.find((d: AnyConfigurationModel) =>
|
|
506
|
-
supportedDisplays.
|
|
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 ${
|
|
533
|
+
throw new Error(`unknown track type ${type}`)
|
|
526
534
|
}
|
|
527
535
|
const viewType = pluginManager.getViewType(self.type)
|
|
528
|
-
const supportedDisplays =
|
|
536
|
+
const supportedDisplays = new Set(
|
|
537
|
+
viewType.displayTypes.map(d => d.name),
|
|
538
|
+
)
|
|
529
539
|
const displayConf = configuration.displays.find(
|
|
530
|
-
(d: AnyConfigurationModel) => supportedDisplays.
|
|
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:
|
|
20
|
-
|
|
33
|
+
public region: SliceRegion,
|
|
34
|
+
public offsetRadians: number,
|
|
21
35
|
public radianWidth: number,
|
|
22
36
|
) {
|
|
23
37
|
const { bpPerRadian } = view
|
|
24
|
-
this.key =
|
|
25
|
-
|
|
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 =
|
|
30
|
-
this.endRadians = region.widthBp / this.bpPerRadian +
|
|
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
|
-
|
|
53
|
-
|
|
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
|
}
|