@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.
Files changed (38) hide show
  1. package/dist/LinearGenomeView/components/{VerticalGuides.d.ts → Gridlines.d.ts} +0 -0
  2. package/dist/LinearGenomeView/components/{VerticalGuides.js → Gridlines.js} +1 -1
  3. package/dist/LinearGenomeView/components/Gridlines.js.map +1 -0
  4. package/dist/LinearGenomeView/components/OverviewRubberBand.js +13 -10
  5. package/dist/LinearGenomeView/components/OverviewRubberBand.js.map +1 -1
  6. package/dist/LinearGenomeView/components/RubberBand.js +0 -1
  7. package/dist/LinearGenomeView/components/RubberBand.js.map +1 -1
  8. package/dist/LinearGenomeView/components/SequenceDialog.js +1 -1
  9. package/dist/LinearGenomeView/components/SequenceDialog.js.map +1 -1
  10. package/dist/LinearGenomeView/components/TracksContainer.js +2 -2
  11. package/dist/LinearGenomeView/components/TracksContainer.js.map +1 -1
  12. package/dist/LinearGenomeView/index.d.ts +63 -76
  13. package/dist/LinearGenomeView/index.js +239 -365
  14. package/dist/LinearGenomeView/index.js.map +1 -1
  15. package/esm/LinearGenomeView/components/{VerticalGuides.d.ts → Gridlines.d.ts} +0 -0
  16. package/esm/LinearGenomeView/components/{VerticalGuides.js → Gridlines.js} +1 -1
  17. package/esm/LinearGenomeView/components/Gridlines.js.map +1 -0
  18. package/esm/LinearGenomeView/components/OverviewRubberBand.js +13 -10
  19. package/esm/LinearGenomeView/components/OverviewRubberBand.js.map +1 -1
  20. package/esm/LinearGenomeView/components/RubberBand.js +0 -1
  21. package/esm/LinearGenomeView/components/RubberBand.js.map +1 -1
  22. package/esm/LinearGenomeView/components/SequenceDialog.js +1 -1
  23. package/esm/LinearGenomeView/components/SequenceDialog.js.map +1 -1
  24. package/esm/LinearGenomeView/components/TracksContainer.js +2 -2
  25. package/esm/LinearGenomeView/components/TracksContainer.js.map +1 -1
  26. package/esm/LinearGenomeView/index.d.ts +63 -76
  27. package/esm/LinearGenomeView/index.js +243 -389
  28. package/esm/LinearGenomeView/index.js.map +1 -1
  29. package/package.json +3 -3
  30. package/src/LinearGenomeView/components/{VerticalGuides.tsx → Gridlines.tsx} +0 -0
  31. package/src/LinearGenomeView/components/OverviewRubberBand.tsx +14 -19
  32. package/src/LinearGenomeView/components/RubberBand.tsx +0 -1
  33. package/src/LinearGenomeView/components/SequenceDialog.tsx +1 -1
  34. package/src/LinearGenomeView/components/TracksContainer.tsx +2 -2
  35. package/src/LinearGenomeView/index.test.ts +13 -36
  36. package/src/LinearGenomeView/index.tsx +360 -519
  37. package/dist/LinearGenomeView/components/VerticalGuides.js.map +0 -1
  38. package/esm/LinearGenomeView/components/VerticalGuides.js.map +0 -1
@@ -14,7 +14,6 @@ import {
14
14
  measureText,
15
15
  parseLocString,
16
16
  springAnimate,
17
- viewBpToPx,
18
17
  } from '@jbrowse/core/util'
19
18
  import BaseResult from '@jbrowse/core/TextSearch/BaseResults'
20
19
  import { BlockSet, BaseBlock } from '@jbrowse/core/util/blockTypes'
@@ -33,9 +32,10 @@ import {
33
32
  } from 'mobx-state-tree'
34
33
 
35
34
  import Base1DView from '@jbrowse/core/util/Base1DViewModel'
36
- import PluginManager from '@jbrowse/core/PluginManager'
37
- import clone from 'clone'
35
+ import { moveTo, pxToBp, bpToPx } from '@jbrowse/core/util/Base1DUtils'
38
36
  import { saveAs } from 'file-saver'
37
+ import clone from 'clone'
38
+ import PluginManager from '@jbrowse/core/PluginManager'
39
39
 
40
40
  // icons
41
41
  import { TrackSelector as TrackSelectorIcon } from '@jbrowse/core/ui/Icons'
@@ -144,6 +144,7 @@ export function stateModelFactory(pluginManager: PluginManager) {
144
144
  const setting = localStorageGetItem('lgv-showCytobands')
145
145
  return setting !== undefined && setting !== null ? !!+setting : true
146
146
  }),
147
+ showGridlines: true,
147
148
  }),
148
149
  )
