@jbrowse/img 2.1.7 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +1 -4
  2. package/dist/bin.d.ts +2 -0
  3. package/dist/bin.js +2 -2
  4. package/dist/bin.js.map +1 -0
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.js +207 -235
  7. package/dist/index.js.map +1 -0
  8. package/dist/index.testmod.d.ts +1 -0
  9. package/dist/index.testmod.js +166 -246
  10. package/dist/index.testmod.js.map +1 -0
  11. package/dist/parseArgv.d.ts +8 -0
  12. package/dist/parseArgv.js +42 -38
  13. package/dist/parseArgv.js.map +1 -0
  14. package/dist/parseArgv.testmod.d.ts +1 -0
  15. package/dist/parseArgv.testmod.js +25 -7
  16. package/dist/parseArgv.testmod.js.map +1 -0
  17. package/dist/renderRegion.d.ts +35 -0
  18. package/dist/renderRegion.js +350 -450
  19. package/dist/renderRegion.js.map +1 -0
  20. package/dist/util.d.ts +5 -0
  21. package/dist/util.js +31 -11
  22. package/dist/util.js.map +1 -0
  23. package/esm/bin.d.ts +2 -0
  24. package/esm/bin.js +4 -0
  25. package/esm/bin.js.map +1 -0
  26. package/esm/index.d.ts +1 -0
  27. package/esm/index.js +185 -0
  28. package/esm/index.js.map +1 -0
  29. package/esm/index.testmod.d.ts +1 -0
  30. package/esm/index.testmod.js +141 -0
  31. package/esm/index.testmod.js.map +1 -0
  32. package/esm/parseArgv.d.ts +8 -0
  33. package/esm/parseArgv.js +42 -0
  34. package/esm/parseArgv.js.map +1 -0
  35. package/esm/parseArgv.testmod.d.ts +1 -0
  36. package/esm/parseArgv.testmod.js +24 -0
  37. package/esm/parseArgv.testmod.js.map +1 -0
  38. package/esm/renderRegion.d.ts +35 -0
  39. package/esm/renderRegion.js +366 -0
  40. package/esm/renderRegion.js.map +1 -0
  41. package/esm/util.d.ts +5 -0
  42. package/esm/util.js +25 -0
  43. package/esm/util.js.map +1 -0
  44. package/package.json +18 -17
  45. package/src/bin.js +2 -0
  46. package/src/index.testmod.js +163 -0
  47. package/src/index.ts +209 -0
  48. package/src/parseArgv.testmod.js +29 -0
  49. package/src/parseArgv.ts +48 -0
  50. package/src/renderRegion.tsx +456 -0
  51. package/src/util.ts +32 -0
