@jbrowse/plugin-linear-genome-view 5.0.0-beta.11 → 5.0.0-beta.15

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 (69) hide show
  1. package/esm/FeatureTrack/configSchema.d.ts +2 -1
  2. package/esm/FeatureTrack/configSchema.js +2 -1
  3. package/esm/LinearGenomeView/afterAttach.js +1 -23
  4. package/esm/LinearGenomeView/components/ExportSvgDialog.d.ts +6 -2
  5. package/esm/LinearGenomeView/components/ExportSvgDialog.js +31 -7
  6. package/esm/LinearGenomeView/components/HeaderPanControls.d.ts +3 -3
  7. package/esm/LinearGenomeView/components/HeaderPanControls.js +4 -2
  8. package/esm/LinearGenomeView/components/HeaderSearchBoxes.js +4 -1
  9. package/esm/LinearGenomeView/components/HighlightBand.d.ts +3 -2
  10. package/esm/LinearGenomeView/components/HighlightBand.js +4 -2
  11. package/esm/LinearGenomeView/components/HighlightChip.d.ts +3 -2
  12. package/esm/LinearGenomeView/components/HighlightChip.js +4 -2
  13. package/esm/LinearGenomeView/components/LinearGenomeView.js +5 -12
  14. package/esm/LinearGenomeView/components/LinearGenomeViewContainer.js +7 -9
  15. package/esm/LinearGenomeView/components/MiniControls.js +8 -3
  16. package/esm/LinearGenomeView/components/OverviewHighlightBand.d.ts +3 -2
  17. package/esm/LinearGenomeView/components/OverviewHighlightBand.js +4 -2
  18. package/esm/LinearGenomeView/components/RegionWidthEditorDialog.d.ts +3 -2
  19. package/esm/LinearGenomeView/components/RegionWidthEditorDialog.js +4 -4
  20. package/esm/LinearGenomeView/components/ScalebarRefNameLabels.js +4 -4
  21. package/esm/LinearGenomeView/components/SearchPanelForm.d.ts +3 -2
  22. package/esm/LinearGenomeView/components/SearchPanelForm.js +4 -2
  23. package/esm/LinearGenomeView/components/SearchResultsDialog.d.ts +4 -9
  24. package/esm/LinearGenomeView/components/SearchResultsDialog.js +7 -4
  25. package/esm/LinearGenomeView/components/SearchResultsTable.d.ts +3 -8
  26. package/esm/LinearGenomeView/components/SearchResultsTable.js +4 -33
  27. package/esm/LinearGenomeView/components/StrandCheckboxes.d.ts +3 -2
  28. package/esm/LinearGenomeView/components/StrandCheckboxes.js +4 -2
  29. package/esm/LinearGenomeView/components/TrackContainer.js +1 -1
  30. package/esm/LinearGenomeView/components/TrackLabel.js +4 -1
  31. package/esm/LinearGenomeView/components/TrackLabelMenu.js +8 -12
  32. package/esm/LinearGenomeView/components/useRangeSelect.js +3 -2
  33. package/esm/LinearGenomeView/components/useSearchBoxPrefs.d.ts +3 -1
  34. package/esm/LinearGenomeView/components/useSearchBoxPrefs.js +4 -2
  35. package/esm/LinearGenomeView/components/useSideScroll.js +11 -12
  36. package/esm/LinearGenomeView/lazyDialogs.d.ts +17 -2
  37. package/esm/LinearGenomeView/menuItems.js +32 -24
  38. package/esm/LinearGenomeView/model.d.ts +121 -65
  39. package/esm/LinearGenomeView/model.js +143 -135
  40. package/esm/LinearGenomeView/rExport/exportLinearView.d.ts +13 -0
  41. package/esm/LinearGenomeView/rExport/exportLinearView.js +98 -0
  42. package/esm/LinearGenomeView/rExport/rScript.d.ts +75 -0
  43. package/esm/LinearGenomeView/rExport/rScript.js +464 -0
  44. package/esm/LinearGenomeView/svgcomponents/SVGLinearGenomeView.js +4 -7
  45. package/esm/LinearGenomeView/svgcomponents/SVGStackedRow.d.ts +1 -2
  46. package/esm/LinearGenomeView/svgcomponents/SVGStackedRow.js +2 -2
  47. package/esm/LinearGenomeView/svgcomponents/SVGTrackLabel.d.ts +1 -2
  48. package/esm/LinearGenomeView/svgcomponents/SVGTrackLabel.js +5 -10
  49. package/esm/LinearGenomeView/svgcomponents/SVGTracks.d.ts +1 -2
  50. package/esm/LinearGenomeView/svgcomponents/SVGTracks.js +5 -5
  51. package/esm/LinearGenomeView/svgcomponents/SVGView.d.ts +1 -2
  52. package/esm/LinearGenomeView/svgcomponents/SVGView.js +3 -3
  53. package/esm/LinearGenomeView/svgcomponents/renderViewTracks.d.ts +24 -21
  54. package/esm/LinearGenomeView/svgcomponents/renderViewTracks.js +42 -29
  55. package/esm/LinearGenomeView/svgcomponents/useViewSvgFigure.js +32 -17
  56. package/esm/LinearGenomeView/svgcomponents/util.d.ts +16 -13
  57. package/esm/LinearGenomeView/svgcomponents/util.js +10 -19
  58. package/esm/LinearGenomeView/util.d.ts +42 -0
  59. package/esm/LinearGenomeView/util.js +81 -0
  60. package/esm/MultiLevelRubberband/MultiLevelRubberband.js +2 -2
  61. package/esm/displayKitTests/perRegionTestEnv.d.ts +45 -12
  62. package/esm/index.d.ts +4 -2
  63. package/esm/index.js +3 -1
  64. package/esm/searchUtils.js +1 -0
  65. package/esm/shared/RubberbandSpan.d.ts +3 -2
  66. package/esm/shared/RubberbandSpan.js +4 -2
  67. package/esm/shared/coordLabels.d.ts +6 -6
  68. package/esm/shared/coordLabels.js +7 -6
  69. package/package.json +10 -10
