@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.
- package/esm/FeatureTrack/configSchema.d.ts +2 -1
- package/esm/FeatureTrack/configSchema.js +2 -1
- package/esm/LinearGenomeView/afterAttach.js +1 -23
- package/esm/LinearGenomeView/components/ExportSvgDialog.d.ts +6 -2
- package/esm/LinearGenomeView/components/ExportSvgDialog.js +31 -7
- package/esm/LinearGenomeView/components/HeaderPanControls.d.ts +3 -3
- package/esm/LinearGenomeView/components/HeaderPanControls.js +4 -2
- package/esm/LinearGenomeView/components/HighlightBand.d.ts +3 -2
- package/esm/LinearGenomeView/components/HighlightBand.js +4 -2
- package/esm/LinearGenomeView/components/HighlightChip.d.ts +3 -2
- package/esm/LinearGenomeView/components/HighlightChip.js +4 -2
- package/esm/LinearGenomeView/components/LinearGenomeView.js +5 -12
- package/esm/LinearGenomeView/components/LinearGenomeViewContainer.js +4 -5
- package/esm/LinearGenomeView/components/OverviewHighlightBand.d.ts +3 -2
- package/esm/LinearGenomeView/components/OverviewHighlightBand.js +4 -2
- package/esm/LinearGenomeView/components/RegionWidthEditorDialog.d.ts +3 -2
- package/esm/LinearGenomeView/components/RegionWidthEditorDialog.js +4 -4
- package/esm/LinearGenomeView/components/ScalebarRefNameLabels.js +4 -4
- package/esm/LinearGenomeView/components/SearchPanelForm.d.ts +3 -2
- package/esm/LinearGenomeView/components/SearchPanelForm.js +4 -2
- package/esm/LinearGenomeView/components/SearchResultsDialog.d.ts +4 -9
- package/esm/LinearGenomeView/components/SearchResultsDialog.js +7 -4
- package/esm/LinearGenomeView/components/SearchResultsTable.d.ts +3 -8
- package/esm/LinearGenomeView/components/SearchResultsTable.js +4 -32
- package/esm/LinearGenomeView/components/StrandCheckboxes.d.ts +3 -2
- package/esm/LinearGenomeView/components/StrandCheckboxes.js +4 -2
- package/esm/LinearGenomeView/components/TrackContainer.js +1 -1
- package/esm/LinearGenomeView/components/TrackLabelMenu.js +8 -12
- package/esm/LinearGenomeView/components/useRangeSelect.js +3 -2
- package/esm/LinearGenomeView/components/useSearchBoxPrefs.d.ts +3 -1
- package/esm/LinearGenomeView/components/useSearchBoxPrefs.js +4 -2
- package/esm/LinearGenomeView/components/useSideScroll.js +11 -12
- package/esm/LinearGenomeView/lazyDialogs.d.ts +17 -2
- package/esm/LinearGenomeView/menuItems.js +32 -24
- package/esm/LinearGenomeView/model.d.ts +197 -151
- package/esm/LinearGenomeView/model.js +203 -201
- package/esm/LinearGenomeView/rExport/exportLinearView.d.ts +13 -0
- package/esm/LinearGenomeView/rExport/exportLinearView.js +98 -0
- package/esm/LinearGenomeView/rExport/rScript.d.ts +75 -0
- package/esm/LinearGenomeView/rExport/rScript.js +465 -0
- package/esm/LinearGenomeView/svgcomponents/SVGLinearGenomeView.js +3 -6
- package/esm/LinearGenomeView/svgcomponents/SVGStackedRow.d.ts +1 -2
- package/esm/LinearGenomeView/svgcomponents/SVGStackedRow.js +2 -2
- package/esm/LinearGenomeView/svgcomponents/SVGTracks.d.ts +1 -2
- package/esm/LinearGenomeView/svgcomponents/SVGTracks.js +4 -4
- package/esm/LinearGenomeView/svgcomponents/SVGView.d.ts +1 -2
- package/esm/LinearGenomeView/svgcomponents/SVGView.js +3 -3
- package/esm/LinearGenomeView/svgcomponents/renderViewTracks.d.ts +22 -20
- package/esm/LinearGenomeView/svgcomponents/renderViewTracks.js +42 -29
- package/esm/LinearGenomeView/svgcomponents/useViewSvgFigure.js +6 -11
- package/esm/LinearGenomeView/svgcomponents/util.d.ts +2 -1
- package/esm/LinearGenomeView/util.d.ts +42 -0
- package/esm/LinearGenomeView/util.js +81 -0
- package/esm/MultiLevelRubberband/MultiLevelRubberband.js +2 -2
- package/esm/displayKitTests/perRegionTestEnv.d.ts +45 -12
- package/esm/index.d.ts +4 -2
- package/esm/index.js +3 -1
- package/esm/searchUtils.js +1 -0
- package/esm/shared/RubberbandSpan.d.ts +3 -2
- package/esm/shared/RubberbandSpan.js +4 -2
- package/esm/shared/coordLabels.d.ts +6 -6
- package/esm/shared/coordLabels.js +7 -6
- 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
|
|
31
|
-
//
|
|
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
|
|
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
|
}
|