149
150
  .volatile(() => ({
@@ -288,105 +289,6 @@ export function stateModelFactory(pluginManager: PluginManager) {
288
289
  }
289
290
  },
290
291
 
291
- bpToPx({
292
- refName,
293
- coord,
294
- regionNumber,
295
- }: {
296
- refName: string
297
- coord: number
298
- regionNumber?: number
299
- }) {
300
- return viewBpToPx({ refName, coord, regionNumber, self })
301
- },
302
- /**
303
- *
304
- * @param px - px in the view area, return value is the displayed regions
305
- * @returns BpOffset of the displayed region that it lands in
306
- */
307
- pxToBp(px: number) {
308
- let bpSoFar = 0
309
- const bp = (self.offsetPx + px) * self.bpPerPx
310
- const n = self.displayedRegions.length
311
- if (bp < 0) {
312
- const region = self.displayedRegions[0]
313
- const offset = bp
314
- const snap = getSnapshot(region)
315
- return {
316
- // xref https://github.com/mobxjs/mobx-state-tree/issues/1524 for Omit
317
- ...(snap as Omit<typeof snap, symbol>),
318
- oob: true,
319
- coord: region.reversed
320
- ? Math.floor(region.end - offset) + 1
321
- : Math.floor(region.start + offset) + 1,
322
- offset,
323
- index: 0,
324
- }
325
- }
326
-
327
- const interRegionPaddingBp = self.interRegionPaddingWidth * self.bpPerPx
328
- const minimumBlockBp = self.minimumBlockWidth * self.bpPerPx
329
-
330
- for (let index = 0; index < self.displayedRegions.length; index += 1) {
331
- const region = self.displayedRegions[index]
332
- const len = region.end - region.start
333
- const offset = bp - bpSoFar
334
- if (len + bpSoFar > bp && bpSoFar <= bp) {
335
- const snap = getSnapshot(region)
336
- return {
337
- // xref https://github.com/mobxjs/mobx-state-tree/issues/1524 for Omit
338
- ...(snap as Omit<typeof snap, symbol>),
339
- oob: false,
340
- offset,
341
- coord: region.reversed
342
- ? Math.floor(region.end - offset) + 1
343
- : Math.floor(region.start + offset) + 1,
344
- index,
345
- }
346
- }
347
-
348
- // add the interRegionPaddingWidth if the boundary is in the screen
349
- // e.g. offset>0 && offset<width
350
- if (
351
- region.end - region.start > minimumBlockBp &&
352
- offset / self.bpPerPx > 0 &&
353
- offset / self.bpPerPx < self.width
354
- ) {
355
- bpSoFar += len + interRegionPaddingBp
356
- } else {
357
- bpSoFar += len
358
- }
359
- }
360
-
361
- if (bp >= bpSoFar) {
362
- const region = self.displayedRegions[n - 1]
363
- const len = region.end - region.start
364
- const offset = bp - bpSoFar + len
365
- const snap = getSnapshot(region)
366
- return {
367
- // xref https://github.com/mobxjs/mobx-state-tree/issues/1524 for Omit
368
- ...(snap as Omit<typeof snap, symbol>),
369
- oob: true,
370
- offset,
371
- coord: region.reversed
372
- ? Math.floor(region.end - offset) + 1
373
- : Math.floor(region.start + offset) + 1,
374
- index: n - 1,
375
- }
376
- }
377
- return {
378
- coord: 0,
379
- index: 0,
380
- refName: '',
381
- oob: true,
382
- assemblyName: '',
383
- offset: 0,
384
- start: 0,
385
- end: 0,
386
- reversed: false,
387
- }
388
- },
389
-
390
292
  getTrack(id: string) {
391
293
  return self.tracks.find(t => t.configuration.trackId === id)
392
294
  },
@@ -437,12 +339,6 @@ export function stateModelFactory(pluginManager: PluginManager) {
437
339
 
438
340
  return allActions
439
341
  },
440
-
441
- get centerLineInfo() {
442
- return self.displayedRegions.length
443
- ? this.pxToBp(self.width / 2)
444
- : undefined
445
- },
446
342
  }))