@@ -0,0 +1,464 @@
1
+ import { expandAssemblySequence, expandSidecarShorthand, refNameAliasAdapterType, } from '@jbrowse/core/assemblyManager/expandAssemblyConfigShorthand';
2
+ import { resolveUri } from '@jbrowse/core/util';
3
+ /**
4
+ * Each display ggjbrowse reads, by the reader that reads it, and the adapters
5
+ * it opens (the slot holding the file and the extension the reader knows it
6
+ * by).
7
+ */
8
+ const READERS = {
9
+ LinearBasicDisplay: {
10
+ call: 'jbrowse_features',
11
+ adapters: {
12
+ Gff3TabixAdapter: { slot: 'gffGzLocation', ext: /\.gff3?\.gz$/ },
13
+ Gff3Adapter: { slot: 'gffLocation', ext: /\.gff3?$/ },
14
+ GtfAdapter: { slot: 'gtfLocation', ext: /\.gtf$/ },
15
+ BedTabixAdapter: { slot: 'bedGzLocation', ext: /\.bed\.gz$/ },
16
+ BedAdapter: { slot: 'bedLocation', ext: /\.bed$/ },
17
+ BigBedAdapter: { slot: 'bigBedLocation', ext: /\.(bb|bigbed)$/ },
18
+ },
19
+ },
20
+ LinearVariantDisplay: {
21
+ call: 'jbrowse_features',
22
+ adapters: {
23
+ VcfTabixAdapter: { slot: 'vcfGzLocation', ext: /\.vcf\.gz$/ },
24
+ VcfAdapter: { slot: 'vcfLocation', ext: /\.vcf$/ },
25
+ },
26
+ },
27
+ LinearMultiSampleVariantDisplay: {
28
+ call: 'jbrowse_variants',
29
+ adapters: {
30
+ VcfTabixAdapter: { slot: 'vcfGzLocation', ext: /\.vcf\.gz$/ },
31
+ VcfAdapter: { slot: 'vcfLocation', ext: /\.vcf$/ },
32
+ },
33
+ },
34
+ LinearWiggleDisplay: {
35
+ call: 'jbrowse_wiggle',
36
+ adapters: {
37
+ BigWigAdapter: { slot: 'bigWigLocation', ext: /$/ },
38
+ MultiWiggleAdapter: {},
39
+ },
40
+ },
41
+ LinearAlignmentsDisplay: {
42
+ call: 'jbrowse_alignments',
43
+ adapters: {
44
+ BamAdapter: { slot: 'bamLocation', ext: /(?<!\.cram)$/ },
45
+ CramAdapter: { slot: 'cramLocation', ext: /\.cram$/ },
46
+ },
47
+ },
48
+ };
49
+ /** The ggjbrowse release whose readers and `jbrowse_stack` the script calls. */
50
+ export const GGJBROWSE_VERSION = '0.10.0';
51
+ const INSTALL = 'remotes::install_github("GMOD/ggjbrowse")';
52
+ function isSet(value) {
53
+ return !(value === undefined ||
54
+ value === null ||
55
+ value === '' ||
56
+ (Array.isArray(value) && value.length === 0) ||
57
+ (typeof value === 'object' &&
58
+ !Array.isArray(value) &&
59
+ Object.keys(value).length === 0));
60
+ }
61
+ /**
62
+ * A resolved config writes an unset slot as `''`, `[]` or `{}`, which a reader
63
+ * would take for a setting. Keeps `false` and `0`, which are values.
64
+ */
65
+ export function withoutUnset(value) {
66
+ if (Array.isArray(value)) {
67
+ return value.map(withoutUnset);
68
+ }
69
+ if (value && typeof value === 'object') {
70
+ return Object.fromEntries(Object.entries(value)
71
+ .map(([k, v]) => [k, withoutUnset(v)])
72
+ .filter(([, v]) => isSet(v)));
73
+ }
74
+ return value;
75
+ }
76
+ export function rStr(s) {
77
+ return JSON.stringify(s);
78
+ }
79
+ const RESERVED = new Set([
80
+ 'if',
81
+ 'else',
82
+ 'repeat',
83
+ 'while',
84
+ 'function',
85
+ 'for',
86
+ 'next',
87
+ 'break',
88
+ 'in',
89
+ 'TRUE',
90
+ 'FALSE',
91
+ 'NULL',
92
+ 'Inf',
93
+ 'NaN',
94
+ 'NA',
95
+ ]);
96
+ function isIdentifier(s) {
97
+ return /^[A-Za-z.][A-Za-z0-9._]*$/.test(s) && !RESERVED.has(s);
98
+ }
99
+ /**
100
+ * A config value as the R literal ggjbrowse hands the engine: a `list` per
101
+ * object, `c()` per vector of scalars, `list()` per vector of objects and per
102
+ * empty vector, which jsonlite would otherwise write as `{}`.
103
+ */
104
+ export function rValue(value) {
105
+ if (value === null || value === undefined) {
106
+ return 'NULL';
107
+ }
108
+ if (typeof value === 'string') {
109
+ return rStr(value);
110
+ }
111
+ if (typeof value === 'number' || typeof value === 'boolean') {
112
+ return typeof value === 'number' ? String(value) : value ? 'TRUE' : 'FALSE';
113
+ }
114
+ if (Array.isArray(value)) {
115
+ const items = value.map(rValue).join(', ');
116
+ return value.length && value.every(v => typeof v !== 'object')
117
+ ? `c(${items})`
118
+ : `list(${items})`;
119
+ }
120
+ return `list(${Object.entries(value)
121
+ .map(([k, v]) => `${isIdentifier(k) ? k : rStr(k)} = ${rValue(v)}`)
122
+ .join(', ')})`;
123
+ }
124
+ /**
125
+ * The path or URL a JBrowse file location points at, as R opens it: a `uri`
126
+ * resolved against its `baseUri` as the browser's fetch resolved it, a
127
+ * `file://` URL as its path, or a `localPath`.
128
+ */
129
+ export function locationPath(location) {
130
+ const loc = location;
131
+ if (!loc?.uri) {
132
+ return loc?.localPath;
133
+ }
134
+ const resolved = resolveUri({ uri: loc.uri, baseUri: loc.baseUri });
135
+ return resolved.startsWith('file://')
136
+ ? decodeURIComponent(new URL(resolved).pathname)
137
+ : resolved;
138
+ }
139
+ function isLocation(v) {
140
+ return (typeof v === 'object' &&
141
+ v !== null &&
142
+ !Array.isArray(v) &&
143
+ (typeof v.uri === 'string' ||
144
+ typeof v.localPath === 'string'));
145
+ }
146
+ /** An adapter config with each location a path or URL R opens, each path also pushed onto `uris`. */
147
+ function openable(value, uris) {
148
+ if (isLocation(value)) {
149
+ const uri = locationPath(value);
150
+ if (uri) {
151
+ uris.push(uri);
152
+ }
153
+ return { uri };
154
+ }
155
+ if (Array.isArray(value)) {
156
+ return value.map(v => openable(v, uris));
157
+ }
158
+ if (value && typeof value === 'object') {
159
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, openable(v, uris)]));
160
+ }
161
+ return value;
162
+ }
163
+ /**
164
+ * The file argument of a track's reader: its path where ggjbrowse would choose
165
+ * the track's adapter for it from the extension, and the adapter config itself
166
+ * where the adapter carries a setting or the name hides its format, so every
167
+ * setting reaches the engine as the browser has it.
168
+ */
169
+ function fileArgument(adapters, adapter) {
170
+ const type = String(adapter.type);
171
+ const known = adapters[type];
172
+ if (!known) {
173
+ return { skipped: `${type} has no R reader for this display` };
174
+ }
175
+ const uris = [];
176
+ const located = withoutUnset(openable(adapter, uris));
177
+ if (uris.length === 0) {
178
+ return { skipped: `${type} names no file` };
179
+ }
180
+ if (uris.some(u => u.startsWith('blob:'))) {
181
+ return {
182
+ skipped: 'it reads a file opened in the browser, which R cannot reach',
183
+ };
184
+ }
185
+ const file = known.slot
186
+ ? (locationPath(adapter[known.slot]) ??
187
+ (typeof adapter.uri === 'string' ? adapter.uri : undefined))
188
+ : undefined;
189
+ const index = adapter.index;
190
+ const indexPath = locationPath(index?.location) ?? locationPath(adapter.craiLocation);
191
+ const besideSuffix = type === 'CramAdapter' ? '.crai' : type === 'BamAdapter' ? '.bai' : '.tbi';
192
+ const plainSlots = new Set([
193
+ 'type',
194
+ 'uri',
195
+ 'index',
196
+ 'craiLocation',
197
+ known.slot,
198
+ ]);
199
+ const plain = file !== undefined &&
200
+ known.ext?.test(file.replace(/\?.*$/, '').toLowerCase()) &&
201
+ Object.keys(located).every(k => plainSlots.has(k)) &&
202
+ Object.keys(index ?? {}).every(k => k === 'location' || k === 'indexType') &&
203
+ (index?.indexType === 'CSI') === !!indexPath?.endsWith('.csi');
204
+ return plain
205
+ ? {
206
+ file: rStr(file),
207
+ index: indexPath && indexPath !== `${file}${besideSuffix}`
208
+ ? indexPath
209
+ : undefined,
210
+ uris,
211
+ }
212
+ : { file: rValue(located), uris };
213
+ }
214
+ /**
215
+ * Each sequence adapter `jbrowse_assembly()` builds from a path, by the
216
+ * extension ggjbrowse reads it off, and the index each takes beside it.
217
+ */
218
+ const SEQUENCE_ADAPTERS = {
219
+ TwoBitAdapter: { slot: 'twoBitLocation', ext: /\.2bit$/, beside: {} },
220
+ BgzipFastaAdapter: {
221
+ slot: 'fastaLocation',
222
+ ext: /\.gz$/,
223
+ beside: { faiLocation: '.fai', gziLocation: '.gzi' },
224
+ },
225
+ IndexedFastaAdapter: {
226
+ slot: 'fastaLocation',
227
+ ext: /^(?!.*\.(2bit|gz)$)/,
228
+ beside: { faiLocation: '.fai' },
229
+ },
230
+ };
231
+ /** The path of a sequence adapter `jbrowse_assembly()` rebuilds from that path alone. */
232
+ function plainSequence(adapter) {
233
+ const { type, uri, baseUri, ...slots } = adapter;
234
+ if (type === undefined && typeof uri === 'string') {
235
+ return Object.keys(slots).length
236
+ ? undefined
237
+ : locationPath({ uri, baseUri });
238
+ }
239
+ const known = SEQUENCE_ADAPTERS[String(type)];
240
+ const file = known && uri === undefined && locationPath(slots[known.slot]);
241
+ return file &&
242
+ known.ext.test(file.replace(/\?.*$/, '').toLowerCase()) &&
243
+ Object.entries(slots).every(([k, v]) => k === known.slot ||
244
+ (known.beside[k] !== undefined &&
245
+ locationPath(v) === `${file}${known.beside[k]}`))
246
+ ? file
247
+ : undefined;
248
+ }
249
+ /** The path of an alias adapter ggjbrowse reads as that path alone. */
250
+ function plainAliases(adapter) {
251
+ const { type, uri, baseUri, location, ...rest } = adapter;
252
+ const file = typeof uri === 'string'
253
+ ? locationPath({ uri, baseUri })
254
+ : locationPath(location);
255
+ return file &&
256
+ Object.keys(rest).length === 0 &&
257
+ type === refNameAliasAdapterType(file)
258
+ ? file
259
+ : undefined;
260
+ }
261
+ /**
262
+ * The view's assembly as the R value every reader takes: `jbrowse_assembly()`
263
+ * of its sequence and alias files where ggjbrowse builds the same config from
264
+ * their paths, and otherwise the config itself, its locations paths R opens.
265
+ */
266
+ export function assemblyArgument(snapshot) {
267
+ const config = expandAssemblySequence(snapshot);
268
+ const name = String(config.name);
269
+ const sequence = withoutUnset(config.sequence?.adapter);
270
+ if (!sequence) {
271
+ return { skipped: 'the assembly names no sequence' };
272
+ }
273
+ const aliasConfig = config.refNameAliases;
274
+ const aliases = aliasConfig
275
+ ? withoutUnset(expandSidecarShorthand(aliasConfig, refNameAliasAdapterType).adapter)
276
+ : undefined;
277
+ const uris = [];
278
+ const located = {
279
+ name,
280
+ sequence: { adapter: openable(sequence, uris) },
281
+ ...(aliases
282
+ ? { refNameAliases: { adapter: openable(aliases, uris) } }
283
+ : {}),
284
+ };
285
+ if (uris.some(u => u.startsWith('blob:'))) {
286
+ return {
287
+ skipped: 'the assembly reads a file opened in the browser, which R cannot reach',
288
+ };
289
+ }
290
+ const sequenceFile = plainSequence(sequence);
291
+ const aliasFile = aliases && plainAliases(aliases);
292
+ return sequenceFile && (!aliases || aliasFile)
293
+ ? {
294
+ value: `jbrowse_assembly(${[
295
+ rStr(sequenceFile),
296
+ ...(aliasFile ? [rStr(aliasFile)] : []),
297
+ `name = ${rStr(name)}`,
298
+ ].join(', ')})`,
299
+ }
300
+ : { value: rValue(located) };
301
+ }
302
+ /** A region as JBrowse's location box writes it, 1-based and inclusive. */
303
+ export function locusOf(refName, start, end) {
304
+ const n = (x) => Math.round(x).toLocaleString('en-US');
305
+ return `${refName}:${n(start + 1)}-${n(end)}`;
306
+ }
307
+ function lociLiteral(loci) {
308
+ return loci.length === 1
309
+ ? rStr(loci[0])
310
+ : `c(${loci.map(l => rStr(l)).join(', ')})`;
311
+ }
312
+ /** An R-safe name per track id, unique within the script. */
313
+ function trackNames(tracks) {
314
+ const taken = new Set(['locus', 'width', 'assembly', 'fig']);
315
+ return tracks.map(track => {
316
+ let base = track.trackId
317
+ .replaceAll(/[^a-zA-Z0-9]/g, '_')
318
+ .replace(/^(?=[\d_])/, 'x');
319
+ if (!isIdentifier(base)) {
320
+ base = `${base}_track`;
321
+ }
322
+ let name = base;
323
+ for (let n = 2; taken.has(name); n++) {
324
+ name = `${base}_${n}`;
325
+ }
326
+ taken.add(name);
327
+ return name;
328
+ });
329
+ }
330
+ /** What the figure cannot show of a track whose settings ggjbrowse reads. */
331
+ function settingNotes(track) {
332
+ const color = track.display.color;
333
+ return [
334
+ ...(color?.field === 'mateRefName'
335
+ ? [
336
+ 'color: mateRefName colors each contig by its hash, where the browser orders them by the assembly',
337
+ ]
338
+ : []),
339
+ ];
340
+ }
341
+ /**
342
+ * One R script for a view's tracks over its regions: a ggjbrowse reader per
343
+ * track, reading each as its display does, then `jbrowse_stack()` drawing
344
+ * them as the view stacks them. A track ggjbrowse cannot read lands in the
345
+ * header instead of stopping the figure: a script that quietly differs from
346
+ * its view says so.
347
+ */
348
+ export function exportTracksToR({ tracks, regions, assembly: assemblySnapshot, bpPerPx, out = 'figure.png', notes: viewNotes = [], }) {
349
+ const notes = [...viewNotes];
350
+ const assembly = assemblySnapshot && assemblyArgument(assemblySnapshot);
351
+ if (assembly && 'skipped' in assembly) {
352
+ notes.push(`assembly: ${assembly.skipped}`);
353
+ }
354
+ const assemblyValue = assembly && 'value' in assembly ? assembly.value : undefined;
355
+ const loci = regions.map(r => locusOf(r.refName, r.start, r.end));
356
+ const totalBp = regions.reduce((sum, r) => sum + r.end - r.start, 0);
357
+ const viewPx = bpPerPx ? Math.round(totalBp / bpPerPx) : undefined;
358
+ const widthReaders = [];
359
+ const names = trackNames(tracks);
360
+ const reads = [];
361
+ const drawn = [];
362
+ let alignments;
363
+ tracks.forEach((track, i) => {
364
+ const reader = READERS[track.displayType];
365
+ if (!reader) {
366
+ notes.push(`${track.trackName}: ${track.displayType} has no R reader in ggjbrowse yet`);
367
+ return;
368
+ }
369
+ const source = fileArgument(reader.adapters, track.adapter);
370
+ if ('skipped' in source) {
371
+ notes.push(`${track.trackName}: ${source.skipped}`);
372
+ return;
373
+ }
374
+ const { heightMode, height } = track;
375
+ const display = reader.call === 'jbrowse_features'
376
+ ? {
377
+ ...track.display,
378
+ ...(heightMode === undefined ? {} : { heightMode }),
379
+ ...(height === undefined ? {} : { height }),
380
+ }
381
+ : track.display;
382
+ const width = viewPx !== undefined;
383
+ const args = [
384
+ ...(Object.keys(display).length ? [`display = ${rValue(display)}`] : []),
385
+ ...(width ? ['width = width'] : []),
386
+ ...(assemblyValue ? ['assembly = assembly'] : []),
387
+ ...(source.index ? [`index = ${rStr(source.index)}`] : []),
388
+ ];
389
+ const name = names[i];
390
+ if (width) {
391
+ widthReaders.push(name);
392
+ }
393
+ reads.push([
394
+ `# ${track.trackName}`,
395
+ `${name} <- ${reader.call}(${source.file},`,
396
+ ` ${['locus', ...args].join(',\n ')})`,
397
+ ].join('\n'));
398
+ drawn.push(name);
399
+ if (reader.call === 'jbrowse_alignments') {
400
+ alignments ??= name;
401
+ }
402
+ notes.push(...[
403
+ ...(source.uris.some(u => u.includes('://') && u.includes('?'))
404
+ ? [
405
+ 'source: a URL carries its query string, credential included, so the script is not one to share',
406
+ ]
407
+ : []),
408
+ ...settingNotes(track),
409
+ ].map(n => `${track.trackName}: ${n}`));
410
+ });
411
+ if (drawn.length === 0) {
412
+ return { drawn: 0, script: undefined, notes };
413
+ }
414
+ const script = [
415
+ "# Generated by JBrowse 2: the view's tracks, read by ggjbrowse, JBrowse's",
416
+ '# own engine in R, and drawn as the view stacks them.',
417
+ `# Install ggjbrowse with ${INSTALL}`,
418
+ '#',
419
+ '# Each reader returns the tables its track draws; jbrowse_stack() draws',
420
+ '# them. Restyle every band with &, or draw one alone and add to it:',
421
+ '# fig & ggplot2::theme_classic()',
422
+ `# ${alignments ? `autoplot(${alignments}, "pileup")` : `autoplot(${drawn[0]})`} + ggplot2::theme_classic()`,
423
+ ...(notes.length
424
+ ? [
425
+ '#',
426
+ '# What this figure does not show:',
427
+ ...notes.map(n => `# - ${n}`),
428
+ ]
429
+ : []),
430
+ '',
431
+ 'if (',
432
+ ' !requireNamespace("ggjbrowse", quietly = TRUE) ||',
433
+ ` packageVersion("ggjbrowse") < ${rStr(GGJBROWSE_VERSION)}`,
434
+ ') {',
435
+ ` stop("this script needs ggjbrowse ${GGJBROWSE_VERSION} or later: ${INSTALL.replaceAll('"', '\\"')}")`,
436
+ '}',
437
+ 'library(ggjbrowse)',
438
+ '',
439
+ `locus <- ${lociLiteral(loci)}`,
440
+ ...(assemblyValue
441
+ ? [
442
+ "# The view's assembly: the reference every reader reads against, and",
443
+ '# the aliases that name each contig as each file spells it.',
444
+ `assembly <- ${assemblyValue}`,
445
+ ]
446
+ : []),
447
+ ...(widthReaders.length
448
+ ? [
449
+ "# The view's pixels across its regions. A smaller number lays the tracks",
450
+ '# out as a narrower browser window would, for a narrower figure with',
451
+ '# fewer labels at the same size.',
452
+ `width <- ${viewPx}`,
453
+ ]
454
+ : []),
455
+ '',
456
+ reads.join('\n\n'),
457
+ '',
458
+ `fig <- jbrowse_stack(${drawn.join(', ')})`,
459
+ 'print(fig)',
460
+ `ggplot2::ggsave(${rStr(out)}, fig, width = jbrowse_width(fig), height = jbrowse_height(fig))`,
461
+ '',
462
+ ].join('\n');
463
+ return { drawn: drawn.length, script, notes };
464
+ }
@@ -27,20 +27,17 @@ export async function renderToSvg(model, opts) {
27
27
  textHeight = defaultTextHeight(fontSize), rulerHeight = 34, trackLabels = 'offset', themeName, fontFamily, showGridlines = false, Wrapper, } = opts;
