@jbrowse/plugin-linear-genome-view 5.0.0-beta.13 → 5.0.0-beta.16

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 (63) 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/HighlightBand.d.ts +3 -2
  9. package/esm/LinearGenomeView/components/HighlightBand.js +4 -2
  10. package/esm/LinearGenomeView/components/HighlightChip.d.ts +3 -2
  11. package/esm/LinearGenomeView/components/HighlightChip.js +4 -2
  12. package/esm/LinearGenomeView/components/LinearGenomeView.js +5 -12
  13. package/esm/LinearGenomeView/components/LinearGenomeViewContainer.js +4 -5
  14. package/esm/LinearGenomeView/components/OverviewHighlightBand.d.ts +3 -2
  15. package/esm/LinearGenomeView/components/OverviewHighlightBand.js +4 -2
  16. package/esm/LinearGenomeView/components/RegionWidthEditorDialog.d.ts +3 -2
  17. package/esm/LinearGenomeView/components/RegionWidthEditorDialog.js +4 -4
  18. package/esm/LinearGenomeView/components/ScalebarRefNameLabels.js +4 -4
  19. package/esm/LinearGenomeView/components/SearchPanelForm.d.ts +3 -2
  20. package/esm/LinearGenomeView/components/SearchPanelForm.js +4 -2
  21. package/esm/LinearGenomeView/components/SearchResultsDialog.d.ts +4 -9
  22. package/esm/LinearGenomeView/components/SearchResultsDialog.js +7 -4
  23. package/esm/LinearGenomeView/components/SearchResultsTable.d.ts +3 -8
  24. package/esm/LinearGenomeView/components/SearchResultsTable.js +4 -32
  25. package/esm/LinearGenomeView/components/StrandCheckboxes.d.ts +3 -2
  26. package/esm/LinearGenomeView/components/StrandCheckboxes.js +4 -2
  27. package/esm/LinearGenomeView/components/TrackContainer.js +1 -1
  28. package/esm/LinearGenomeView/components/TrackLabelMenu.js +8 -12
  29. package/esm/LinearGenomeView/components/useRangeSelect.js +3 -2
  30. package/esm/LinearGenomeView/components/useSearchBoxPrefs.d.ts +3 -1
  31. package/esm/LinearGenomeView/components/useSearchBoxPrefs.js +4 -2
  32. package/esm/LinearGenomeView/components/useSideScroll.js +11 -12
  33. package/esm/LinearGenomeView/lazyDialogs.d.ts +17 -2
  34. package/esm/LinearGenomeView/menuItems.js +32 -24
  35. package/esm/LinearGenomeView/model.d.ts +197 -151
  36. package/esm/LinearGenomeView/model.js +203 -201
  37. package/esm/LinearGenomeView/rExport/exportLinearView.d.ts +13 -0
  38. package/esm/LinearGenomeView/rExport/exportLinearView.js +98 -0
  39. package/esm/LinearGenomeView/rExport/rScript.d.ts +75 -0
  40. package/esm/LinearGenomeView/rExport/rScript.js +465 -0
  41. package/esm/LinearGenomeView/svgcomponents/SVGLinearGenomeView.js +3 -6
  42. package/esm/LinearGenomeView/svgcomponents/SVGStackedRow.d.ts +1 -2
  43. package/esm/LinearGenomeView/svgcomponents/SVGStackedRow.js +2 -2
  44. package/esm/LinearGenomeView/svgcomponents/SVGTracks.d.ts +1 -2
  45. package/esm/LinearGenomeView/svgcomponents/SVGTracks.js +4 -4
  46. package/esm/LinearGenomeView/svgcomponents/SVGView.d.ts +1 -2
  47. package/esm/LinearGenomeView/svgcomponents/SVGView.js +3 -3
  48. package/esm/LinearGenomeView/svgcomponents/renderViewTracks.d.ts +22 -20
  49. package/esm/LinearGenomeView/svgcomponents/renderViewTracks.js +42 -29
  50. package/esm/LinearGenomeView/svgcomponents/useViewSvgFigure.js +6 -11
  51. package/esm/LinearGenomeView/svgcomponents/util.d.ts +2 -1
  52. package/esm/LinearGenomeView/util.d.ts +42 -0
  53. package/esm/LinearGenomeView/util.js +81 -0
  54. package/esm/MultiLevelRubberband/MultiLevelRubberband.js +2 -2
  55. package/esm/displayKitTests/perRegionTestEnv.d.ts +45 -12
  56. package/esm/index.d.ts +4 -2
  57. package/esm/index.js +3 -1
  58. package/esm/searchUtils.js +1 -0
  59. package/esm/shared/RubberbandSpan.d.ts +3 -2
  60. package/esm/shared/RubberbandSpan.js +4 -2
  61. package/esm/shared/coordLabels.d.ts +6 -6
  62. package/esm/shared/coordLabels.js +7 -6
  63. package/package.json +9 -9