447
343
  .actions(self => ({
448
344
  setShowCytobands(flag: boolean) {
@@ -466,6 +362,9 @@ export function stateModelFactory(pluginManager: PluginManager) {
466
362
  toggleNoTracksActive() {
467
363
  self.hideNoTracksActive = !self.hideNoTracksActive
468
364
  },
365
+ toggleShowGridlines() {
366
+ self.showGridlines = !self.showGridlines
367
+ },
469
368
 
470
369
  scrollTo(offsetPx: number) {
471
370
  const newOffsetPx = clamp(offsetPx, self.minOffset, self.maxOffset)
@@ -651,429 +550,83 @@ export function stateModelFactory(pluginManager: PluginManager) {
651
550
  throw new Error(`invalid track selector type ${self.trackSelectorType}`)
652
551
  },
653
552
 
654
- navToLocString(locString: string, optAssemblyName?: string) {
655
- const { assemblyNames } = self
656
- const { assemblyManager } = getSession(self)
657
- const { isValidRefName } = assemblyManager
658
- const assemblyName = optAssemblyName || assemblyNames[0]
659
- let parsedLocStrings
660
- const inputs = locString
661
- .split(/(\s+)/)
662
- .map(f => f.trim())
663
- .filter(f => !!f)
664
-
665
- // first try interpreting as a whitespace-separated sequence of
666
- // multiple locstrings
667
- try {
668
- parsedLocStrings = inputs.map(l =>
669
- parseLocString(l, ref => isValidRefName(ref, assemblyName)),
670
- )
671
- } catch (e) {
672
- // if this fails, try interpreting as a whitespace-separated refname,
673
- // start, end if start and end are integer inputs
674
- const [refName, start, end] = inputs
675
- if (
676
- `${e}`.match(/Unknown reference sequence/) &&
677
- Number.isInteger(+start) &&
678
- Number.isInteger(+end)
679
- ) {
680
- parsedLocStrings = [
681
- parseLocString(refName + ':' + start + '..' + end, ref =>
682
- isValidRefName(ref, assemblyName),
683
- ),
684
- ]
685
- } else {
686
- throw e
687
- }
688
- }
553
+ /**
554
+ * Helper method for the fetchSequence.
555
+ * Retrieves the corresponding regions that were selected by the rubberband
556
+ *
557
+ * @param leftOffset - `object as {start, end, index, offset}`, offset = start of user drag
558
+ * @param rightOffset - `object as {start, end, index, offset}`, offset = end of user drag
559
+ * @returns array of Region[]
560
+ */
561
+ getSelectedRegions(leftOffset?: BpOffset, rightOffset?: BpOffset) {
562
+ const snap = getSnapshot(self)
563
+ const simView = Base1DView.create({
564
+ // xref https://github.com/mobxjs/mobx-state-tree/issues/1524 for Omit
565
+ ...(snap as Omit<typeof self, symbol>),
566
+ interRegionPaddingWidth: self.interRegionPaddingWidth,
567
+ })
689
568
 
690
- const locations = parsedLocStrings?.map(region => {
691
- const asmName = region.assemblyName || assemblyName
692
- const asm = assemblyManager.get(asmName)
693
- const { refName } = region
694
- if (!asm) {
695
- throw new Error(`assembly ${asmName} not found`)
696
- }
697
- const { regions } = asm
698
- if (!regions) {
699
- throw new Error(`regions not loaded yet for ${asmName}`)
700
- }
701
- const canonicalRefName = asm.getCanonicalRefName(region.refName)
702
- if (!canonicalRefName) {
703
- throw new Error(`Could not find refName ${refName} in ${asm.name}`)
704
- }
705
- const parentRegion = regions.find(
706
- region => region.refName === canonicalRefName,
707
- )
708
- if (!parentRegion) {
709
- throw new Error(`Could not find refName ${refName} in ${asmName}`)
710
- }
569
+ simView.setVolatileWidth(self.width)
570
+ simView.moveTo(leftOffset, rightOffset)
711
571
 
712
- return {
713
- ...region,
714
- assemblyName: asmName,
715
- parentRegion,
716
- }
717
- })
572
+ return simView.dynamicBlocks.contentBlocks.map(region => ({
573
+ ...region,
574
+ start: Math.floor(region.start),
575
+ end: Math.ceil(region.end),
576
+ }))
577
+ },
718
578
 
719
- if (locations.length === 1) {
720
- const loc = locations[0]
721
- this.setDisplayedRegions([
722
- { reversed: loc.reversed, ...loc.parentRegion },
723
- ])
724
- const { start, end, parentRegion } = loc
579
+ // schedule something to be run after the next time displayedRegions is set
580
+ afterDisplayedRegionsSet(cb: Function) {
581
+ self.afterDisplayedRegionsSetCallbacks.push(cb)
582
+ },
725
583
 
726
- this.navTo({
727
- ...loc,
728
- start: clamp(start ?? 0, 0, parentRegion.end),
729
- end: clamp(end ?? parentRegion.end, 0, parentRegion.end),
730
- })
731
- } else {
732
- this.setDisplayedRegions(
733
- // @ts-ignore
734
- locations.map(r => (r.start === undefined ? r.parentRegion : r)),
735
- )
736
- this.showAllRegions()
737
- }
584
+ horizontalScroll(distance: number) {
585
+ const oldOffsetPx = self.offsetPx
586
+ // newOffsetPx is the actual offset after the scroll is clamped
587
+ const newOffsetPx = self.scrollTo(self.offsetPx + distance)
588
+ return newOffsetPx - oldOffsetPx
738
589
  },
739
590
 
740
- /**
741
- * Navigate to a location based on its refName and optionally start, end,
742
- * and assemblyName. Can handle if there are multiple displayedRegions
743
- * from same refName. Only navigates to a location if it is entirely
744
- * within a displayedRegion. Navigates to the first matching location
745
- * encountered.
746
- *
747
- * Throws an error if navigation was unsuccessful
748
- *
749
- * @param location - a proposed location to navigate to
750
- */
751
- navTo(query: NavLocation) {
752
- this.navToMultiple([query])
591
+ center() {
592
+ const centerBp = self.totalBp / 2
593
+ const centerPx = centerBp / self.bpPerPx
594
+ self.scrollTo(Math.round(centerPx - self.width / 2))
753
595
  },
754
596
 
755
- navToMultiple(locations: NavLocation[]) {
756
- const firstLocation = locations[0]
757
- let { refName } = firstLocation
758
- const {
759
- start,
760
- end,
761
- assemblyName = self.assemblyNames[0],
762
- } = firstLocation
597
+ showAllRegions() {
598
+ self.zoomTo(self.maxBpPerPx)
599
+ this.center()
600
+ },
763
601
 
764
- if (start !== undefined && end !== undefined && start > end) {
765
- throw new Error(`start "${start + 1}" is greater than end "${end}"`)
766
- }
602
+ showAllRegionsInAssembly(assemblyName?: string) {
767
603
  const session = getSession(self)
768
604
  const { assemblyManager } = session
769
- const assembly = assemblyManager.get(assemblyName)
770
- if (assembly) {
771
- const canonicalRefName = assembly.getCanonicalRefName(refName)
772
- if (canonicalRefName) {
773
- refName = canonicalRefName
605
+ if (!assemblyName) {
606
+ const assemblyNames = [
607
+ ...new Set(
608
+ self.displayedRegions.map(region => region.assemblyName),
609
+ ),
610
+ ]
611
+ if (assemblyNames.length > 1) {
612
+ session.notify(
613
+ `Can't perform this with multiple assemblies currently`,
614
+ )
615
+ return
774
616
  }
617
+
618
+ ;[assemblyName] = assemblyNames
775
619
  }
776
- let s = start
777
- let e = end
778
- let refNameMatched = false
779
- const predicate = (r: Region) => {
780
- if (refName === r.refName) {
781
- refNameMatched = true
782
- if (s === undefined) {
783
- s = r.start
784
- }
785
- if (e === undefined) {
786
- e = r.end
787
- }
788
- if (s >= r.start && s <= r.end && e <= r.end && e >= r.start) {
789
- return true
790
- }
791
- s = start
792
- e = end
620
+ const assembly = assemblyManager.get(assemblyName)
621
+ if (assembly) {
622
+ const { regions } = assembly
623
+ if (regions) {
624
+ this.setDisplayedRegions(regions)
625
+ self.zoomTo(self.maxBpPerPx)
626
+ this.center()
793
627
  }
794
- return false
795
628
  }
796
-
797
- const lastIndex = findLastIndex(self.displayedRegions, predicate)
798
- let index
799
- while (index !== lastIndex) {
800
- try {
801
- const previousIndex: number | undefined = index
802
- index = self.displayedRegions
803
- .slice(previousIndex === undefined ? 0 : previousIndex + 1)
804
- .findIndex(predicate)
805
- if (previousIndex !== undefined) {
806
- index += previousIndex + 1
807
- }
808
- if (!refNameMatched) {
809
- throw new Error(
810
- `could not find a region with refName "${refName}"`,
811
- )
812
- }
813
- if (s === undefined) {
814
- throw new Error(
815
- `could not find a region with refName "${refName}" that contained an end position ${e}`,
816
- )
817
- }
818
- if (e === undefined) {
819
- throw new Error(
820
- `could not find a region with refName "${refName}" that contained a start position ${
821
- s + 1
822
- }`,
823
- )
824
- }
825
- if (index === -1) {
826
- throw new Error(
827
- `could not find a region that completely contained "${assembleLocString(
828
- firstLocation,
829
- )}"`,
830
- )
831
- }
832
- if (locations.length === 1) {
833
- const f = self.displayedRegions[index]
834
- this.moveTo(
835
- { index, offset: f.reversed ? f.end - e : s - f.start },
836
- { index, offset: f.reversed ? f.end - s : e - f.start },
837
- )
838
- return
839
- }
840
- let locationIndex = 0
841
- let locationStart = 0
842
- let locationEnd = 0
843
- for (
844
- locationIndex;
845
- locationIndex < locations.length;
846
- locationIndex++
847
- ) {
848
- const location = locations[locationIndex]
849
- const region = self.displayedRegions[index + locationIndex]
850
- locationStart = location.start || region.start
851
- locationEnd = location.end || region.end
852
- if (location.refName !== region.refName) {
853
- throw new Error(
854
- `Entered location ${assembleLocString(
855
- location,
856
- )} does not match with displayed regions`,
857
- )
858
- }
859
- }
860
- locationIndex -= 1
861
- const startDisplayedRegion = self.displayedRegions[index]
862
- const endDisplayedRegion =
863
- self.displayedRegions[index + locationIndex]
864
- this.moveTo(
865
- {
866
- index,
867
- offset: startDisplayedRegion.reversed
868
- ? startDisplayedRegion.end - e
869
- : s - startDisplayedRegion.start,
870
- },
871
- {
872
- index: index + locationIndex,
873
- offset: endDisplayedRegion.reversed
874
- ? endDisplayedRegion.end - locationStart
875
- : locationEnd - endDisplayedRegion.start,
876
- },
877
- )
878
- return
879
- } catch (error) {
880
- if (index === lastIndex) {
881
- throw error
882
- }
883
- }
884
- }
885
- },
886
-
887
- /**
888
- * Navigate to a location based on user clicking and dragging on the
889
- * overview scale bar to select a region to zoom into.
890
- * Can handle if there are multiple displayedRegions from same refName.
891
- * Only navigates to a location if it is entirely within a displayedRegion.
892
- *
893
- * @param leftPx- `object as {start, end, index, offset}`, offset = start of user drag
894
- * @param rightPx- `object as {start, end, index, offset}`, offset = end of user drag
895
- */
896
- zoomToDisplayedRegions(leftPx: BpOffset, rightPx: BpOffset) {
897
- if (leftPx === undefined || rightPx === undefined) {
898
- return
899
- }
900
-
901
- const singleRefSeq =
902
- leftPx.refName === rightPx.refName && leftPx.index === rightPx.index
903
- // zooming into one displayed Region
904
- if (
905
- (singleRefSeq && rightPx.offset < leftPx.offset) ||
906
- leftPx.index > rightPx.index
907
- ) {
908
- ;[leftPx, rightPx] = [rightPx, leftPx]
909
- }
910
- const startOffset = {
911
- start: leftPx.start,
912
- end: leftPx.end,
913
- index: leftPx.index,
914
- offset: leftPx.offset,
915
- }
916
- const endOffset = {
917
- start: rightPx.start,
918
- end: rightPx.end,
919
- index: rightPx.index,
920
- offset: rightPx.offset,
921
- }
922
- if (startOffset && endOffset) {
923
- this.moveTo(startOffset, endOffset)
924
- } else {
925
- const session = getSession(self)
926
- session.notify('No regions found to navigate to', 'warning')
927
- }
928
- },
929
- /**
930
- * Helper method for the fetchSequence.
931
- * Retrieves the corresponding regions that were selected by the rubberband
932
- *
933
- * @param leftOffset - `object as {start, end, index, offset}`, offset = start of user drag
934
- * @param rightOffset - `object as {start, end, index, offset}`, offset = end of user drag
935
- * @returns array of Region[]
936
- */
937
- getSelectedRegions(
938
- leftOffset: BpOffset | undefined,
939
- rightOffset: BpOffset | undefined,
940
- ) {
941
- const snap = getSnapshot(self)
942
- const simView = Base1DView.create({
943
- // xref https://github.com/mobxjs/mobx-state-tree/issues/1524 for Omit
944
- ...(snap as Omit<typeof self, symbol>),
945
- interRegionPaddingWidth: self.interRegionPaddingWidth,
946
- })
947
-
948
- simView.setVolatileWidth(self.width)
949
- simView.zoomToDisplayedRegions(leftOffset, rightOffset)
950
-
951
- return simView.dynamicBlocks.contentBlocks.map(region => ({
952
- ...region,
953
- start: Math.floor(region.start),
954
- end: Math.ceil(region.end),
955
- }))
956
- },
957
-
958
- // schedule something to be run after the next time displayedRegions is set
959
- afterDisplayedRegionsSet(cb: Function) {
960
- self.afterDisplayedRegionsSetCallbacks.push(cb)
961
- },
962
- /**
963
- * offset is the base-pair-offset in the displayed region, index is the index of the
964
- * displayed region in the linear genome view
965
- *
966
- * @param start - object as `{start, end, offset, index}`
967
- * @param end - object as `{start, end, offset, index}`
968
- */
969
- moveTo(start: BpOffset, end: BpOffset) {
970
- // find locations in the modellist
971
- let bpSoFar = 0
972
-
973
- if (start.index === end.index) {
974
- bpSoFar += end.offset - start.offset
975
- } else {
976
- const s = self.displayedRegions[start.index]
977
- bpSoFar += s.end - s.start - start.offset
978
- if (end.index - start.index >= 2) {
979
- for (let i = start.index + 1; i < end.index; i += 1) {
980
- bpSoFar +=
981
- self.displayedRegions[i].end - self.displayedRegions[i].start
982
- }
983
- }
984
- bpSoFar += end.offset
985
- }
986
- const targetBpPerPx =
987
- bpSoFar /
988
- (self.width -
989
- self.interRegionPaddingWidth * (end.index - start.index))
990
- const newBpPerPx = self.zoomTo(targetBpPerPx)
991
- // If our target bpPerPx was smaller than the allowed minBpPerPx, adjust
992
- // the scroll so the requested range is in the middle of the screen
993
- let extraBp = 0
994
- if (targetBpPerPx < newBpPerPx) {
995
- extraBp = ((newBpPerPx - targetBpPerPx) * self.width) / 2
996
- }
997
-
998
- let bpToStart = -extraBp
999
- for (let i = 0; i < self.displayedRegions.length; i += 1) {
1000
- const region = self.displayedRegions[i]
1001
- if (start.index === i) {
1002
- bpToStart += start.offset
1003
- break
1004
- } else {
1005
- bpToStart += region.end - region.start
1006
- }
1007
- }
1008
- self.scrollTo(
1009
- Math.round(bpToStart / self.bpPerPx) +
1010
- self.interRegionPaddingWidth * start.index,
1011
- )
1012
- },
1013
-
1014
- horizontalScroll(distance: number) {
1015
- const oldOffsetPx = self.offsetPx
1016
- // newOffsetPx is the actual offset after the scroll is clamped
1017
- const newOffsetPx = self.scrollTo(self.offsetPx + distance)
1018
- return newOffsetPx - oldOffsetPx
1019
- },
1020
-
1021
- /**
1022
- * scrolls the view to center on the given bp. if that is not in any
1023
- * of the displayed regions, does nothing
1024
- * @param bp - basepair at which you want to center the view
1025
- * @param refName - refName of the displayedRegion you are centering at
1026
- * @param regionIndex - index of the displayedRegion
1027
- */
1028
- centerAt(bp: number, refName: string, regionIndex: number) {
1029
- const centerPx = self.bpToPx({
1030
- refName,
1031
- coord: bp,
1032
- regionNumber: regionIndex,
1033
- })
1034
- if (centerPx) {
1035
- self.scrollTo(Math.round(centerPx.offsetPx - self.width / 2))
1036
- }
1037
- },
1038
-
1039
- center() {
1040
- const centerBp = self.totalBp / 2
1041
- self.scrollTo(Math.round(centerBp / self.bpPerPx - self.width / 2))
1042
- },
1043
-
1044
- showAllRegions() {
1045
- self.zoomTo(self.maxBpPerPx)
1046
- this.center()
1047
- },
1048
-
1049
- showAllRegionsInAssembly(assemblyName?: string) {
1050
- const session = getSession(self)
1051
- const { assemblyManager } = session
1052
- if (!assemblyName) {
1053
- const assemblyNames = [
1054
- ...new Set(
1055
- self.displayedRegions.map(region => region.assemblyName),
1056
- ),
1057
- ]
1058
- if (assemblyNames.length > 1) {
1059
- session.notify(
1060
- `Can't perform this with multiple assemblies currently`,
1061
- )
1062
- return
1063
- }
1064
-
1065
- ;[assemblyName] = assemblyNames
1066
- }
1067
- const assembly = assemblyManager.get(assemblyName)
1068
- if (assembly) {
1069
- const { regions } = assembly
1070
- if (regions) {
1071
- this.setDisplayedRegions(regions)
1072
- self.zoomTo(self.maxBpPerPx)
1073
- this.center()
1074
- }
1075
- }
1076
- },
629
+ },
1077
630
 
1078
631
  setDraggingTrackId(idx?: string) {
1079
632
  self.draggingTrackId = idx
@@ -1220,6 +773,13 @@ export function stateModelFactory(pluginManager: PluginManager) {
1220
773
  checked: !self.hideNoTracksActive,
1221
774
  onClick: self.toggleNoTracksActive,
1222
775
  },
776
+ {
777
+ label: 'Show gridlines',
778
+ icon: VisibilityIcon,
779
+ type: 'checkbox',
780
+ checked: self.showGridlines,
781
+ onClick: self.toggleShowGridlines,
782
+ },
1223
783
  {
1224
784
  label: 'Track labels',
1225
785
  icon: LabelIcon,
@@ -1346,6 +906,249 @@ export function stateModelFactory(pluginManager: PluginManager) {
1346
906
  const blob = new Blob([html], { type: 'image/svg+xml' })
1347
907
  saveAs(blob, opts.filename || 'image.svg')
1348
908
  },
909
+ /**
910
+ * offset is the base-pair-offset in the displayed region, index is the index of the
911
+ * displayed region in the linear genome view
912
+ *
913
+ * @param start - object as `{start, end, offset, index}`
914
+ * @param end - object as `{start, end, offset, index}`
915
+ */
916
+ moveTo(start?: BpOffset, end?: BpOffset) {
917
+ moveTo(self, start, end)
918
+ },
919
+
920
+ navToLocString(locString: string, optAssemblyName?: string) {
921
+ const { assemblyNames } = self
922
+ const { assemblyManager } = getSession(self)
923
+ const { isValidRefName } = assemblyManager
924
+ const assemblyName = optAssemblyName || assemblyNames[0]
925
+ let parsedLocStrings
926
+ const inputs = locString
927
+ .split(/(\s+)/)
928
+ .map(f => f.trim())
929
+ .filter(f => !!f)
930
+
931
+ // first try interpreting as a whitespace-separated sequence of
932
+ // multiple locstrings
933
+ try {
934
+ parsedLocStrings = inputs.map(l =>
935
+ parseLocString(l, ref => isValidRefName(ref, assemblyName)),
936
+ )
937
+ } catch (e) {
938
+ // if this fails, try interpreting as a whitespace-separated refname,
939
+ // start, end if start and end are integer inputs
940
+ const [refName, start, end] = inputs
941
+ if (
942
+ `${e}`.match(/Unknown reference sequence/) &&
943
+ Number.isInteger(+start) &&
944
+ Number.isInteger(+end)
945
+ ) {
946
+ parsedLocStrings = [
947
+ parseLocString(refName + ':' + start + '..' + end, ref =>
948
+ isValidRefName(ref, assemblyName),
949
+ ),
950
+ ]
951
+ } else {
952
+ throw e
953
+ }
954
+ }
955
+
956
+ const locations = parsedLocStrings?.map(region => {
957
+ const asmName = region.assemblyName || assemblyName
958
+ const asm = assemblyManager.get(asmName)
959
+ const { refName } = region
960
+ if (!asm) {
961
+ throw new Error(`assembly ${asmName} not found`)
962
+ }
963
+ const { regions } = asm
964
+ if (!regions) {
965
+ throw new Error(`regions not loaded yet for ${asmName}`)
966
+ }
967
+ const canonicalRefName = asm.getCanonicalRefName(region.refName)
968
+ if (!canonicalRefName) {
969
+ throw new Error(`Could not find refName ${refName} in ${asm.name}`)
970
+ }
971
+ const parentRegion = regions.find(
972
+ region => region.refName === canonicalRefName,
973
+ )
974
+ if (!parentRegion) {
975
+ throw new Error(`Could not find refName ${refName} in ${asmName}`)
976
+ }
977
+
978
+ return {
979
+ ...region,
980
+ assemblyName: asmName,
981
+ parentRegion,
982
+ }
983
+ })
984
+
985
+ if (locations.length === 1) {
986
+ const loc = locations[0]
987
+ self.setDisplayedRegions([
988
+ { reversed: loc.reversed, ...loc.parentRegion },
989
+ ])
990
+ const { start, end, parentRegion } = loc
991
+
992
+ this.navTo({
993
+ ...loc,
994
+ start: clamp(start ?? 0, 0, parentRegion.end),
995
+ end: clamp(end ?? parentRegion.end, 0, parentRegion.end),
996
+ })
997
+ } else {
998
+ self.setDisplayedRegions(
999
+ // @ts-ignore
1000
+ locations.map(r => (r.start === undefined ? r.parentRegion : r)),
1001
+ )
1002
+ self.showAllRegions()
1003
+ }
1004
+ },
1005
+
1006
+ /**
1007
+ * Navigate to a location based on its refName and optionally start, end,
1008
+ * and assemblyName. Can handle if there are multiple displayedRegions
1009
+ * from same refName. Only navigates to a location if it is entirely
1010
+ * within a displayedRegion. Navigates to the first matching location
1011
+ * encountered.
1012
+ *
1013
+ * Throws an error if navigation was unsuccessful
1014
+ *
1015
+ * @param location - a proposed location to navigate to
1016
+ */
1017
+ navTo(query: NavLocation) {
1018
+ this.navToMultiple([query])
1019
+ },
1020
+
1021
+ navToMultiple(locations: NavLocation[]) {
1022
+ const firstLocation = locations[0]
1023
+ let { refName } = firstLocation
1024
+ const {
1025
+ start,
1026
+ end,
1027
+ assemblyName = self.assemblyNames[0],
1028
+ } = firstLocation
1029
+
1030
+ if (start !== undefined && end !== undefined && start > end) {
1031
+ throw new Error(`start "${start + 1}" is greater than end "${end}"`)
1032
+ }
1033
+ const session = getSession(self)
1034
+ const { assemblyManager } = session
1035
+ const assembly = assemblyManager.get(assemblyName)
1036
+ if (assembly) {
1037
+ const canonicalRefName = assembly.getCanonicalRefName(refName)
1038
+ if (canonicalRefName) {
1039
+ refName = canonicalRefName
1040
+ }
1041
+ }
1042
+ let s = start
1043
+ let e = end
1044
+ let refNameMatched = false
1045
+ const predicate = (r: Region) => {
1046
+ if (refName === r.refName) {
1047
+ refNameMatched = true
1048
+ if (s === undefined) {
1049
+ s = r.start
1050
+ }
1051
+ if (e === undefined) {
1052
+ e = r.end
1053
+ }
1054
+ if (s >= r.start && s <= r.end && e <= r.end && e >= r.start) {
1055
+ return true
1056
+ }
1057
+ s = start
1058
+ e = end
1059
+ }
1060
+ return false
1061
+ }
1062
+
1063
+ const lastIndex = findLastIndex(self.displayedRegions, predicate)
1064
+ let index
1065
+ while (index !== lastIndex) {
1066
+ try {
1067
+ const previousIndex: number | undefined = index
1068
+ index = self.displayedRegions
1069
+ .slice(previousIndex === undefined ? 0 : previousIndex + 1)
1070
+ .findIndex(predicate)
1071
+ if (previousIndex !== undefined) {
1072
+ index += previousIndex + 1
1073
+ }
1074
+ if (!refNameMatched) {
1075
+ throw new Error(
1076
+ `could not find a region with refName "${refName}"`,
1077
+ )
1078
+ }
1079
+ if (s === undefined) {
1080
+ throw new Error(
1081
+ `could not find a region with refName "${refName}" that contained an end position ${e}`,
1082
+ )
1083
+ }
1084
+ if (e === undefined) {
1085
+ throw new Error(
1086
+ `could not find a region with refName "${refName}" that contained a start position ${
1087
+ s + 1
1088
+ }`,
1089
+ )
1090
+ }
1091
+ if (index === -1) {
1092
+ throw new Error(
1093
+ `could not find a region that completely contained "${assembleLocString(
1094
+ firstLocation,
1095
+ )}"`,
1096
+ )
1097
+ }
1098
+ if (locations.length === 1) {
1099
+ const f = self.displayedRegions[index]
1100
+ this.moveTo(
1101
+ { index, offset: f.reversed ? f.end - e : s - f.start },
1102
+ { index, offset: f.reversed ? f.end - s : e - f.start },
1103
+ )
1104
+ return
1105
+ }
1106
+ let locationIndex = 0
1107
+ let locationStart = 0
1108
+ let locationEnd = 0
1109
+ for (
1110
+ locationIndex;
1111
+ locationIndex < locations.length;
1112
+ locationIndex++
1113
+ ) {
1114
+ const location = locations[locationIndex]
1115
+ const region = self.displayedRegions[index + locationIndex]
1116
+ locationStart = location.start || region.start
1117
+ locationEnd = location.end || region.end
1118
+ if (location.refName !== region.refName) {
1119
+ throw new Error(
1120
+ `Entered location ${assembleLocString(
1121
+ location,
1122
+ )} does not match with displayed regions`,
1123
+ )
1124
+ }
1125
+ }
1126
+ locationIndex -= 1
1127
+ const startDisplayedRegion = self.displayedRegions[index]
1128
+ const endDisplayedRegion =
1129
+ self.displayedRegions[index + locationIndex]
1130
+ this.moveTo(
1131
+ {
1132
+ index,
1133
+ offset: startDisplayedRegion.reversed
1134
+ ? startDisplayedRegion.end - e
1135
+ : s - startDisplayedRegion.start,
1136
+ },
1137
+ {
1138
+ index: index + locationIndex,
1139
+ offset: endDisplayedRegion.reversed
1140
+ ? endDisplayedRegion.end - locationStart
1141
+ : locationEnd - endDisplayedRegion.start,
1142
+ },
1143
+ )
1144
+ return
1145
+ } catch (error) {
1146
+ if (index === lastIndex) {
1147
+ throw error
1148
+ }
1149
+ }
1150
+ }
1151
+ },
1349
1152
  }))
1350
1153
  .views(self => ({
1351
1154
  rubberBandMenuItems(): MenuItem[] {
@@ -1355,9 +1158,7 @@ export function stateModelFactory(pluginManager: PluginManager) {
1355
1158
  icon: ZoomInIcon,
1356
1159
  onClick: () => {
1357
1160
  const { leftOffset, rightOffset } = self
1358
- if (leftOffset && rightOffset) {
1359
- self.moveTo(leftOffset, rightOffset)
1360
- }
1161
+ self.moveTo(leftOffset, rightOffset)
1361
1162
  },
1362
1163
  },
1363
1164
  {
@@ -1369,6 +1170,46 @@ export function stateModelFactory(pluginManager: PluginManager) {
1369
1170
  },
1370
1171
  ]
1371
1172
  },
1173
+
1174
+ bpToPx({
1175
+ refName,
1176
+ coord,
1177
+ regionNumber,
1178
+ }: {
1179
+ refName: string
1180
+ coord: number
1181
+ regionNumber?: number
1182
+ }) {
1183
+ return bpToPx({ refName, coord, regionNumber, self })
1184
+ },
1185
+
1186
+ /**
1187
+ * scrolls the view to center on the given bp. if that is not in any
1188
+ * of the displayed regions, does nothing
1189
+ * @param coord - basepair at which you want to center the view
1190
+ * @param refName - refName of the displayedRegion you are centering at
1191
+ * @param regionNumber - index of the displayedRegion
1192
+ */
1193
+ centerAt(coord: number, refName: string, regionNumber: number) {
1194
+ const centerPx = this.bpToPx({
1195
+ refName,
1196
+ coord,
1197
+ regionNumber,
1198
+ })
1199
+ if (centerPx) {
1200
+ self.scrollTo(Math.round(centerPx.offsetPx - self.width / 2))
1201
+ }
1202
+ },
1203
+
1204
+ pxToBp(px: number) {
1205
+ return pxToBp(self, px)
1206
+ },
1207
+
1208
+ get centerLineInfo() {
1209
+ return self.displayedRegions.length
1210
+ ? this.pxToBp(self.width / 2)
1211
+ : undefined
1212
+ },
1372
1213
  }))
1373
1214
  }
1374
1215