28
28
  const session = getSession(model);
29
29
  const theme = session.getActiveThemeOptions?.(themeName);
30
- // owns the two orderings this used to spell out by hand: legendWidth before
31
- // the awaits, tracksHeight after them. Every display's `renderSvg` owns its
30
+ // owns the ordering this used to spell out by hand: the stack's height and
31
+ // legend gutter measured after the awaits. Every display's `renderSvg` owns its
32
32
  // own readiness wait — an LGV display through `renderDisplaySvg`'s
33
33
  // `awaitSvgReady`, a non-LGV one (dotplot, synteny, circular) by calling that
34
34
  // itself.
35
35
  const { tracks, displayResults, tracksHeight, legendWidth, skippedTracks } = await renderViewTracks({
36
36
  view: model,
37
- opts,
37
+ opts: { ...opts, fontSize },
38
38
  theme,
39
39
  textHeight,
40
40
  trackLabels,
41
- // the standalone export is the one with room to give: it widens its
42
- // canvas below so a legend sits beside the plot rather than over it
43
- reserveLegendWidth: true,
44
41
  });
45
42
  notifySkippedSvgTracks(session, skippedTracks);
46
43
  // read after the displays' waits, and handed to SVGHeader rather than read
@@ -72,6 +69,6 @@ export async function renderToSvg(model, opts) {
72
69
  // header is the full one: cytoband overview and total-bp scalebar as
73
70
  // well as the assembly name and ruler. `tracksTop` is the height
74
71
  // getHeaderLayout gave it.
75
- header: _jsx(SVGHeader, { model: model, fontSize: fontSize, rulerHeight: rulerHeight, layout: headerLayout, showCytobands: effectiveShowCytobands }), fontSize: fontSize, textHeight: textHeight, trackLabels: trackLabels, trackLabelOffset: trackLabelOffset, contentTop: tracksTop, tracksHeight: tracksHeight, showGridlines: showGridlines, leftBuffer: exportMargin, legendWidth: legendWidth }) })),
72
+ header: _jsx(SVGHeader, { model: model, fontSize: fontSize, rulerHeight: rulerHeight, layout: headerLayout, showCytobands: effectiveShowCytobands }), fontSize: fontSize, textHeight: textHeight, trackLabels: trackLabels, trackLabelOffset: trackLabelOffset, contentTop: tracksTop, tracksHeight: tracksHeight, showGridlines: showGridlines, leftBuffer: exportMargin }) })),
76
73
  });
