@jbrowse/plugin-linear-genome-view 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/LinearGenomeView/components/{VerticalGuides.d.ts → Gridlines.d.ts} +0 -0
- package/dist/LinearGenomeView/components/{VerticalGuides.js → Gridlines.js} +1 -1
- package/dist/LinearGenomeView/components/Gridlines.js.map +1 -0
- package/dist/LinearGenomeView/components/OverviewRubberBand.js +13 -10
- package/dist/LinearGenomeView/components/OverviewRubberBand.js.map +1 -1
- package/dist/LinearGenomeView/components/RubberBand.js +0 -1
- package/dist/LinearGenomeView/components/RubberBand.js.map +1 -1
- package/dist/LinearGenomeView/components/SequenceDialog.js +1 -1
- package/dist/LinearGenomeView/components/SequenceDialog.js.map +1 -1
- package/dist/LinearGenomeView/components/TracksContainer.js +2 -2
- package/dist/LinearGenomeView/components/TracksContainer.js.map +1 -1
- package/dist/LinearGenomeView/index.d.ts +63 -76
- package/dist/LinearGenomeView/index.js +239 -365
- package/dist/LinearGenomeView/index.js.map +1 -1
- package/esm/LinearGenomeView/components/{VerticalGuides.d.ts → Gridlines.d.ts} +0 -0
- package/esm/LinearGenomeView/components/{VerticalGuides.js → Gridlines.js} +1 -1
- package/esm/LinearGenomeView/components/Gridlines.js.map +1 -0
- package/esm/LinearGenomeView/components/OverviewRubberBand.js +13 -10
- package/esm/LinearGenomeView/components/OverviewRubberBand.js.map +1 -1
- package/esm/LinearGenomeView/components/RubberBand.js +0 -1
- package/esm/LinearGenomeView/components/RubberBand.js.map +1 -1
- package/esm/LinearGenomeView/components/SequenceDialog.js +1 -1
- package/esm/LinearGenomeView/components/SequenceDialog.js.map +1 -1
- package/esm/LinearGenomeView/components/TracksContainer.js +2 -2
- package/esm/LinearGenomeView/components/TracksContainer.js.map +1 -1
- package/esm/LinearGenomeView/index.d.ts +63 -76
- package/esm/LinearGenomeView/index.js +243 -389
- package/esm/LinearGenomeView/index.js.map +1 -1
- package/package.json +3 -3
- package/src/LinearGenomeView/components/{VerticalGuides.tsx → Gridlines.tsx} +0 -0
- package/src/LinearGenomeView/components/OverviewRubberBand.tsx +14 -19
- package/src/LinearGenomeView/components/RubberBand.tsx +0 -1
- package/src/LinearGenomeView/components/SequenceDialog.tsx +1 -1
- package/src/LinearGenomeView/components/TracksContainer.tsx +2 -2
- package/src/LinearGenomeView/index.test.ts +13 -36
- package/src/LinearGenomeView/index.tsx +360 -519
- package/dist/LinearGenomeView/components/VerticalGuides.js.map +0 -1
- package/esm/LinearGenomeView/components/VerticalGuides.js.map +0 -1
|
@@ -2,15 +2,16 @@ import { getConf } from '@jbrowse/core/configuration';
|
|
|
2
2
|
import { BaseViewModel } from '@jbrowse/core/pluggableElementTypes/models';
|
|
3
3
|
import { ElementId, Region as MUIRegion } from '@jbrowse/core/util/types/mst';
|
|
4
4
|
import { ReturnToImportFormDialog } from '@jbrowse/core/ui';
|
|
5
|
-
import { assembleLocString, clamp, findLastIndex, getContainingView, getSession, isViewContainer, isSessionModelWithWidgets, measureText, parseLocString, springAnimate,
|
|
5
|
+
import { assembleLocString, clamp, findLastIndex, getContainingView, getSession, isViewContainer, isSessionModelWithWidgets, measureText, parseLocString, springAnimate, } from '@jbrowse/core/util';
|
|
6
6
|
import calculateDynamicBlocks from '@jbrowse/core/util/calculateDynamicBlocks';
|
|
7
7
|
import calculateStaticBlocks from '@jbrowse/core/util/calculateStaticBlocks';
|
|
8
8
|
import { getParentRenderProps } from '@jbrowse/core/util/tracks';
|
|
9
9
|
import { transaction, autorun } from 'mobx';
|
|
10
10
|
import { addDisposer, cast, getSnapshot, getRoot, resolveIdentifier, types, } from 'mobx-state-tree';
|
|
11
11
|
import Base1DView from '@jbrowse/core/util/Base1DViewModel';
|
|
12
|
-
import
|
|
12
|
+
import { moveTo, pxToBp, bpToPx } from '@jbrowse/core/util/Base1DUtils';
|
|
13
13
|
import { saveAs } from 'file-saver';
|
|
14
|
+
import clone from 'clone';
|
|
14
15
|
// icons
|
|
15
16
|
import { TrackSelector as TrackSelectorIcon } from '@jbrowse/core/ui/Icons';
|
|
16
17
|
import SyncAltIcon from '@mui/icons-material/SyncAlt';
|
|
@@ -74,6 +75,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
74
75
|
const setting = localStorageGetItem('lgv-showCytobands');
|
|
75
76
|
return setting !== undefined && setting !== null ? !!+setting : true;
|
|
76
77
|
}),
|
|
78
|
+
showGridlines: true,
|
|
77
79
|
}))
|
|
78
80
|
.volatile(() => ({
|
|
79
81
|
volatileWidth: undefined,
|
|
@@ -197,91 +199,6 @@ export function stateModelFactory(pluginManager) {
|
|
|
197
199
|
tracks: self.tracks,
|
|
198
200
|
};
|
|
199
201
|
},
|
|
200
|
-
bpToPx({ refName, coord, regionNumber, }) {
|
|
201
|
-
return viewBpToPx({ refName, coord, regionNumber, self });
|
|
202
|
-
},
|
|
203
|
-
/**
|
|
204
|
-
*
|
|
205
|
-
* @param px - px in the view area, return value is the displayed regions
|
|
206
|
-
* @returns BpOffset of the displayed region that it lands in
|
|
207
|
-
*/
|
|
208
|
-
pxToBp(px) {
|
|
209
|
-
let bpSoFar = 0;
|
|
210
|
-
const bp = (self.offsetPx + px) * self.bpPerPx;
|
|
211
|
-
const n = self.displayedRegions.length;
|
|
212
|
-
if (bp < 0) {
|
|
213
|
-
const region = self.displayedRegions[0];
|
|
214
|
-
const offset = bp;
|
|
215
|
-
const snap = getSnapshot(region);
|
|
216
|
-
return {
|
|
217
|
-
// xref https://github.com/mobxjs/mobx-state-tree/issues/1524 for Omit
|
|
218
|
-
...snap,
|
|
219
|
-
oob: true,
|
|
220
|
-
coord: region.reversed
|
|
221
|
-
? Math.floor(region.end - offset) + 1
|
|
222
|
-
: Math.floor(region.start + offset) + 1,
|
|
223
|
-
offset,
|
|
224
|
-
index: 0,
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
const interRegionPaddingBp = self.interRegionPaddingWidth * self.bpPerPx;
|
|
228
|
-
const minimumBlockBp = self.minimumBlockWidth * self.bpPerPx;
|
|
229
|
-
for (let index = 0; index < self.displayedRegions.length; index += 1) {
|
|
230
|
-
const region = self.displayedRegions[index];
|
|
231
|
-
const len = region.end - region.start;
|
|
232
|
-
const offset = bp - bpSoFar;
|
|
233
|
-
if (len + bpSoFar > bp && bpSoFar <= bp) {
|
|
234
|
-
const snap = getSnapshot(region);
|
|
235
|
-
return {
|
|
236
|
-
// xref https://github.com/mobxjs/mobx-state-tree/issues/1524 for Omit
|
|
237
|
-
...snap,
|
|
238
|
-
oob: false,
|
|
239
|
-
offset,
|
|
240
|
-
coord: region.reversed
|
|
241
|
-
? Math.floor(region.end - offset) + 1
|
|
242
|
-
: Math.floor(region.start + offset) + 1,
|
|
243
|
-
index,
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
// add the interRegionPaddingWidth if the boundary is in the screen
|
|
247
|
-
// e.g. offset>0 && offset<width
|
|
248
|
-
if (region.end - region.start > minimumBlockBp &&
|
|
249
|
-
offset / self.bpPerPx > 0 &&
|
|
250
|
-
offset / self.bpPerPx < self.width) {
|
|
251
|
-
bpSoFar += len + interRegionPaddingBp;
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
bpSoFar += len;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
if (bp >= bpSoFar) {
|
|
258
|
-
const region = self.displayedRegions[n - 1];
|
|
259
|
-
const len = region.end - region.start;
|
|
260
|
-
const offset = bp - bpSoFar + len;
|
|
261
|
-
const snap = getSnapshot(region);
|
|
262
|
-
return {
|
|
263
|
-
// xref https://github.com/mobxjs/mobx-state-tree/issues/1524 for Omit
|
|
264
|
-
...snap,
|
|
265
|
-
oob: true,
|
|
266
|
-
offset,
|
|
267
|
-
coord: region.reversed
|
|
268
|
-
? Math.floor(region.end - offset) + 1
|
|
269
|
-
: Math.floor(region.start + offset) + 1,
|
|
270
|
-
index: n - 1,
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
return {
|
|
274
|
-
coord: 0,
|
|
275
|
-
index: 0,
|
|
276
|
-
refName: '',
|
|
277
|
-
oob: true,
|
|
278
|
-
assemblyName: '',
|
|
279
|
-
offset: 0,
|
|
280
|
-
start: 0,
|
|
281
|
-
end: 0,
|
|
282
|
-
reversed: false,
|
|
283
|
-
};
|
|
284
|
-
},
|
|
285
202
|
getTrack(id) {
|
|
286
203
|
return self.tracks.find(t => t.configuration.trackId === id);
|
|
287
204
|
},
|
|
@@ -326,11 +243,6 @@ export function stateModelFactory(pluginManager) {
|
|
|
326
243
|
});
|
|
327
244
|
return allActions;
|
|
328
245
|
},
|
|
329
|
-
get centerLineInfo() {
|
|
330
|
-
return self.displayedRegions.length
|
|
331
|
-
? this.pxToBp(self.width / 2)
|
|
332
|
-
: undefined;
|
|
333
|
-
},
|
|
334
246
|
}))
|
|
335
247
|
.actions(self => ({
|
|
336
248
|
setShowCytobands(flag) {
|
|
@@ -352,6 +264,9 @@ export function stateModelFactory(pluginManager) {
|
|
|
352
264
|
toggleNoTracksActive() {
|
|
353
265
|
self.hideNoTracksActive = !self.hideNoTracksActive;
|
|
354
266
|
},
|
|
267
|
+
toggleShowGridlines() {
|
|
268
|
+
self.showGridlines = !self.showGridlines;
|
|
269
|
+
},
|
|
355
270
|
scrollTo(offsetPx) {
|
|
356
271
|
const newOffsetPx = clamp(offsetPx, self.minOffset, self.maxOffset);
|
|
357
272
|
self.offsetPx = newOffsetPx;
|
|
@@ -498,233 +413,6 @@ export function stateModelFactory(pluginManager) {
|
|
|
498
413
|
}
|
|
499
414
|
throw new Error(`invalid track selector type ${self.trackSelectorType}`);
|
|
500
415
|
},
|
|
501
|
-
navToLocString(locString, optAssemblyName) {
|
|
502
|
-
const { assemblyNames } = self;
|
|
503
|
-
const { assemblyManager } = getSession(self);
|
|
504
|
-
const { isValidRefName } = assemblyManager;
|
|
505
|
-
const assemblyName = optAssemblyName || assemblyNames[0];
|
|
506
|
-
let parsedLocStrings;
|
|
507
|
-
const inputs = locString
|
|
508
|
-
.split(/(\s+)/)
|
|
509
|
-
.map(f => f.trim())
|
|
510
|
-
.filter(f => !!f);
|
|
511
|
-
// first try interpreting as a whitespace-separated sequence of
|
|
512
|
-
// multiple locstrings
|
|
513
|
-
try {
|
|
514
|
-
parsedLocStrings = inputs.map(l => parseLocString(l, ref => isValidRefName(ref, assemblyName)));
|
|
515
|
-
}
|
|
516
|
-
catch (e) {
|
|
517
|
-
// if this fails, try interpreting as a whitespace-separated refname,
|
|
518
|
-
// start, end if start and end are integer inputs
|
|
519
|
-
const [refName, start, end] = inputs;
|
|
520
|
-
if (`${e}`.match(/Unknown reference sequence/) &&
|
|
521
|
-
Number.isInteger(+start) &&
|
|
522
|
-
Number.isInteger(+end)) {
|
|
523
|
-
parsedLocStrings = [
|
|
524
|
-
parseLocString(refName + ':' + start + '..' + end, ref => isValidRefName(ref, assemblyName)),
|
|
525
|
-
];
|
|
526
|
-
}
|
|
527
|
-
else {
|
|
528
|
-
throw e;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
const locations = parsedLocStrings === null || parsedLocStrings === void 0 ? void 0 : parsedLocStrings.map(region => {
|
|
532
|
-
const asmName = region.assemblyName || assemblyName;
|
|
533
|
-
const asm = assemblyManager.get(asmName);
|
|
534
|
-
const { refName } = region;
|
|
535
|
-
if (!asm) {
|
|
536
|
-
throw new Error(`assembly ${asmName} not found`);
|
|
537
|
-
}
|
|
538
|
-
const { regions } = asm;
|
|
539
|
-
if (!regions) {
|
|
540
|
-
throw new Error(`regions not loaded yet for ${asmName}`);
|
|
541
|
-
}
|
|
542
|
-
const canonicalRefName = asm.getCanonicalRefName(region.refName);
|
|
543
|
-
if (!canonicalRefName) {
|
|
544
|
-
throw new Error(`Could not find refName ${refName} in ${asm.name}`);
|
|
545
|
-
}
|
|
546
|
-
const parentRegion = regions.find(region => region.refName === canonicalRefName);
|
|
547
|
-
if (!parentRegion) {
|
|
548
|
-
throw new Error(`Could not find refName ${refName} in ${asmName}`);
|
|
549
|
-
}
|
|
550
|
-
return {
|
|
551
|
-
...region,
|
|
552
|
-
assemblyName: asmName,
|
|
553
|
-
parentRegion,
|
|
554
|
-
};
|
|
555
|
-
});
|
|
556
|
-
if (locations.length === 1) {
|
|
557
|
-
const loc = locations[0];
|
|
558
|
-
this.setDisplayedRegions([
|
|
559
|
-
{ reversed: loc.reversed, ...loc.parentRegion },
|
|
560
|
-
]);
|
|
561
|
-
const { start, end, parentRegion } = loc;
|
|
562
|
-
this.navTo({
|
|
563
|
-
...loc,
|
|
564
|
-
start: clamp(start !== null && start !== void 0 ? start : 0, 0, parentRegion.end),
|
|
565
|
-
end: clamp(end !== null && end !== void 0 ? end : parentRegion.end, 0, parentRegion.end),
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
else {
|
|
569
|
-
this.setDisplayedRegions(
|
|
570
|
-
// @ts-ignore
|
|
571
|
-
locations.map(r => (r.start === undefined ? r.parentRegion : r)));
|
|
572
|
-
this.showAllRegions();
|
|
573
|
-
}
|
|
574
|
-
},
|
|
575
|
-
/**
|
|
576
|
-
* Navigate to a location based on its refName and optionally start, end,
|
|
577
|
-
* and assemblyName. Can handle if there are multiple displayedRegions
|
|
578
|
-
* from same refName. Only navigates to a location if it is entirely
|
|
579
|
-
* within a displayedRegion. Navigates to the first matching location
|
|
580
|
-
* encountered.
|
|
581
|
-
*
|
|
582
|
-
* Throws an error if navigation was unsuccessful
|
|
583
|
-
*
|
|
584
|
-
* @param location - a proposed location to navigate to
|
|
585
|
-
*/
|
|
586
|
-
navTo(query) {
|
|
587
|
-
this.navToMultiple([query]);
|
|
588
|
-
},
|
|
589
|
-
navToMultiple(locations) {
|
|
590
|
-
const firstLocation = locations[0];
|
|
591
|
-
let { refName } = firstLocation;
|
|
592
|
-
const { start, end, assemblyName = self.assemblyNames[0], } = firstLocation;
|
|
593
|
-
if (start !== undefined && end !== undefined && start > end) {
|
|
594
|
-
throw new Error(`start "${start + 1}" is greater than end "${end}"`);
|
|
595
|
-
}
|
|
596
|
-
const session = getSession(self);
|
|
597
|
-
const { assemblyManager } = session;
|
|
598
|
-
const assembly = assemblyManager.get(assemblyName);
|
|
599
|
-
if (assembly) {
|
|
600
|
-
const canonicalRefName = assembly.getCanonicalRefName(refName);
|
|
601
|
-
if (canonicalRefName) {
|
|
602
|
-
refName = canonicalRefName;
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
let s = start;
|
|
606
|
-
let e = end;
|
|
607
|
-
let refNameMatched = false;
|
|
608
|
-
const predicate = (r) => {
|
|
609
|
-
if (refName === r.refName) {
|
|
610
|
-
refNameMatched = true;
|
|
611
|
-
if (s === undefined) {
|
|
612
|
-
s = r.start;
|
|
613
|
-
}
|
|
614
|
-
if (e === undefined) {
|
|
615
|
-
e = r.end;
|
|
616
|
-
}
|
|
617
|
-
if (s >= r.start && s <= r.end && e <= r.end && e >= r.start) {
|
|
618
|
-
return true;
|
|
619
|
-
}
|
|
620
|
-
s = start;
|
|
621
|
-
e = end;
|
|
622
|
-
}
|
|
623
|
-
return false;
|
|
624
|
-
};
|
|
625
|
-
const lastIndex = findLastIndex(self.displayedRegions, predicate);
|
|
626
|
-
let index;
|
|
627
|
-
while (index !== lastIndex) {
|
|
628
|
-
try {
|
|
629
|
-
const previousIndex = index;
|
|
630
|
-
index = self.displayedRegions
|
|
631
|
-
.slice(previousIndex === undefined ? 0 : previousIndex + 1)
|
|
632
|
-
.findIndex(predicate);
|
|
633
|
-
if (previousIndex !== undefined) {
|
|
634
|
-
index += previousIndex + 1;
|
|
635
|
-
}
|
|
636
|
-
if (!refNameMatched) {
|
|
637
|
-
throw new Error(`could not find a region with refName "${refName}"`);
|
|
638
|
-
}
|
|
639
|
-
if (s === undefined) {
|
|
640
|
-
throw new Error(`could not find a region with refName "${refName}" that contained an end position ${e}`);
|
|
641
|
-
}
|
|
642
|
-
if (e === undefined) {
|
|
643
|
-
throw new Error(`could not find a region with refName "${refName}" that contained a start position ${s + 1}`);
|
|
644
|
-
}
|
|
645
|
-
if (index === -1) {
|
|
646
|
-
throw new Error(`could not find a region that completely contained "${assembleLocString(firstLocation)}"`);
|
|
647
|
-
}
|
|
648
|
-
if (locations.length === 1) {
|
|
649
|
-
const f = self.displayedRegions[index];
|
|
650
|
-
this.moveTo({ index, offset: f.reversed ? f.end - e : s - f.start }, { index, offset: f.reversed ? f.end - s : e - f.start });
|
|
651
|
-
return;
|
|
652
|
-
}
|
|
653
|
-
let locationIndex = 0;
|
|
654
|
-
let locationStart = 0;
|
|
655
|
-
let locationEnd = 0;
|
|
656
|
-
for (locationIndex; locationIndex < locations.length; locationIndex++) {
|
|
657
|
-
const location = locations[locationIndex];
|
|
658
|
-
const region = self.displayedRegions[index + locationIndex];
|
|
659
|
-
locationStart = location.start || region.start;
|
|
660
|
-
locationEnd = location.end || region.end;
|
|
661
|
-
if (location.refName !== region.refName) {
|
|
662
|
-
throw new Error(`Entered location ${assembleLocString(location)} does not match with displayed regions`);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
locationIndex -= 1;
|
|
666
|
-
const startDisplayedRegion = self.displayedRegions[index];
|
|
667
|
-
const endDisplayedRegion = self.displayedRegions[index + locationIndex];
|
|
668
|
-
this.moveTo({
|
|
669
|
-
index,
|
|
670
|
-
offset: startDisplayedRegion.reversed
|
|
671
|
-
? startDisplayedRegion.end - e
|
|
672
|
-
: s - startDisplayedRegion.start,
|
|
673
|
-
}, {
|
|
674
|
-
index: index + locationIndex,
|
|
675
|
-
offset: endDisplayedRegion.reversed
|
|
676
|
-
? endDisplayedRegion.end - locationStart
|
|
677
|
-
: locationEnd - endDisplayedRegion.start,
|
|
678
|
-
});
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
|
-
catch (error) {
|
|
682
|
-
if (index === lastIndex) {
|
|
683
|
-
throw error;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
},
|
|
688
|
-
/**
|
|
689
|
-
* Navigate to a location based on user clicking and dragging on the
|
|
690
|
-
* overview scale bar to select a region to zoom into.
|
|
691
|
-
* Can handle if there are multiple displayedRegions from same refName.
|
|
692
|
-
* Only navigates to a location if it is entirely within a displayedRegion.
|
|
693
|
-
*
|
|
694
|
-
* @param leftPx- `object as {start, end, index, offset}`, offset = start of user drag
|
|
695
|
-
* @param rightPx- `object as {start, end, index, offset}`, offset = end of user drag
|
|
696
|
-
*/
|
|
697
|
-
zoomToDisplayedRegions(leftPx, rightPx) {
|
|
698
|
-
if (leftPx === undefined || rightPx === undefined) {
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
const singleRefSeq = leftPx.refName === rightPx.refName && leftPx.index === rightPx.index;
|
|
702
|
-
// zooming into one displayed Region
|
|
703
|
-
if ((singleRefSeq && rightPx.offset < leftPx.offset) ||
|
|
704
|
-
leftPx.index > rightPx.index) {
|
|
705
|
-
;
|
|
706
|
-
[leftPx, rightPx] = [rightPx, leftPx];
|
|
707
|
-
}
|
|
708
|
-
const startOffset = {
|
|
709
|
-
start: leftPx.start,
|
|
710
|
-
end: leftPx.end,
|
|
711
|
-
index: leftPx.index,
|
|
712
|
-
offset: leftPx.offset,
|
|
713
|
-
};
|
|
714
|
-
const endOffset = {
|
|
715
|
-
start: rightPx.start,
|
|
716
|
-
end: rightPx.end,
|
|
717
|
-
index: rightPx.index,
|
|
718
|
-
offset: rightPx.offset,
|
|
719
|
-
};
|
|
720
|
-
if (startOffset && endOffset) {
|
|
721
|
-
this.moveTo(startOffset, endOffset);
|
|
722
|
-
}
|
|
723
|
-
else {
|
|
724
|
-
const session = getSession(self);
|
|
725
|
-
session.notify('No regions found to navigate to', 'warning');
|
|
726
|
-
}
|
|
727
|
-
},
|
|
728
416
|
/**
|
|
729
417
|
* Helper method for the fetchSequence.
|
|
730
418
|
* Retrieves the corresponding regions that were selected by the rubberband
|
|
@@ -741,7 +429,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
741
429
|
interRegionPaddingWidth: self.interRegionPaddingWidth,
|
|
742
430
|
});
|
|
743
431
|
simView.setVolatileWidth(self.width);
|
|
744
|
-
simView.
|
|
432
|
+
simView.moveTo(leftOffset, rightOffset);
|
|
745
433
|
return simView.dynamicBlocks.contentBlocks.map(region => ({
|
|
746
434
|
...region,
|
|
747
435
|
start: Math.floor(region.start),
|
|
@@ -752,80 +440,16 @@ export function stateModelFactory(pluginManager) {
|
|
|
752
440
|
afterDisplayedRegionsSet(cb) {
|
|
753
441
|
self.afterDisplayedRegionsSetCallbacks.push(cb);
|
|
754
442
|
},
|
|
755
|
-
/**
|
|
756
|
-
* offset is the base-pair-offset in the displayed region, index is the index of the
|
|
757
|
-
* displayed region in the linear genome view
|
|
758
|
-
*
|
|
759
|
-
* @param start - object as `{start, end, offset, index}`
|
|
760
|
-
* @param end - object as `{start, end, offset, index}`
|
|
761
|
-
*/
|
|
762
|
-
moveTo(start, end) {
|
|
763
|
-
// find locations in the modellist
|
|
764
|
-
let bpSoFar = 0;
|
|
765
|
-
if (start.index === end.index) {
|
|
766
|
-
bpSoFar += end.offset - start.offset;
|
|
767
|
-
}
|
|
768
|
-
else {
|
|
769
|
-
const s = self.displayedRegions[start.index];
|
|
770
|
-
bpSoFar += s.end - s.start - start.offset;
|
|
771
|
-
if (end.index - start.index >= 2) {
|
|
772
|
-
for (let i = start.index + 1; i < end.index; i += 1) {
|
|
773
|
-
bpSoFar +=
|
|
774
|
-
self.displayedRegions[i].end - self.displayedRegions[i].start;
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
bpSoFar += end.offset;
|
|
778
|
-
}
|
|
779
|
-
const targetBpPerPx = bpSoFar /
|
|
780
|
-
(self.width -
|
|
781
|
-
self.interRegionPaddingWidth * (end.index - start.index));
|
|
782
|
-
const newBpPerPx = self.zoomTo(targetBpPerPx);
|
|
783
|
-
// If our target bpPerPx was smaller than the allowed minBpPerPx, adjust
|
|
784
|
-
// the scroll so the requested range is in the middle of the screen
|
|
785
|
-
let extraBp = 0;
|
|
786
|
-
if (targetBpPerPx < newBpPerPx) {
|
|
787
|
-
extraBp = ((newBpPerPx - targetBpPerPx) * self.width) / 2;
|
|
788
|
-
}
|
|
789
|
-
let bpToStart = -extraBp;
|
|
790
|
-
for (let i = 0; i < self.displayedRegions.length; i += 1) {
|
|
791
|
-
const region = self.displayedRegions[i];
|
|
792
|
-
if (start.index === i) {
|
|
793
|
-
bpToStart += start.offset;
|
|
794
|
-
break;
|
|
795
|
-
}
|
|
796
|
-
else {
|
|
797
|
-
bpToStart += region.end - region.start;
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
self.scrollTo(Math.round(bpToStart / self.bpPerPx) +
|
|
801
|
-
self.interRegionPaddingWidth * start.index);
|
|
802
|
-
},
|
|
803
443
|
horizontalScroll(distance) {
|
|
804
444
|
const oldOffsetPx = self.offsetPx;
|
|
805
445
|
// newOffsetPx is the actual offset after the scroll is clamped
|
|
806
446
|
const newOffsetPx = self.scrollTo(self.offsetPx + distance);
|
|
807
447
|
return newOffsetPx - oldOffsetPx;
|
|
808
448
|
},
|
|
809
|
-
/**
|
|
810
|
-
* scrolls the view to center on the given bp. if that is not in any
|
|
811
|
-
* of the displayed regions, does nothing
|
|
812
|
-
* @param bp - basepair at which you want to center the view
|
|
813
|
-
* @param refName - refName of the displayedRegion you are centering at
|
|
814
|
-
* @param regionIndex - index of the displayedRegion
|
|
815
|
-
*/
|
|
816
|
-
centerAt(bp, refName, regionIndex) {
|
|
817
|
-
const centerPx = self.bpToPx({
|
|
818
|
-
refName,
|
|
819
|
-
coord: bp,
|
|
820
|
-
regionNumber: regionIndex,
|
|
821
|
-
});
|
|
822
|
-
if (centerPx) {
|
|
823
|
-
self.scrollTo(Math.round(centerPx.offsetPx - self.width / 2));
|
|
824
|
-
}
|
|
825
|
-
},
|
|
826
449
|
center() {
|
|
827
450
|
const centerBp = self.totalBp / 2;
|
|
828
|
-
|
|
451
|
+
const centerPx = centerBp / self.bpPerPx;
|
|
452
|
+
self.scrollTo(Math.round(centerPx - self.width / 2));
|
|
829
453
|
},
|
|
830
454
|
showAllRegions() {
|
|
831
455
|
self.zoomTo(self.maxBpPerPx);
|
|
@@ -981,6 +605,13 @@ export function stateModelFactory(pluginManager) {
|
|
|
981
605
|
checked: !self.hideNoTracksActive,
|
|
982
606
|
onClick: self.toggleNoTracksActive,
|
|
983
607
|
},
|
|
608
|
+
{
|
|
609
|
+
label: 'Show gridlines',
|
|
610
|
+
icon: VisibilityIcon,
|
|
611
|
+
type: 'checkbox',
|
|
612
|
+
checked: self.showGridlines,
|
|
613
|
+
onClick: self.toggleShowGridlines,
|
|
614
|
+
},
|
|
984
615
|
{
|
|
985
616
|
label: 'Track labels',
|
|
986
617
|
icon: LabelIcon,
|
|
@@ -1094,6 +725,203 @@ export function stateModelFactory(pluginManager) {
|
|
|
1094
725
|
const blob = new Blob([html], { type: 'image/svg+xml' });
|
|
1095
726
|
saveAs(blob, opts.filename || 'image.svg');
|
|
1096
727
|
},
|
|
728
|
+
/**
|
|
729
|
+
* offset is the base-pair-offset in the displayed region, index is the index of the
|
|
730
|
+
* displayed region in the linear genome view
|
|
731
|
+
*
|
|
732
|
+
* @param start - object as `{start, end, offset, index}`
|
|
733
|
+
* @param end - object as `{start, end, offset, index}`
|
|
734
|
+
*/
|
|
735
|
+
moveTo(start, end) {
|
|
736
|
+
moveTo(self, start, end);
|
|
737
|
+
},
|
|
738
|
+
navToLocString(locString, optAssemblyName) {
|
|
739
|
+
const { assemblyNames } = self;
|
|
740
|
+
const { assemblyManager } = getSession(self);
|
|
741
|
+
const { isValidRefName } = assemblyManager;
|
|
742
|
+
const assemblyName = optAssemblyName || assemblyNames[0];
|
|
743
|
+
let parsedLocStrings;
|
|
744
|
+
const inputs = locString
|
|
745
|
+
.split(/(\s+)/)
|
|
746
|
+
.map(f => f.trim())
|
|
747
|
+
.filter(f => !!f);
|
|
748
|
+
// first try interpreting as a whitespace-separated sequence of
|
|
749
|
+
// multiple locstrings
|
|
750
|
+
try {
|
|
751
|
+
parsedLocStrings = inputs.map(l => parseLocString(l, ref => isValidRefName(ref, assemblyName)));
|
|
752
|
+
}
|
|
753
|
+
catch (e) {
|
|
754
|
+
// if this fails, try interpreting as a whitespace-separated refname,
|
|
755
|
+
// start, end if start and end are integer inputs
|
|
756
|
+
const [refName, start, end] = inputs;
|
|
757
|
+
if (`${e}`.match(/Unknown reference sequence/) &&
|
|
758
|
+
Number.isInteger(+start) &&
|
|
759
|
+
Number.isInteger(+end)) {
|
|
760
|
+
parsedLocStrings = [
|
|
761
|
+
parseLocString(refName + ':' + start + '..' + end, ref => isValidRefName(ref, assemblyName)),
|
|
762
|
+
];
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
throw e;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const locations = parsedLocStrings === null || parsedLocStrings === void 0 ? void 0 : parsedLocStrings.map(region => {
|
|
769
|
+
const asmName = region.assemblyName || assemblyName;
|
|
770
|
+
const asm = assemblyManager.get(asmName);
|
|
771
|
+
const { refName } = region;
|
|
772
|
+
if (!asm) {
|
|
773
|
+
throw new Error(`assembly ${asmName} not found`);
|
|
774
|
+
}
|
|
775
|
+
const { regions } = asm;
|
|
776
|
+
if (!regions) {
|
|
777
|
+
throw new Error(`regions not loaded yet for ${asmName}`);
|
|
778
|
+
}
|
|
779
|
+
const canonicalRefName = asm.getCanonicalRefName(region.refName);
|
|
780
|
+
if (!canonicalRefName) {
|
|
781
|
+
throw new Error(`Could not find refName ${refName} in ${asm.name}`);
|
|
782
|
+
}
|
|
783
|
+
const parentRegion = regions.find(region => region.refName === canonicalRefName);
|
|
784
|
+
if (!parentRegion) {
|
|
785
|
+
throw new Error(`Could not find refName ${refName} in ${asmName}`);
|
|
786
|
+
}
|
|
787
|
+
return {
|
|
788
|
+
...region,
|
|
789
|
+
assemblyName: asmName,
|
|
790
|
+
parentRegion,
|
|
791
|
+
};
|
|
792
|
+
});
|
|
793
|
+
if (locations.length === 1) {
|
|
794
|
+
const loc = locations[0];
|
|
795
|
+
self.setDisplayedRegions([
|
|
796
|
+
{ reversed: loc.reversed, ...loc.parentRegion },
|
|
797
|
+
]);
|
|
798
|
+
const { start, end, parentRegion } = loc;
|
|
799
|
+
this.navTo({
|
|
800
|
+
...loc,
|
|
801
|
+
start: clamp(start !== null && start !== void 0 ? start : 0, 0, parentRegion.end),
|
|
802
|
+
end: clamp(end !== null && end !== void 0 ? end : parentRegion.end, 0, parentRegion.end),
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
self.setDisplayedRegions(
|
|
807
|
+
// @ts-ignore
|
|
808
|
+
locations.map(r => (r.start === undefined ? r.parentRegion : r)));
|
|
809
|
+
self.showAllRegions();
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
/**
|
|
813
|
+
* Navigate to a location based on its refName and optionally start, end,
|
|
814
|
+
* and assemblyName. Can handle if there are multiple displayedRegions
|
|
815
|
+
* from same refName. Only navigates to a location if it is entirely
|
|
816
|
+
* within a displayedRegion. Navigates to the first matching location
|
|
817
|
+
* encountered.
|
|
818
|
+
*
|
|
819
|
+
* Throws an error if navigation was unsuccessful
|
|
820
|
+
*
|
|
821
|
+
* @param location - a proposed location to navigate to
|
|
822
|
+
*/
|
|
823
|
+
navTo(query) {
|
|
824
|
+
this.navToMultiple([query]);
|
|
825
|
+
},
|
|
826
|
+
navToMultiple(locations) {
|
|
827
|
+
const firstLocation = locations[0];
|
|
828
|
+
let { refName } = firstLocation;
|
|
829
|
+
const { start, end, assemblyName = self.assemblyNames[0], } = firstLocation;
|
|
830
|
+
if (start !== undefined && end !== undefined && start > end) {
|
|
831
|
+
throw new Error(`start "${start + 1}" is greater than end "${end}"`);
|
|
832
|
+
}
|
|
833
|
+
const session = getSession(self);
|
|
834
|
+
const { assemblyManager } = session;
|
|
835
|
+
const assembly = assemblyManager.get(assemblyName);
|
|
836
|
+
if (assembly) {
|
|
837
|
+
const canonicalRefName = assembly.getCanonicalRefName(refName);
|
|
838
|
+
if (canonicalRefName) {
|
|
839
|
+
refName = canonicalRefName;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
let s = start;
|
|
843
|
+
let e = end;
|
|
844
|
+
let refNameMatched = false;
|
|
845
|
+
const predicate = (r) => {
|
|
846
|
+
if (refName === r.refName) {
|
|
847
|
+
refNameMatched = true;
|
|
848
|
+
if (s === undefined) {
|
|
849
|
+
s = r.start;
|
|
850
|
+
}
|
|
851
|
+
if (e === undefined) {
|
|
852
|
+
e = r.end;
|
|
853
|
+
}
|
|
854
|
+
if (s >= r.start && s <= r.end && e <= r.end && e >= r.start) {
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
s = start;
|
|
858
|
+
e = end;
|
|
859
|
+
}
|
|
860
|
+
return false;
|
|
861
|
+
};
|
|
862
|
+
const lastIndex = findLastIndex(self.displayedRegions, predicate);
|
|
863
|
+
let index;
|
|
864
|
+
while (index !== lastIndex) {
|
|
865
|
+
try {
|
|
866
|
+
const previousIndex = index;
|
|
867
|
+
index = self.displayedRegions
|
|
868
|
+
.slice(previousIndex === undefined ? 0 : previousIndex + 1)
|
|
869
|
+
.findIndex(predicate);
|
|
870
|
+
if (previousIndex !== undefined) {
|
|
871
|
+
index += previousIndex + 1;
|
|
872
|
+
}
|
|
873
|
+
if (!refNameMatched) {
|
|
874
|
+
throw new Error(`could not find a region with refName "${refName}"`);
|
|
875
|
+
}
|
|
876
|
+
if (s === undefined) {
|
|
877
|
+
throw new Error(`could not find a region with refName "${refName}" that contained an end position ${e}`);
|
|
878
|
+
}
|
|
879
|
+
if (e === undefined) {
|
|
880
|
+
throw new Error(`could not find a region with refName "${refName}" that contained a start position ${s + 1}`);
|
|
881
|
+
}
|
|
882
|
+
if (index === -1) {
|
|
883
|
+
throw new Error(`could not find a region that completely contained "${assembleLocString(firstLocation)}"`);
|
|
884
|
+
}
|
|
885
|
+
if (locations.length === 1) {
|
|
886
|
+
const f = self.displayedRegions[index];
|
|
887
|
+
this.moveTo({ index, offset: f.reversed ? f.end - e : s - f.start }, { index, offset: f.reversed ? f.end - s : e - f.start });
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
let locationIndex = 0;
|
|
891
|
+
let locationStart = 0;
|
|
892
|
+
let locationEnd = 0;
|
|
893
|
+
for (locationIndex; locationIndex < locations.length; locationIndex++) {
|
|
894
|
+
const location = locations[locationIndex];
|
|
895
|
+
const region = self.displayedRegions[index + locationIndex];
|
|
896
|
+
locationStart = location.start || region.start;
|
|
897
|
+
locationEnd = location.end || region.end;
|
|
898
|
+
if (location.refName !== region.refName) {
|
|
899
|
+
throw new Error(`Entered location ${assembleLocString(location)} does not match with displayed regions`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
locationIndex -= 1;
|
|
903
|
+
const startDisplayedRegion = self.displayedRegions[index];
|
|
904
|
+
const endDisplayedRegion = self.displayedRegions[index + locationIndex];
|
|
905
|
+
this.moveTo({
|
|
906
|
+
index,
|
|
907
|
+
offset: startDisplayedRegion.reversed
|
|
908
|
+
? startDisplayedRegion.end - e
|
|
909
|
+
: s - startDisplayedRegion.start,
|
|
910
|
+
}, {
|
|
911
|
+
index: index + locationIndex,
|
|
912
|
+
offset: endDisplayedRegion.reversed
|
|
913
|
+
? endDisplayedRegion.end - locationStart
|
|
914
|
+
: locationEnd - endDisplayedRegion.start,
|
|
915
|
+
});
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
catch (error) {
|
|
919
|
+
if (index === lastIndex) {
|
|
920
|
+
throw error;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
},
|
|
1097
925
|
}))
|
|
1098
926
|
.views(self => ({
|
|
1099
927
|
rubberBandMenuItems() {
|
|
@@ -1103,9 +931,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
1103
931
|
icon: ZoomInIcon,
|
|
1104
932
|
onClick: () => {
|
|
1105
933
|
const { leftOffset, rightOffset } = self;
|
|
1106
|
-
|
|
1107
|
-
self.moveTo(leftOffset, rightOffset);
|
|
1108
|
-
}
|
|
934
|
+
self.moveTo(leftOffset, rightOffset);
|
|
1109
935
|
},
|
|
1110
936
|
},
|
|
1111
937
|
{
|
|
@@ -1117,6 +943,34 @@ export function stateModelFactory(pluginManager) {
|
|
|
1117
943
|
},
|
|
1118
944
|
];
|
|
1119
945
|
},
|
|
946
|
+
bpToPx({ refName, coord, regionNumber, }) {
|
|
947
|
+
return bpToPx({ refName, coord, regionNumber, self });
|
|
948
|
+
},
|
|
949
|
+
/**
|
|
950
|
+
* scrolls the view to center on the given bp. if that is not in any
|
|
951
|
+
* of the displayed regions, does nothing
|
|
952
|
+
* @param coord - basepair at which you want to center the view
|
|
953
|
+
* @param refName - refName of the displayedRegion you are centering at
|
|
954
|
+
* @param regionNumber - index of the displayedRegion
|
|
955
|
+
*/
|
|
956
|
+
centerAt(coord, refName, regionNumber) {
|
|
957
|
+
const centerPx = this.bpToPx({
|
|
958
|
+
refName,
|
|
959
|
+
coord,
|
|
960
|
+
regionNumber,
|
|
961
|
+
});
|
|
962
|
+
if (centerPx) {
|
|
963
|
+
self.scrollTo(Math.round(centerPx.offsetPx - self.width / 2));
|
|
964
|
+
}
|
|
965
|
+
},
|
|
966
|
+
pxToBp(px) {
|
|
967
|
+
return pxToBp(self, px);
|
|
968
|
+
},
|
|
969
|
+
get centerLineInfo() {
|
|
970
|
+
return self.displayedRegions.length
|
|
971
|
+
? this.pxToBp(self.width / 2)
|
|
972
|
+
: undefined;
|
|
973
|
+
},
|
|
1120
974
|
}));
|
|
1121
975
|
}
|
|
1122
976
|
export { renderToSvg, RefNameAutocomplete, SearchBox };
|