@@ -0,0 +1,13 @@
1
+ import type { LinearGenomeViewModel } from '../model.ts';
2
+ /** The R script that redraws the tracks open in the view, over its visible regions. */
3
+ export declare function exportLinearViewToR(view: LinearGenomeViewModel, out?: string): {
4
+ drawn: number;
5
+ script: undefined;
6
+ notes: string[];
7
+ } | {
8
+ drawn: number;
9
+ script: string;
10
+ notes: string[];
11
+ };
12
+ /** Downloads the view's R script, telling the user what it leaves out. */
13
+ export declare function downloadRScript(view: LinearGenomeViewModel): Promise<void>;
@@ -0,0 +1,98 @@
1
+ import { fullConfSnapshot, readConfObject } from '@jbrowse/core/configuration';
2
+ import { getSession } from '@jbrowse/core/util';
3
+ import { getSnapshot } from '@jbrowse/mobx-state-tree';
4
+ import { exportTracksToR } from './rScript.js';
5
+ function trackConf(track) {
6
+ return track.configuration;
7
+ }
8
+ /** The regions on screen, each displayed region cut to what the view shows of it. */
9
+ function viewRegions(view) {
10
+ const blocks = view.dynamicBlocks.contentBlocks;
11
+ if (blocks.length === 0) {
12
+ return view.displayedRegions.map(r => ({
13
+ refName: r.refName,
14
+ start: Math.floor(r.start),
15
+ end: Math.ceil(r.end),
16
+ }));
17
+ }
18
+ const byRegion = new Map();
19
+ for (const block of blocks) {
20
+ const key = block.displayedRegionIndex ?? -1;
21
+ const held = byRegion.get(key);
22
+ if (held?.refName === block.refName) {
23
+ held.start = Math.min(held.start, Math.floor(block.start));
24
+ held.end = Math.max(held.end, Math.ceil(block.end));
25
+ }
26
+ else {
27
+ byRegion.set(key, {
28
+ refName: block.refName,
29
+ start: Math.floor(block.start),
30
+ end: Math.ceil(block.end),
31
+ });
32
+ }
33
+ }
34
+ return [...byRegion.values()];
35
+ }
36
+ /** The view's assembly config as written. */
37
+ function assemblyOf(view) {
38
+ const name = view.assemblyNames[0];
39
+ const configuration = name
40
+ ? getSession(view).assemblyManager.get(name)?.configuration
41
+ : undefined;
42
+ return configuration
43
+ ? getSnapshot(configuration)
44
+ : undefined;
45
+ }
46
+ function tracksOf(view) {
47
+ const tracks = [];
48
+ for (const track of view.tracks) {
49
+ const display = track.displays[0];
50
+ if (display) {
51
+ const { type, displayId, ...written } = getSnapshot(display.configuration);
52
+ const { heightMode, height } = fullConfSnapshot(display.configuration);
53
+ tracks.push({
54
+ trackId: track.configuration.trackId,
55
+ trackName: readConfObject(trackConf(track), 'name') ||
56
+ track.configuration.trackId,
57
+ displayType: display.type,
58
+ adapter: readConfObject(trackConf(track), 'adapter'),
59
+ display: written,
60
+ heightMode,
61
+ height,
62
+ });
63
+ }
64
+ }
65
+ return tracks;
66
+ }
67
+ /** The R script that redraws the tracks open in the view, over its visible regions. */
68
+ export function exportLinearViewToR(view, out) {
69
+ return exportTracksToR({
70
+ tracks: tracksOf(view),
71
+ regions: viewRegions(view),
72
+ assembly: assemblyOf(view),
73
+ bpPerPx: view.bpPerPx,
74
+ out,
75
+ notes: view.displayedRegions.some(r => r.reversed)
76
+ ? ['view: shown right to left, where the figure runs left to right']
77
+ : [],
78
+ });
79
+ }
80
+ /** Downloads the view's R script, telling the user what it leaves out. */
81
+ export async function downloadRScript(view) {
82
+ const session = getSession(view);
83
+ try {
84
+ const { saveAs } = await import('@jbrowse/core/util/FileSaver');
85
+ const { script, notes } = exportLinearViewToR(view);
86
+ if (!script) {
87
+ session.notify(`No open track has an R export. ${notes.join('; ')}`, 'warning');
88
+ return;
89
+ }
90
+ saveAs(new Blob([script], { type: 'text/plain;charset=utf-8' }), 'jbrowse_view.R');
91
+ if (notes.length) {
92
+ session.notify(`Exported with ${notes.length} note${notes.length === 1 ? '' : 's'} in the script header`, 'info');
93
+ }
94
+ }
95
+ catch (e) {
96
+ session.notifyError(`${e}`, e);
97
+ }
98
+ }
@@ -0,0 +1,75 @@
1
+ /** A view's region, 0-based half-open, on the assembly's canonical contig. */
2
+ export interface Region {
3
+ refName: string;
4
+ start: number;
5
+ end: number;
6
+ }
7
+ export interface RExportTrack {
8
+ trackId: string;
9
+ trackName: string;
10
+ displayType: string;
11
+ adapter: Record<string, unknown>;
12
+ /** The display's config as written, its defaults left out, which the engine fills from the same schema. */
13
+ display: Record<string, unknown>;
14
+ /** The display's resolved sizing, which a feature track exports so the figure drops what the view drops. */
15
+ heightMode?: unknown;
16
+ height?: unknown;
17
+ }
18
+ /** The ggjbrowse release whose readers and `jbrowse_stack` the script calls. */
19
+ export declare const GGJBROWSE_VERSION = "0.10.0";
20
+ /**
21
+ * A resolved config writes an unset slot as `''`, `[]` or `{}`, which a reader
22
+ * would take for a setting. Keeps `false` and `0`, which are values.
23
+ */
24
+ export declare function withoutUnset(value: unknown): unknown;
25
+ export declare function rStr(s: string): string;
26
+ /**
27
+ * A config value as the R literal ggjbrowse hands the engine: a `list` per
28
+ * object, `c()` per vector of scalars, `list()` per vector of objects and per
29
+ * empty vector, which jsonlite would otherwise write as `{}`.
30
+ */
31
+ export declare function rValue(value: unknown): string;
32
+ /**
33
+ * The path or URL a JBrowse file location points at, as R opens it: a `uri`
34
+ * resolved against its `baseUri` as the browser's fetch resolved it, a
35
+ * `file://` URL as its path, or a `localPath`.
36
+ */
37
+ export declare function locationPath(location: unknown): string | undefined;
38
+ /**
39
+ * The view's assembly as the R value every reader takes: `jbrowse_assembly()`
40
+ * of its sequence and alias files where ggjbrowse builds the same config from
41
+ * their paths, and otherwise the config itself, its locations paths R opens.
42
+ */
43
+ export declare function assemblyArgument(snapshot: Record<string, unknown>): {
44
+ value: string;
45
+ } | {
46
+ skipped: string;
47
+ };
48
+ /** A region as JBrowse's location box writes it, 1-based and inclusive. */
49
+ export declare function locusOf(refName: string, start: number, end: number): string;
50
+ /**
51
+ * One R script for a view's tracks over its regions: a ggjbrowse reader per
52
+ * track, reading each as its display does, then `jbrowse_stack()` drawing
53
+ * them as the view stacks them. A track ggjbrowse cannot read lands in the
54
+ * header instead of stopping the figure: a script that quietly differs from
55
+ * its view says so.
56
+ */
57
+ export declare function exportTracksToR({ tracks, regions, assembly: assemblySnapshot, bpPerPx, out, notes: viewNotes, }: {
58
+ tracks: readonly RExportTrack[];
59
+ regions: readonly Region[];
60
+ /** The view's assembly config as written, which names its sequence and its contigs' aliases. */
61
+ assembly?: Record<string, unknown>;
62
+ /** The view's zoom, which decides what a feature or quantitative display shows. */
63
+ bpPerPx?: number;
64
+ out?: string;
65
+ /** What the caller knows of the view that the figure does not show. */
66
+ notes?: readonly string[];
67
+ }): {
68
+ drawn: number;
69
+ script: undefined;
70
+ notes: string[];
71
+ } | {
72
+ drawn: number;
73
+ script: string;
74
+ notes: string[];
75
+ };
@@ -0,0 +1,465 @@
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
+ GtfTabixAdapter: { slot: 'gtfGzLocation', ext: /\.gtf\.gz$/ },
15
+ GtfAdapter: { slot: 'gtfLocation', ext: /\.gtf$/ },
16
+ BedTabixAdapter: { slot: 'bedGzLocation', ext: /\.bed\.gz$/ },
17
+ BedAdapter: { slot: 'bedLocation', ext: /\.bed$/ },
18
+ BigBedAdapter: { slot: 'bigBedLocation', ext: /\.(bb|bigbed)$/ },
19
+ },
20
+ },
21
+ LinearVariantDisplay: {
22
+ call: 'jbrowse_features',
23
+ adapters: {
24
+ VcfTabixAdapter: { slot: 'vcfGzLocation', ext: /\.vcf\.gz$/ },
25
+ VcfAdapter: { slot: 'vcfLocation', ext: /\.vcf$/ },
26
+ },
27
+ },
28
+ LinearMultiSampleVariantDisplay: {
29
+ call: 'jbrowse_variants',
30
+ adapters: {
31
+ VcfTabixAdapter: { slot: 'vcfGzLocation', ext: /\.vcf\.gz$/ },
32
+ VcfAdapter: { slot: 'vcfLocation', ext: /\.vcf$/ },
33
+ },
34
+ },
35
+ LinearWiggleDisplay: {
36
+ call: 'jbrowse_wiggle',
37
+ adapters: {
38
+ BigWigAdapter: { slot: 'bigWigLocation', ext: /$/ },
39
+ MultiWiggleAdapter: {},
40
+ },
41
+ },
42
+ LinearAlignmentsDisplay: {
43
+ call: 'jbrowse_alignments',
44
+ adapters: {
45
+ BamAdapter: { slot: 'bamLocation', ext: /(?<!\.cram)$/ },
46
+ CramAdapter: { slot: 'cramLocation', ext: /\.cram$/ },
47
+ },
48
+ },
49
+ };
50
+ /** The ggjbrowse release whose readers and `jbrowse_stack` the script calls. */
51
+ export const GGJBROWSE_VERSION = '0.10.0';
52
+ const INSTALL = 'remotes::install_github("GMOD/ggjbrowse")';
53
+ function isSet(value) {
54
+ return !(value === undefined ||
55
+ value === null ||
56
+ value === '' ||
57
+ (Array.isArray(value) && value.length === 0) ||
58
+ (typeof value === 'object' &&
59
+ !Array.isArray(value) &&
60
+ Object.keys(value).length === 0));
61
+ }
62
+ /**
63
+ * A resolved config writes an unset slot as `''`, `[]` or `{}`, which a reader
64
+ * would take for a setting. Keeps `false` and `0`, which are values.
65
+ */
66
+ export function withoutUnset(value) {
67
+ if (Array.isArray(value)) {
68
+ return value.map(withoutUnset);
69
+ }
70
+ if (value && typeof value === 'object') {
71
+ return Object.fromEntries(Object.entries(value)
72
+ .map(([k, v]) => [k, withoutUnset(v)])
73
+ .filter(([, v]) => isSet(v)));
74
+ }
75
+ return value;
76
+ }
77
+ export function rStr(s) {
78
+ return JSON.stringify(s);
79
+ }
80
+ const RESERVED = new Set([
81
+ 'if',
82
+ 'else',
83
+ 'repeat',
84
+ 'while',
85
+ 'function',
86
+ 'for',
87
+ 'next',
88
+ 'break',
89
+ 'in',
90
+ 'TRUE',
91
+ 'FALSE',
92
+ 'NULL',
93
+ 'Inf',
94
+ 'NaN',
95
+ 'NA',
96
+ ]);
97
+ function isIdentifier(s) {
98
+ return /^[A-Za-z.][A-Za-z0-9._]*$/.test(s) && !RESERVED.has(s);
99
+ }
100
+ /**
101
+ * A config value as the R literal ggjbrowse hands the engine: a `list` per
102
+ * object, `c()` per vector of scalars, `list()` per vector of objects and per
103
+ * empty vector, which jsonlite would otherwise write as `{}`.
104
+ */
105
+ export function rValue(value) {
106
+ if (value === null || value === undefined) {
107
+ return 'NULL';
108
+ }
109
+ if (typeof value === 'string') {
110
+ return rStr(value);
111
+ }
112
+ if (typeof value === 'number' || typeof value === 'boolean') {
113
+ return typeof value === 'number' ? String(value) : value ? 'TRUE' : 'FALSE';
114
+ }
115
+ if (Array.isArray(value)) {
116
+ const items = value.map(rValue).join(', ');
117
+ return value.length && value.every(v => typeof v !== 'object')
118
+ ? `c(${items})`
119
+ : `list(${items})`;
120
+ }
121
+ return `list(${Object.entries(value)
122
+ .map(([k, v]) => `${isIdentifier(k) ? k : rStr(k)} = ${rValue(v)}`)
123
+ .join(', ')})`;
124
+ }
125
+ /**
126
+ * The path or URL a JBrowse file location points at, as R opens it: a `uri`
127
+ * resolved against its `baseUri` as the browser's fetch resolved it, a
128
+ * `file://` URL as its path, or a `localPath`.
129
+ */
130
+ export function locationPath(location) {
131
+ const loc = location;
132
+ if (!loc?.uri) {
133
+ return loc?.localPath;
134
+ }
135
+ const resolved = resolveUri({ uri: loc.uri, baseUri: loc.baseUri });
136
+ return resolved.startsWith('file://')
137
+ ? decodeURIComponent(new URL(resolved).pathname)
138
+ : resolved;
139
+ }
140
+ function isLocation(v) {
141
+ return (typeof v === 'object' &&
142
+ v !== null &&
143
+ !Array.isArray(v) &&
144
+ (typeof v.uri === 'string' ||
145
+ typeof v.localPath === 'string'));
146
+ }
147
+ /** An adapter config with each location a path or URL R opens, each path also pushed onto `uris`. */
148
+ function openable(value, uris) {
149
+ if (isLocation(value)) {
150
+ const uri = locationPath(value);
151
+ if (uri) {
152
+ uris.push(uri);
153
+ }
154
+ return { uri };
155
+ }
156
+ if (Array.isArray(value)) {
157
+ return value.map(v => openable(v, uris));
158
+ }
159
+ if (value && typeof value === 'object') {
160
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, openable(v, uris)]));
161
+ }
162
+ return value;
163
+ }
164
+ /**
165
+ * The file argument of a track's reader: its path where ggjbrowse would choose
166
+ * the track's adapter for it from the extension, and the adapter config itself
167
+ * where the adapter carries a setting or the name hides its format, so every
168
+ * setting reaches the engine as the browser has it.
169
+ */
170
+ function fileArgument(adapters, adapter) {
171
+ const type = String(adapter.type);
172
+ const known = adapters[type];
173
+ if (!known) {
174
+ return { skipped: `${type} has no R reader for this display` };
175
+ }
176
+ const uris = [];
177
+ const located = withoutUnset(openable(adapter, uris));
178
+ if (uris.length === 0) {
179
+ return { skipped: `${type} names no file` };
180
+ }
181
+ if (uris.some(u => u.startsWith('blob:'))) {
182
+ return {
183
+ skipped: 'it reads a file opened in the browser, which R cannot reach',
184
+ };
185
+ }
186
+ const file = known.slot
187
+ ? (locationPath(adapter[known.slot]) ??
188
+ (typeof adapter.uri === 'string' ? adapter.uri : undefined))
189
+ : undefined;
190
+ const index = adapter.index;
191
+ const indexPath = locationPath(index?.location) ?? locationPath(adapter.craiLocation);
192
+ const besideSuffix = type === 'CramAdapter' ? '.crai' : type === 'BamAdapter' ? '.bai' : '.tbi';
193
+ const plainSlots = new Set([
194
+ 'type',
195
+ 'uri',
196
+ 'index',
197
+ 'craiLocation',
198
+ known.slot,
199
+ ]);
200
+ const plain = file !== undefined &&
201
+ known.ext?.test(file.replace(/\?.*$/, '').toLowerCase()) &&
202
+ Object.keys(located).every(k => plainSlots.has(k)) &&
203
+ Object.keys(index ?? {}).every(k => k === 'location' || k === 'indexType') &&
204
+ (index?.indexType === 'CSI') === !!indexPath?.endsWith('.csi');
205
+ return plain
206
+ ? {
207
+ file: rStr(file),
208
+ index: indexPath && indexPath !== `${file}${besideSuffix}`
209
+ ? indexPath
210
+ : undefined,
211
+ uris,
212
+ }
213
+ : { file: rValue(located), uris };
214
+ }
215
+ /**
216
+ * Each sequence adapter `jbrowse_assembly()` builds from a path, by the
217
+ * extension ggjbrowse reads it off, and the index each takes beside it.
218
+ */
219
+ const SEQUENCE_ADAPTERS = {
220
+ TwoBitAdapter: { slot: 'twoBitLocation', ext: /\.2bit$/, beside: {} },
221
+ BgzipFastaAdapter: {
222
+ slot: 'fastaLocation',
223
+ ext: /\.gz$/,
224
+ beside: { faiLocation: '.fai', gziLocation: '.gzi' },
225
+ },
226
+ IndexedFastaAdapter: {
227
+ slot: 'fastaLocation',
228
+ ext: /^(?!.*\.(2bit|gz)$)/,
229
+ beside: { faiLocation: '.fai' },
230
+ },
231
+ };
232
+ /** The path of a sequence adapter `jbrowse_assembly()` rebuilds from that path alone. */
233
+ function plainSequence(adapter) {
234
+ const { type, uri, baseUri, ...slots } = adapter;
235
+ if (type === undefined && typeof uri === 'string') {
236
+ return Object.keys(slots).length
237
+ ? undefined
238
+ : locationPath({ uri, baseUri });
239
+ }
240
+ const known = SEQUENCE_ADAPTERS[String(type)];
241
+ const file = known && uri === undefined && locationPath(slots[known.slot]);
242
+ return file &&
243
+ known.ext.test(file.replace(/\?.*$/, '').toLowerCase()) &&
244
+ Object.entries(slots).every(([k, v]) => k === known.slot ||
245
+ (known.beside[k] !== undefined &&
246
+ locationPath(v) === `${file}${known.beside[k]}`))
247
+ ? file
248
+ : undefined;
249
+ }
250
+ /** The path of an alias adapter ggjbrowse reads as that path alone. */
251
+ function plainAliases(adapter) {
252
+ const { type, uri, baseUri, location, ...rest } = adapter;
253
+ const file = typeof uri === 'string'
254
+ ? locationPath({ uri, baseUri })
255
+ : locationPath(location);
256
+ return file &&
257
+ Object.keys(rest).length === 0 &&
258
+ type === refNameAliasAdapterType(file)
259
+ ? file
260
+ : undefined;
261
+ }
262
+ /**
263
+ * The view's assembly as the R value every reader takes: `jbrowse_assembly()`
264
+ * of its sequence and alias files where ggjbrowse builds the same config from
265
+ * their paths, and otherwise the config itself, its locations paths R opens.
266
+ */
267
+ export function assemblyArgument(snapshot) {
268
+ const config = expandAssemblySequence(snapshot);
269
+ const name = String(config.name);
270
+ const sequence = withoutUnset(config.sequence?.adapter);
271
+ if (!sequence) {
272
+ return { skipped: 'the assembly names no sequence' };
273
+ }
274
+ const aliasConfig = config.refNameAliases;
275
+ const aliases = aliasConfig
276
+ ? withoutUnset(expandSidecarShorthand(aliasConfig, refNameAliasAdapterType).adapter)
277
+ : undefined;
278
+ const uris = [];
279
+ const located = {
280
+ name,
281
+ sequence: { adapter: openable(sequence, uris) },
282
+ ...(aliases
283
+ ? { refNameAliases: { adapter: openable(aliases, uris) } }
284
+ : {}),
285
+ };
286
+ if (uris.some(u => u.startsWith('blob:'))) {
287
+ return {
288
+ skipped: 'the assembly reads a file opened in the browser, which R cannot reach',
289
+ };
290
+ }
291
+ const sequenceFile = plainSequence(sequence);
292
+ const aliasFile = aliases && plainAliases(aliases);
293
+ return sequenceFile && (!aliases || aliasFile)
294
+ ? {
295
+ value: `jbrowse_assembly(${[
296
+ rStr(sequenceFile),
297
+ ...(aliasFile ? [rStr(aliasFile)] : []),
298
+ `name = ${rStr(name)}`,
299
+ ].join(', ')})`,
300
+ }
301
+ : { value: rValue(located) };
302
+ }
303
+ /** A region as JBrowse's location box writes it, 1-based and inclusive. */
304
+ export function locusOf(refName, start, end) {
305
+ const n = (x) => Math.round(x).toLocaleString('en-US');
306
+ return `${refName}:${n(start + 1)}-${n(end)}`;
307
+ }
308
+ function lociLiteral(loci) {
309
+ return loci.length === 1
310
+ ? rStr(loci[0])
311
+ : `c(${loci.map(l => rStr(l)).join(', ')})`;
312
+ }
313
+ /** An R-safe name per track id, unique within the script. */
314
+ function trackNames(tracks) {
315
+ const taken = new Set(['locus', 'width', 'assembly', 'fig']);
316
+ return tracks.map(track => {
317
+ let base = track.trackId
318
+ .replaceAll(/[^a-zA-Z0-9]/g, '_')
319
+ .replace(/^(?=[\d_])/, 'x');
320
+ if (!isIdentifier(base)) {
321
+ base = `${base}_track`;
322
+ }
323
+ let name = base;
324
+ for (let n = 2; taken.has(name); n++) {
325
+ name = `${base}_${n}`;
326
+ }
327
+ taken.add(name);
328
+ return name;
329
+ });
330
+ }
331
+ /** What the figure cannot show of a track whose settings ggjbrowse reads. */
332
+ function settingNotes(track) {
333
+ const color = track.display.color;
334
+ return [
335
+ ...(color?.field === 'mateRefName'
336
+ ? [
337
+ 'color: mateRefName colors each contig by its hash, where the browser orders them by the assembly',
338
+ ]
339
+ : []),
340
+ ];
341
+ }
342
+ /**
343
+ * One R script for a view's tracks over its regions: a ggjbrowse reader per
344
+ * track, reading each as its display does, then `jbrowse_stack()` drawing
345
+ * them as the view stacks them. A track ggjbrowse cannot read lands in the
346
+ * header instead of stopping the figure: a script that quietly differs from
347
+ * its view says so.
348
+ */
349
+ export function exportTracksToR({ tracks, regions, assembly: assemblySnapshot, bpPerPx, out = 'figure.png', notes: viewNotes = [], }) {
350
+ const notes = [...viewNotes];
351
+ const assembly = assemblySnapshot && assemblyArgument(assemblySnapshot);
352
+ if (assembly && 'skipped' in assembly) {
353
+ notes.push(`assembly: ${assembly.skipped}`);
354
+ }
355
+ const assemblyValue = assembly && 'value' in assembly ? assembly.value : undefined;
356
+ const loci = regions.map(r => locusOf(r.refName, r.start, r.end));
357
+ const totalBp = regions.reduce((sum, r) => sum + r.end - r.start, 0);
358
+ const viewPx = bpPerPx ? Math.round(totalBp / bpPerPx) : undefined;
359
+ const widthReaders = [];
360
+ const names = trackNames(tracks);
361
+ const reads = [];
362
+ const drawn = [];
363
+ let alignments;
364
+ tracks.forEach((track, i) => {
365
+ const reader = READERS[track.displayType];
366
+ if (!reader) {
367
+ notes.push(`${track.trackName}: ${track.displayType} has no R reader in ggjbrowse yet`);
368
+ return;
369
+ }
370
+ const source = fileArgument(reader.adapters, track.adapter);
371
+ if ('skipped' in source) {
372
+ notes.push(`${track.trackName}: ${source.skipped}`);
373
+ return;
374
+ }
375
+ const { heightMode, height } = track;
376
+ const display = reader.call === 'jbrowse_features'
377
+ ? {
378
+ ...track.display,
379
+ ...(heightMode === undefined ? {} : { heightMode }),
380
+ ...(height === undefined ? {} : { height }),
381
+ }
382
+ : track.display;
383
+ const width = viewPx !== undefined;
384
+ const args = [
385
+ ...(Object.keys(display).length ? [`display = ${rValue(display)}`] : []),
386
+ ...(width ? ['width = width'] : []),
387
+ ...(assemblyValue ? ['assembly = assembly'] : []),
388
+ ...(source.index ? [`index = ${rStr(source.index)}`] : []),
389
+ ];
390
+ const name = names[i];
391
+ if (width) {
392
+ widthReaders.push(name);
393
+ }
394
+ reads.push([
395
+ `# ${track.trackName}`,
396
+ `${name} <- ${reader.call}(${source.file},`,
397
+ ` ${['locus', ...args].join(',\n ')})`,
398
+ ].join('\n'));
399
+ drawn.push(name);
400
+ if (reader.call === 'jbrowse_alignments') {
401
+ alignments ??= name;
402
+ }
403
+ notes.push(...[
404
+ ...(source.uris.some(u => u.includes('://') && u.includes('?'))
405
+ ? [
406
+ 'source: a URL carries its query string, credential included, so the script is not one to share',
407
+ ]
408
+ : []),
409
+ ...settingNotes(track),
410
+ ].map(n => `${track.trackName}: ${n}`));
411
+ });
412
+ if (drawn.length === 0) {
413
+ return { drawn: 0, script: undefined, notes };
414
+ }
415
+ const script = [
416
+ "# Generated by JBrowse 2: the view's tracks, read by ggjbrowse, JBrowse's",
417
+ '# own engine in R, and drawn as the view stacks them.',
418
+ `# Install ggjbrowse with ${INSTALL}`,
419
+ '#',
420
+ '# Each reader returns the tables its track draws; jbrowse_stack() draws',
421
+ '# them. Restyle every band with &, or draw one alone and add to it:',
422
+ '# fig & ggplot2::theme_classic()',
423
+ `# ${alignments ? `autoplot(${alignments}, "pileup")` : `autoplot(${drawn[0]})`} + ggplot2::theme_classic()`,
424
+ ...(notes.length
425
+ ? [
426
+ '#',
427
+ '# What this figure does not show:',
428
+ ...notes.map(n => `# - ${n}`),
429
+ ]
430
+ : []),
431
+ '',
432
+ 'if (',
433
+ ' !requireNamespace("ggjbrowse", quietly = TRUE) ||',
434
+ ` packageVersion("ggjbrowse") < ${rStr(GGJBROWSE_VERSION)}`,
435
+ ') {',
436
+ ` stop("this script needs ggjbrowse ${GGJBROWSE_VERSION} or later: ${INSTALL.replaceAll('"', '\\"')}")`,
437
+ '}',
438
+ 'library(ggjbrowse)',
439
+ '',
440
+ `locus <- ${lociLiteral(loci)}`,
441
+ ...(assemblyValue
442
+ ? [
443
+ "# The view's assembly: the reference every reader reads against, and",
444
+ '# the aliases that name each contig as each file spells it.',
445
+ `assembly <- ${assemblyValue}`,
446
+ ]
447
+ : []),
448
+ ...(widthReaders.length
449
+ ? [
450
+ "# The view's pixels across its regions. A smaller number lays the tracks",
451
+ '# out as a narrower browser window would, for a narrower figure with',
452
+ '# fewer labels at the same size.',
453
+ `width <- ${viewPx}`,
454
+ ]
455
+ : []),
456
+ '',
457
+ reads.join('\n\n'),
458
+ '',
459
+ `fig <- jbrowse_stack(${drawn.join(', ')})`,
460
+ 'print(fig)',
461
+ `ggplot2::ggsave(${rStr(out)}, fig, width = jbrowse_width(fig), height = jbrowse_height(fig))`,
462
+ '',
463
+ ].join('\n');
464
+ return { drawn: drawn.length, script, notes };
465
+ }
@@ -27,8 +27,8 @@ 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.
@@ -38,9 +38,6 @@ export async function renderToSvg(model, opts) {
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
  }