77
74
  }
@@ -7,7 +7,7 @@ import type { TrackLabelMode } from '@jbrowse/display-kit/types';
7
7
  * reserves `getRowHeaderLayout().bandHeight` above it for the header to draw
8
8
  * into, and the row is `rulerHeight + rendered.tracksHeight` tall below it.
9
9
  */
10
- export default function SVGStackedRow({ view, rendered, top, margin, fontSize, textHeight, rulerHeight, trackLabels, trackLabelOffset, showGridlines, legendWidth, showAssemblyName, showScalebar, }: {
10
+ export default function SVGStackedRow({ view, rendered, top, margin, fontSize, textHeight, rulerHeight, trackLabels, trackLabelOffset, showGridlines, showAssemblyName, showScalebar, }: {
11
11
  view: LinearGenomeViewModel;
12
12
  rendered: {
13
13
  displayResults: SvgDisplayResult[];
@@ -21,7 +21,6 @@ export default function SVGStackedRow({ view, rendered, top, margin, fontSize, t
21
21
  trackLabels: TrackLabelMode;
22
22
  trackLabelOffset: number;
23
23
  showGridlines: boolean;
24
- legendWidth?: number;
25
24
  showAssemblyName?: boolean;
26
25
  showScalebar?: boolean;
27
26
  }): import("react").JSX.Element;
@@ -7,6 +7,6 @@ import SVGView from './SVGView.js';
7
7
  * reserves `getRowHeaderLayout().bandHeight` above it for the header to draw
8
8
  * into, and the row is `rulerHeight + rendered.tracksHeight` tall below it.
9
9
  */
10
- export default function SVGStackedRow({ view, rendered, top, margin, fontSize, textHeight, rulerHeight, trackLabels, trackLabelOffset, showGridlines, legendWidth, showAssemblyName, showScalebar, }) {
11
- return (_jsx("g", { transform: `translate(${margin} ${top})`, children: _jsx(SVGView, { view: view, displayResults: rendered.displayResults, header: _jsx(SVGRowHeader, { view: view, fontSize: fontSize, rulerHeight: rulerHeight, showAssemblyName: showAssemblyName, showScalebar: showScalebar }), fontSize: fontSize, textHeight: textHeight, trackLabels: trackLabels, trackLabelOffset: trackLabelOffset, contentTop: rulerHeight, tracksHeight: rendered.tracksHeight, showGridlines: showGridlines, leftBuffer: margin, legendWidth: legendWidth }) }));
10
+ export default function SVGStackedRow({ view, rendered, top, margin, fontSize, textHeight, rulerHeight, trackLabels, trackLabelOffset, showGridlines, showAssemblyName, showScalebar, }) {
11
+ return (_jsx("g", { transform: `translate(${margin} ${top})`, children: _jsx(SVGView, { view: view, displayResults: rendered.displayResults, header: _jsx(SVGRowHeader, { view: view, fontSize: fontSize, rulerHeight: rulerHeight, showAssemblyName: showAssemblyName, showScalebar: showScalebar }), fontSize: fontSize, textHeight: textHeight, trackLabels: trackLabels, trackLabelOffset: trackLabelOffset, contentTop: rulerHeight, tracksHeight: rendered.tracksHeight, showGridlines: showGridlines, leftBuffer: margin }) }));
12
12
  }
@@ -1,10 +1,9 @@
1
1
  import type { TrackLabelMode } from '../types.ts';
2
- export default function SVGTrackLabel({ trackLabels, trackName, fontSize, textHeight, trackLabelOffset, x, raised, }: {
2
+ export default function SVGTrackLabel({ trackLabels, trackName, fontSize, textHeight, trackLabelOffset, x, }: {
3
3
  trackName: string;
4
4
  trackLabels: TrackLabelMode;
5
5
  fontSize: number;
6
6
  textHeight: number;
7
7
  trackLabelOffset: number;
8
8
  x: number;
9
- raised?: boolean;
10
9
  }): import("react").JSX.Element | null;
@@ -4,24 +4,20 @@ import { stripAlpha } from '@jbrowse/core/util';
4
4
  import { TRACK_LABEL_GAP, insetLabelBaselineY, offsetLabelBaselineY, } from './util.js';
5
5
  // Where the label sits, per mode. 'left' right-aligns in the gutter
6
6
  // trackLabelLeftOffset reserved (hence the shared TRACK_LABEL_GAP), beside the
7
- // first row or, `raised`, in the band above a track whose sidebar holds the
8
- // gutter beside its rows. The other modes hang off the leftmost visible content
7
+ // first row. The other modes hang off the leftmost visible content
9
8
  // at `x`, either just above the track body ('offset') or inset over it
10
9
  // ('overlapping').
11
10
  //
12
11
  // Every mode is placed on its alphabetic baseline (the SVG default, so no
13
- // dominantBaseline is emitted at all): 'offset' and a raised 'left' so their
14
- // descenders land a known distance above the track body — the band is
12
+ // dominantBaseline is emitted at all): 'offset' so its descenders land a known distance above the track body — the band is
15
13
  // `textHeight` tall, and offsetLabelBaselineY owns that arithmetic — and
16
14
  // 'left'/'overlapping' so their ascenders stay inside the track box they align
17
15
  // with.
18
- function labelPosition({ trackLabels, trackLabelOffset, textHeight, fontSize, x, raised, }) {
16
+ function labelPosition({ trackLabels, trackLabelOffset, textHeight, fontSize, x, }) {
19
17
  if (trackLabels === 'left') {
20
18
  return {
21
19
  x: trackLabelOffset - TRACK_LABEL_GAP,
22
- y: raised
23
- ? offsetLabelBaselineY(textHeight, fontSize)
24
- : insetLabelBaselineY(fontSize),
20
+ y: insetLabelBaselineY(fontSize),
25
21
  textAnchor: 'end',
26
22
  };
27
23
  }
@@ -34,7 +30,7 @@ function labelPosition({ trackLabels, trackLabelOffset, textHeight, fontSize, x,
34
30
  // stay inside it (at y=0 they rose into the track above)
35
31
  return { x: x + 5, y: insetLabelBaselineY(fontSize) };
36
32
  }
37
- export default function SVGTrackLabel({ trackLabels, trackName, fontSize, textHeight, trackLabelOffset, x, raised = false, }) {
33
+ export default function SVGTrackLabel({ trackLabels, trackName, fontSize, textHeight, trackLabelOffset, x, }) {
38
34
  const palette = usePalette();
39
35
  if (trackLabels === 'hidden') {
40
36
  return null;
@@ -45,7 +41,6 @@ export default function SVGTrackLabel({ trackLabels, trackName, fontSize, textHe
45
41
  textHeight,
46
42
  fontSize,
47
43
  x,
48
- raised,
49
44
  });
50
45
  // 'overlapping' prints over the track's own data, which the on-screen label
51
46
  // clears with a paper box, so it carries the halo text over a plot needs
@@ -2,7 +2,7 @@ import type { LinearGenomeViewModel } from '../index.ts';
2
2
  import type { TrackLabelMode } from '../types.ts';
3
3
  import type { SvgDisplayResult } from './util.ts';
4
4
  type LGV = LinearGenomeViewModel;
5
- export default function SVGTracks({ displayResults, model, textHeight, fontSize, trackLabels, trackLabelOffset, leftBuffer, legendWidth, }: {
5
+ export default function SVGTracks({ displayResults, model, textHeight, fontSize, trackLabels, trackLabelOffset, leftBuffer, }: {
6
6
  displayResults: SvgDisplayResult[];
7
7
  model: LGV;
8
8
  textHeight: number;
@@ -10,6 +10,5 @@ export default function SVGTracks({ displayResults, model, textHeight, fontSize,
10
10
  trackLabels: TrackLabelMode;
11
11
  trackLabelOffset: number;
12
12
  leftBuffer: number;
13
- legendWidth: number;
14
13
  }): import("react").JSX.Element;
15
14
  export {};
@@ -3,20 +3,20 @@ import { SvgClipRect } from '@jbrowse/core/svg/SvgExport';
3
3
  import { svgNodeId } from '@jbrowse/core/svg/svgId';
4
4
  import { svgTrackName } from '@jbrowse/core/svg/trackNames';
5
5
  import { getSession } from '@jbrowse/core/util';
6
- import { exportsTooLargeNote } from '@jbrowse/display-kit/renderDisplaySvg';
6
+ import { SvgLegend } from '@jbrowse/display-kit/renderDisplaySvg';
7
7
  import SVGRegionSeparators from './SVGRegionSeparators.js';
8
8
  import SVGTrackLabel from './SVGTrackLabel.js';
9
- import { labelOffset, leftLabelRaised, trackBoxOffsets, trackLabelMode, } from './util.js';
10
- export default function SVGTracks({ displayResults, model, textHeight, fontSize, trackLabels, trackLabelOffset, leftBuffer, legendWidth, }) {
9
+ import { labelOffset, trackBoxOffsets, trackLabelMode } from './util.js';
10
+ export default function SVGTracks({ displayResults, model, textHeight, fontSize, trackLabels, trackLabelOffset, leftBuffer, }) {
11
11
  const session = getSession(model);
12
12
  const x = Math.max(-model.offsetPx, 0);
13
13
  const offsets = trackBoxOffsets(displayResults.map(r => r.track), trackLabels, textHeight);
14
- return (_jsx(_Fragment, { children: displayResults.map(({ track, result }, i) => {
14
+ return (_jsx(_Fragment, { children: displayResults.map(({ track, result, legendHeight }, i) => {
15
15
  const conf = track.configuration;
16
16
  const trackName = svgTrackName(track, session);
17
17
  const display = track.displays[0];
18
18
  const currentOffset = offsets[i];
19
19
  const textOffset = labelOffset(track, trackLabels, textHeight);
20
- return (_jsxs("g", { transform: `translate(0 ${currentOffset})`, children: [_jsx(SvgClipRect, { id: `track-clip-${svgNodeId(model)}-${conf.trackId}`, x: -leftBuffer, y: textOffset, width: model.width + trackLabelOffset + leftBuffer + legendWidth, height: display.height, children: _jsxs("g", { transform: `translate(${trackLabelOffset} ${textOffset})`, children: [result, exportsTooLargeNote(display) ? null : (_jsx(SVGRegionSeparators, { model: model, height: display.height }))] }) }), _jsx(SVGTrackLabel, { trackName: trackName, fontSize: fontSize, textHeight: textHeight, trackLabels: trackLabelMode(track, trackLabels), trackLabelOffset: trackLabelOffset, raised: leftLabelRaised(track, trackLabels), x: x + trackLabelOffset })] }, conf.trackId));
20
+ return (_jsxs("g", { transform: `translate(0 ${currentOffset})`, children: [_jsx(SvgClipRect, { id: `track-clip-${svgNodeId(model)}-${conf.trackId}`, x: -leftBuffer, y: textOffset, width: model.width + trackLabelOffset + leftBuffer, height: display.height, children: _jsxs("g", { transform: `translate(${trackLabelOffset} ${textOffset})`, children: [result, display.regionTooLarge ? null : (_jsx(SVGRegionSeparators, { model: model, height: display.height }))] }) }), legendHeight === undefined ? null : (_jsx("g", { transform: `translate(${trackLabelOffset} ${textOffset})`, children: _jsx(SvgLegend, { model: display, width: model.width, height: legendHeight }) })), _jsx(SVGTrackLabel, { trackName: trackName, fontSize: fontSize, textHeight: textHeight, trackLabels: trackLabelMode(track, trackLabels), trackLabelOffset: trackLabelOffset, x: x + trackLabelOffset })] }, conf.trackId));
21
21
  }) }));
22
22
  }
@@ -2,7 +2,7 @@ import type { LinearGenomeViewModel } from '../index.ts';
2
2
  import type { TrackLabelMode } from '../types.ts';
3
3
  import type { SvgDisplayResult } from './util.ts';
4
4
  import type { ReactNode } from 'react';
5
- export default function SVGView({ view, displayResults, header, fontSize, textHeight, trackLabels, trackLabelOffset, contentTop, tracksHeight, showGridlines, leftBuffer, legendWidth, }: {
5
+ export default function SVGView({ view, displayResults, header, fontSize, textHeight, trackLabels, trackLabelOffset, contentTop, tracksHeight, showGridlines, leftBuffer, }: {
6
6
  view: LinearGenomeViewModel;
7
7
  displayResults: SvgDisplayResult[];
8
8
  header: ReactNode;
@@ -14,5 +14,4 @@ export default function SVGView({ view, displayResults, header, fontSize, textHe
14
14
  tracksHeight: number;
15
15
  showGridlines: boolean;
16
16
  leftBuffer: number;
17
- legendWidth?: number;
18
17
  }): import("react").JSX.Element;
@@ -7,7 +7,7 @@ import SVGTracks from './SVGTracks.js';
7
7
  // Shared by all three exports that stack an LGV — the standalone one, and the
8
8
  // linear-synteny and breakpoint-split rows — so their per-view layout can't
9
9
  // drift. It did drift while the standalone export kept its own copy of these
10
- // four groups: only that copy widened the per-track clip by `legendWidth`.
11
- export default function SVGView({ view, displayResults, header, fontSize, textHeight, trackLabels, trackLabelOffset, contentTop, tracksHeight, showGridlines, leftBuffer, legendWidth = 0, }) {
12
- return (_jsxs(_Fragment, { children: [_jsx("g", { transform: `translate(${trackLabelOffset})`, children: header }), showGridlines ? (_jsx("g", { transform: `translate(${trackLabelOffset} ${contentTop})`, children: _jsx(SVGGridlines, { model: view, height: tracksHeight }) })) : null, _jsx("g", { transform: `translate(0 ${contentTop})`, children: _jsx(SVGTracks, { textHeight: textHeight, trackLabels: trackLabels, fontSize: fontSize, model: view, displayResults: displayResults, trackLabelOffset: trackLabelOffset, leftBuffer: leftBuffer, legendWidth: legendWidth }) }), _jsx("g", { transform: `translate(${trackLabelOffset} ${contentTop})`, children: _jsx(SVGHighlightsOverlay, { model: view, tracksHeight: tracksHeight }) })] }));
10
+ // four groups.
11
+ export default function SVGView({ view, displayResults, header, fontSize, textHeight, trackLabels, trackLabelOffset, contentTop, tracksHeight, showGridlines, leftBuffer, }) {
12
+ return (_jsxs(_Fragment, { children: [_jsx("g", { transform: `translate(${trackLabelOffset})`, children: header }), showGridlines ? (_jsx("g", { transform: `translate(${trackLabelOffset} ${contentTop})`, children: _jsx(SVGGridlines, { model: view, height: tracksHeight }) })) : null, _jsx("g", { transform: `translate(0 ${contentTop})`, children: _jsx(SVGTracks, { textHeight: textHeight, trackLabels: trackLabels, fontSize: fontSize, model: view, displayResults: displayResults, trackLabelOffset: trackLabelOffset, leftBuffer: leftBuffer }) }), _jsx("g", { transform: `translate(${trackLabelOffset} ${contentTop})`, children: _jsx(SVGHighlightsOverlay, { model: view, tracksHeight: tracksHeight }) })] }));
13
13
  }