@@ -0,0 +1,456 @@
1
+ import React from 'react'
2
+ import { createViewState } from '@jbrowse/react-linear-genome-view'
3
+ import {
4
+ LinearGenomeViewModel,
5
+ renderToSvg,
6
+ } from '@jbrowse/plugin-linear-genome-view'
7
+ import createCache from '@emotion/cache'
8
+ import { CacheProvider } from '@emotion/react'
9
+ import path from 'path'
10
+ import fs from 'fs'
11
+
12
+ // local
13
+ import { Entry } from './parseArgv'
14
+ import { booleanize } from './util'
15
+
16
+ export interface Opts {
17
+ noRasterize?: boolean
18
+ loc?: string
19
+ width?: number
20
+ session?: string
21
+ assembly?: string
22
+ config?: string
23
+ fasta?: string
24
+ aliases?: string
25
+ cytobands?: string
26
+ defaultSession?: string
27
+ trackList: Entry[]
28
+ tracks?: string
29
+ }
30
+
31
+ function read(file: string) {
32
+ let res
33
+ try {
34
+ res = JSON.parse(fs.readFileSync(file, 'utf8'))
35
+ } catch (e) {
36
+ throw new Error(
37
+ `Failed to parse ${file} as JSON, use --fasta if you mean to pass a FASTA file`,
38
+ )
39
+ }
40
+ return res
41
+ }
42
+ function makeLocation(file: string) {
43
+ return file.startsWith('http') ? { uri: file } : { localPath: file }
44
+ }
45
+
46
+ function addRelativePaths(config: Record<string, unknown>, configPath: string) {
47
+ if (typeof config === 'object') {
48
+ for (const key of Object.keys(config)) {
49
+ if (typeof config[key] === 'object') {
50
+ addRelativePaths(config[key] as Record<string, unknown>, configPath)
51
+ } else if (key === 'localPath') {
52
+ config.localPath = path.resolve(configPath, config.localPath as string)
53
+ }
54
+ }
55
+ }
56
+ }
57
+
58
+ interface Assembly {
59
+ name: string
60
+ sequence: Record<string, unknown>
61
+ refNameAliases?: Record<string, unknown>
62
+ cytobands?: Record<string, unknown>
63
+ }
64
+
65
+ interface Track {
66
+ trackId: string
67
+ [key: string]: unknown
68
+ }
69
+ interface Config {
70
+ assemblies: Assembly[]
71
+ assembly: Assembly
72
+ tracks: Track[]
73
+ [key: string]: unknown
74
+ }
75
+
76
+ export function readData({
77
+ assembly: asm,
78
+ config,
79
+ session,
80
+ fasta,
81
+ aliases,
82
+ cytobands,
83
+ defaultSession,
84
+ tracks,
85
+ trackList = [],
86
+ }: Opts) {
87
+ const assemblyData = asm && fs.existsSync(asm) ? read(asm) : undefined
88
+ const tracksData = tracks ? read(tracks) : undefined
89
+ const configData = (config ? read(config) : {}) as Config
90
+ let sessionData = session ? read(session) : undefined
91
+
92
+ if (config) {
93
+ addRelativePaths(configData, path.dirname(path.resolve(config)))
94
+ }
95
+
96
+ // the session.json can be a raw session or a json file with a "session"
97
+ // attribute, which is what is exported via the "File->Export session" in
98
+ // jbrowse-web
99
+ if (sessionData?.session) {
100
+ sessionData = sessionData.session
101
+ }
102
+
103
+ // only export first view
104
+ if (sessionData?.views) {
105
+ sessionData.view = sessionData.views[0]
106
+ }
107
+
108
+ // use assembly from file if a file existed
109
+ if (assemblyData) {
110
+ configData.assembly = assemblyData
111
+ }
112
+ // else check if it was an assembly name in a config file
113
+ else if (configData.assemblies?.length) {
114
+ configData.assemblies.find(entry => entry.name === asm)
115
+ if (asm) {
116
+ const assembly = configData.assemblies.find(entry => entry.name === asm)
117
+ if (!assembly) {
118
+ throw new Error(`assembly ${asm} not found in config`)
119
+ }
120
+ configData.assembly = assembly
121
+ } else {
122
+ configData.assembly = configData.assemblies[0]
123
+ }
124
+ }
125
+ // else load fasta from command line
126
+ else if (fasta) {
127
+ const bgzip = fasta.endsWith('gz')
128
+
129
+ configData.assembly = {
130
+ name: path.basename(fasta),
131
+ sequence: {
132
+ type: 'ReferenceSequenceTrack',
133
+ trackId: 'refseq',
134
+ adapter: {
135
+ type: bgzip ? 'BgzipFastaAdapter' : 'IndexedFastaAdapter',
136
+ fastaLocation: makeLocation(fasta),
137
+ faiLocation: makeLocation(fasta + '.fai'),
138
+ gziLocation: bgzip ? makeLocation(fasta + '.gzi') : undefined,
139
+ },
140
+ },
141
+ }
142
+ if (aliases) {
143
+ configData.assembly.refNameAliases = {
144
+ adapter: {
145
+ type: 'RefNameAliasAdapter',
146
+ location: makeLocation(aliases),
147
+ },
148
+ }
149
+ }
150
+ if (cytobands) {
151
+ configData.assembly.cytobands = {
152
+ adapter: {
153
+ type: 'CytobandAdapter',
154
+ location: makeLocation(cytobands),
155
+ },
156
+ }
157
+ }
158
+ }
159
+
160
+ // throw if still no assembly
161
+ if (!configData.assembly) {
162
+ throw new Error(
163
+ 'no assembly specified, use --fasta to supply an indexed FASTA file (generated with samtools faidx yourfile.fa). see README for alternatives with --assembly and --config',
164
+ )
165
+ }
166
+
167
+ if (tracksData) {
168
+ configData.tracks = tracksData
169
+ } else if (!configData.tracks) {
170
+ configData.tracks = []
171
+ }
172
+
173
+ trackList.forEach(track => {
174
+ const [type, [file]] = track
175
+
176
+ if (type === 'bam') {
177
+ configData.tracks = [
178
+ ...configData.tracks,
179
+ {
180
+ type: 'AlignmentsTrack',
181
+ trackId: path.basename(file),
182
+ name: path.basename(file),
183
+ assemblyNames: [configData.assembly.name],
184
+ adapter: {
185
+ type: 'BamAdapter',
186
+ bamLocation: makeLocation(file),
187
+ index: { location: makeLocation(file + '.bai') },
188
+ sequenceAdapter: configData.assembly.sequence.adapter,
189
+ },
190
+ },
191
+ ]
192
+ }
193
+ if (type === 'cram') {
194
+ configData.tracks = [
195
+ ...configData.tracks,
196
+ {
197
+ type: 'AlignmentsTrack',
198
+ trackId: path.basename(file),
199
+ name: path.basename(file),
200
+ assemblyNames: [configData.assembly.name],
201
+ adapter: {
202
+ type: 'CramAdapter',
203
+ cramLocation: makeLocation(file),
204
+ craiLocation: makeLocation(file + '.crai'),
205
+ sequenceAdapter: configData.assembly.sequence.adapter,
206
+ },
207
+ },
208
+ ]
209
+ }
210
+ if (type === 'bigwig') {
211
+ configData.tracks = [
212
+ ...configData.tracks,
213
+ {
214
+ type: 'QuantitativeTrack',
215
+ trackId: path.basename(file),
216
+ name: path.basename(file),
217
+ assemblyNames: [configData.assembly.name],
218
+ adapter: {
219
+ type: 'BigWigAdapter',
220
+ bigWigLocation: makeLocation(file),
221
+ },
222
+ },
223
+ ]
224
+ }
225
+
226
+ if (type === 'vcfgz') {
227
+ configData.tracks = [
228
+ ...configData.tracks,
229
+ {
230
+ type: 'VariantTrack',
231
+ trackId: path.basename(file),
232
+ name: path.basename(file),
233
+ assemblyNames: [configData.assembly.name],
234
+ adapter: {
235
+ type: 'VcfTabixAdapter',
236
+ vcfGzLocation: makeLocation(file),
237
+ index: {
238
+ location: makeLocation(file + '.tbi'),
239
+ },
240
+ },
241
+ },
242
+ ]
243
+ }
244
+
245
+ if (type === 'gffgz') {
246
+ configData.tracks = [
247
+ ...configData.tracks,
248
+ {
249
+ type: 'FeatureTrack',
250
+ trackId: path.basename(file),
251
+ name: path.basename(file),
252
+ assemblyNames: [configData.assembly.name],
253
+ adapter: {
254
+ type: 'Gff3TabixAdapter',
255
+ gffGzLocation: makeLocation(file),
256
+ index: {
257
+ location: makeLocation(file + '.tbi'),
258
+ },
259
+ },
260
+ },
261
+ ]
262
+ }
263
+
264
+ if (type === 'hic') {
265
+ configData.tracks = [
266
+ ...configData.tracks,
267
+ {
268
+ type: 'HicTrack',
269
+ trackId: path.basename(file),
270
+ name: path.basename(file),
271
+ assemblyNames: [configData.assembly.name],
272
+ adapter: {
273
+ type: 'HicAdapter',
274
+ hicLocation: makeLocation(file),
275
+ },
276
+ },
277
+ ]
278
+ }
279
+ if (type === 'bigbed') {
280
+ configData.tracks = [
281
+ ...configData.tracks,
282
+ {
283
+ type: 'FeatureTrack',
284
+ trackId: path.basename(file),
285
+ name: path.basename(file),
286
+ assemblyNames: [configData.assembly.name],
287
+ adapter: {
288
+ type: 'BigBedAdapter',
289
+ bigBedLocation: makeLocation(file),
290
+ },
291
+ },
292
+ ]
293
+ }
294
+ if (type === 'bedgz') {
295
+ configData.tracks = [
296
+ ...configData.tracks,
297
+ {
298
+ type: 'FeatureTrack',
299
+ trackId: path.basename(file),
300
+ name: path.basename(file),
301
+ assemblyNames: [configData.assembly.name],
302
+ adapter: {
303
+ type: 'BedTabixAdapter',
304
+ bedGzLocation: makeLocation(file),
305
+ index: {
306
+ location: makeLocation(file + '.tbi'),
307
+ },
308
+ },
309
+ },
310
+ ]
311
+ }
312
+ })
313
+
314
+ if (!defaultSession) {
315
+ // don't use defaultSession from config.json file, can result in assembly
316
+ // name confusion
317
+ delete configData.defaultSession
318
+ }
319
+
320
+ // only allow an external manually specified session
321
+ if (sessionData) {
322
+ configData.defaultSession = sessionData
323
+ }
324
+
325
+ return configData
326
+ }
327
+
328
+ // without this, the styles can become messed up especially in lgv header
329
+ // xref https://github.com/garronej/tss-react/issues/25
330
+ export const muiCache = createCache({
331
+ key: 'mui',
332
+ prepend: true,
333
+ })
334
+
335
+ function process(
336
+ trackEntry: Entry,
337
+ view: LinearGenomeViewModel,
338
+ extra: (arg: string) => string = c => c,
339
+ ) {
340
+ const [, [track, ...opts]] = trackEntry
341
+ const currentTrack = view.showTrack(extra(track))
342
+ const display = currentTrack.displays[0]
343
+ opts.forEach(opt => {
344
+ // apply height to any track
345
+ if (opt.startsWith('height:')) {
346
+ const [, height] = opt.split(':')
347
+ display.setHeight(+height)
348
+ }
349
+
350
+ // apply sort to pileup
351
+ else if (opt.startsWith('sort:')) {
352
+ const [, type, tag] = opt.split(':')
353
+ display.PileupDisplay.setSortedBy(type, tag)
354
+ }
355
+
356
+ // apply color scheme to pileup
357
+ else if (opt.startsWith('color:')) {
358
+ const [, type, tag] = opt.split(':')
359
+ if (display.PileupDisplay) {
360
+ display.PileupDisplay.setColorScheme({ type, tag })
361
+ } else {
362
+ display.setColor(type)
363
+ }
364
+ }
365
+
366
+ // force track to render even if maxbpperpx limit hit...
367
+ else if (opt.startsWith('force:')) {
368
+ const [, force] = opt.split(':')
369
+ if (force) {
370
+ display.updateStatsLimit({ bytes: Number.MAX_VALUE })
371
+ }
372
+ }
373
+
374
+ // apply wiggle autoscale
375
+ else if (opt.startsWith('autoscale:')) {
376
+ const [, autoscale] = opt.split(':')
377
+ display.setAutoscale(autoscale)
378
+ }
379
+
380
+ // apply min and max score to wiggle
381
+ else if (opt.startsWith('minmax:')) {
382
+ const [, min, max] = opt.split(':')
383
+ display.setMinScore(+min)
384
+ display.setMaxScore(+max)
385
+ }
386
+
387
+ // apply linear or log scale to wiggle
388
+ else if (opt.startsWith('scaletype:')) {
389
+ const [, scaletype] = opt.split(':')
390
+ display.setScaleType(scaletype)
391
+ }
392
+
393
+ // draw crosshatches on wiggle
394
+ else if (opt.startsWith('crosshatch:')) {
395
+ const [, val] = opt.split(':')
396
+ display.setCrossHatches(booleanize(val))
397
+ }
398
+
399
+ // turn off fill on bigwig with fill:false
400
+ else if (opt.startsWith('fill:')) {
401
+ const [, val] = opt.split(':')
402
+ display.setFill(booleanize(val))
403
+ }
404
+
405
+ // set resolution:superfine to use finer bigwig bin size
406
+ else if (opt.startsWith('resolution:')) {
407
+ let [, val] = opt.split(':')
408
+ if (val === 'fine') {
409
+ val = '10'
410
+ } else if (val === 'superfine') {
411
+ val = '100'
412
+ }
413
+ display.setResolution(+val)
414
+ }
415
+ })
416
+ }
417
+
418
+ export async function renderRegion(opts: Opts) {
419
+ const model = createViewState(readData(opts))
420
+ const {
421
+ loc,
422
+ width = 1500,
423
+ trackList = [],
424
+ session: sessionParam,
425
+ defaultSession,
426
+ } = opts
427
+
428
+ const { session } = model
429
+ const { view } = session
430
+ const { assemblyManager } = model
431
+
432
+ view.setWidth(width)
433
+
434
+ if (loc) {
435
+ const [assembly] = assemblyManager.assemblies
436
+ if (loc === 'all') {
437
+ view.showAllRegionsInAssembly(assembly.name)
438
+ } else {
439
+ await view.navToLocString(loc, assembly.name)
440
+ }
441
+ } else if (!sessionParam && !defaultSession) {
442
+ console.warn('No loc specified')
443
+ }
444
+
445
+ trackList.forEach(track => {
446
+ process(track, view, extra => path.basename(extra))
447
+ })
448
+
449
+ return renderToSvg(view, {
450
+ rasterizeLayers: !opts.noRasterize,
451
+ ...opts,
452
+ Wrapper: ({ children }) => (
453
+ <CacheProvider value={muiCache}>{children}</CacheProvider>
454
+ ),
455
+ })
456
+ }
package/src/util.ts ADDED
@@ -0,0 +1,32 @@
1
+ import fs from 'fs'
2
+ import tmp from 'tmp'
3
+ import { spawnSync } from 'child_process'
4
+
5
+ // nice helper function from https://stackoverflow.com/questions/263965/
6
+ export function booleanize(str: string) {
7
+ return str === 'false' ? false : !!str
8
+ }
9
+
10
+ function createTmp() {
11
+ return tmp.fileSync({
12
+ mode: 0o644,
13
+ prefix: 'prefix-',
14
+ postfix: '.svg',
15
+ })
16
+ }
17
+
18
+ export function convert(
19
+ result: string,
20
+ args: { out: string; pngwidth?: string },
21
+ spawnArgs: string[] = [],
22
+ ) {
23
+ const { name } = createTmp()
24
+ const { pngwidth = '2048', out } = args
25
+ fs.writeFileSync(name, result)
26
+ const a = ['-w', pngwidth, name, '-o', out, ...spawnArgs] as string[]
27
+ const ls = spawnSync('rsvg-convert', a)
28
+
29
+ console.error(`rsvg-convert stderr: ${ls.stderr.toString()}`)
30
+ console.log(`rsvg-convert stdout: ${ls.stdout.toString()}`) // eslint-disable-line no-console
31
+ fs.unlinkSync(name)
32
+ }