jbrowse-plugin-mafviewer 1.2.3 → 1.3.0

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 (147) hide show
  1. package/dist/BgzipTaffyAdapter/BgzipTaffyAdapter.d.ts +1 -1
  2. package/dist/BigMafAdapter/BigMafAdapter.d.ts +1 -1
  3. package/dist/BigMafAdapter/BigMafAdapter.js +50 -49
  4. package/dist/BigMafAdapter/BigMafAdapter.js.map +1 -1
  5. package/dist/LinearMafDisplay/components/Crosshairs.d.ts +10 -0
  6. package/dist/LinearMafDisplay/components/Crosshairs.js +18 -0
  7. package/dist/LinearMafDisplay/components/Crosshairs.js.map +1 -0
  8. package/dist/LinearMafDisplay/components/GetSequenceDialog/GetSequenceDialog.d.ts +11 -0
  9. package/dist/LinearMafDisplay/components/GetSequenceDialog/GetSequenceDialog.js +97 -0
  10. package/dist/LinearMafDisplay/components/GetSequenceDialog/GetSequenceDialog.js.map +1 -0
  11. package/dist/LinearMafDisplay/components/{ReactComponent.d.ts → LinearMafDisplayComponent.d.ts} +1 -1
  12. package/dist/LinearMafDisplay/components/LinearMafDisplayComponent.js +168 -0
  13. package/dist/LinearMafDisplay/components/LinearMafDisplayComponent.js.map +1 -0
  14. package/dist/LinearMafDisplay/components/MAFTooltip.d.ts +12 -0
  15. package/dist/LinearMafDisplay/components/MAFTooltip.js +29 -0
  16. package/dist/LinearMafDisplay/components/MAFTooltip.js.map +1 -0
  17. package/dist/LinearMafDisplay/components/SetRowHeightDialog/SetRowHeightDialog.js +38 -0
  18. package/dist/LinearMafDisplay/components/SetRowHeightDialog/SetRowHeightDialog.js.map +1 -0
  19. package/dist/LinearMafDisplay/components/Sidebar/ColorLegend.d.ts +6 -0
  20. package/dist/LinearMafDisplay/components/{ColorLegend.js → Sidebar/ColorLegend.js} +2 -3
  21. package/dist/LinearMafDisplay/components/Sidebar/ColorLegend.js.map +1 -0
  22. package/dist/LinearMafDisplay/components/Sidebar/RectBg.js.map +1 -0
  23. package/dist/LinearMafDisplay/components/{SvgWrapper.d.ts → Sidebar/SvgWrapper.d.ts} +1 -1
  24. package/dist/LinearMafDisplay/components/{SvgWrapper.js → Sidebar/SvgWrapper.js} +3 -1
  25. package/dist/LinearMafDisplay/components/Sidebar/SvgWrapper.js.map +1 -0
  26. package/dist/LinearMafDisplay/components/{Tree.d.ts → Sidebar/Tree.d.ts} +2 -1
  27. package/dist/LinearMafDisplay/components/{Tree.js → Sidebar/Tree.js} +2 -0
  28. package/dist/LinearMafDisplay/components/Sidebar/Tree.js.map +1 -0
  29. package/dist/LinearMafDisplay/components/{YScaleBars.d.ts → Sidebar/YScaleBars.d.ts} +1 -1
  30. package/dist/LinearMafDisplay/components/Sidebar/YScaleBars.js +11 -0
  31. package/dist/LinearMafDisplay/components/Sidebar/YScaleBars.js.map +1 -0
  32. package/dist/LinearMafDisplay/index.js +1 -1
  33. package/dist/LinearMafDisplay/index.js.map +1 -1
  34. package/dist/LinearMafDisplay/renderSvg.js +1 -1
  35. package/dist/LinearMafDisplay/renderSvg.js.map +1 -1
  36. package/dist/LinearMafDisplay/stateModel.d.ts +23 -20
  37. package/dist/LinearMafDisplay/stateModel.js +62 -8
  38. package/dist/LinearMafDisplay/stateModel.js.map +1 -1
  39. package/dist/LinearMafDisplay/types.d.ts +5 -3
  40. package/dist/LinearMafDisplay/types.js +1 -15
  41. package/dist/LinearMafDisplay/types.js.map +1 -1
  42. package/dist/LinearMafDisplay/util.d.ts +4 -0
  43. package/dist/LinearMafDisplay/util.js +16 -0
  44. package/dist/LinearMafDisplay/util.js.map +1 -0
  45. package/dist/LinearMafRenderer/LinearMafRenderer.d.ts +5 -4
  46. package/dist/LinearMafRenderer/LinearMafRenderer.js +8 -10
  47. package/dist/LinearMafRenderer/LinearMafRenderer.js.map +1 -1
  48. package/dist/LinearMafRenderer/makeImageData.d.ts +1 -0
  49. package/dist/LinearMafRenderer/makeImageData.js +25 -20
  50. package/dist/LinearMafRenderer/makeImageData.js.map +1 -1
  51. package/dist/MafAddTrackWorkflow/index.js +0 -1
  52. package/dist/MafAddTrackWorkflow/index.js.map +1 -1
  53. package/dist/{MafRPC/index.d.ts → MafGetSamples/MafGetSamples.d.ts} +1 -3
  54. package/dist/{MafRPC/index.js → MafGetSamples/MafGetSamples.js} +2 -7
  55. package/dist/MafGetSamples/MafGetSamples.js.map +1 -0
  56. package/dist/MafGetSamples/index.d.ts +2 -0
  57. package/dist/MafGetSamples/index.js +7 -0
  58. package/dist/MafGetSamples/index.js.map +1 -0
  59. package/dist/MafGetSequences/MafGetSequences.d.ts +16 -0
  60. package/dist/MafGetSequences/MafGetSequences.js +20 -0
  61. package/dist/MafGetSequences/MafGetSequences.js.map +1 -0
  62. package/dist/MafGetSequences/index.d.ts +2 -0
  63. package/dist/MafGetSequences/index.js +7 -0
  64. package/dist/MafGetSequences/index.js.map +1 -0
  65. package/dist/MafTabixAdapter/MafTabixAdapter.d.ts +1 -1
  66. package/dist/MafTabixAdapter/MafTabixAdapter.js +9 -11
  67. package/dist/MafTabixAdapter/MafTabixAdapter.js.map +1 -1
  68. package/dist/MafTabixAdapter/configSchema.js +29 -1
  69. package/dist/MafTabixAdapter/configSchema.js.map +1 -1
  70. package/dist/index.js +4 -2
  71. package/dist/index.js.map +1 -1
  72. package/dist/jbrowse-plugin-mafviewer.umd.production.min.js +9 -20
  73. package/dist/jbrowse-plugin-mafviewer.umd.production.min.js.map +4 -4
  74. package/dist/util/extractSubsequence.d.ts +12 -0
  75. package/dist/util/extractSubsequence.js +60 -0
  76. package/dist/util/extractSubsequence.js.map +1 -0
  77. package/dist/util/extractSubsequence.test.d.ts +1 -0
  78. package/dist/util/extractSubsequence.test.js +42 -0
  79. package/dist/util/extractSubsequence.test.js.map +1 -0
  80. package/dist/util/fastaUtils.d.ts +16 -0
  81. package/dist/util/fastaUtils.js +84 -0
  82. package/dist/util/fastaUtils.js.map +1 -0
  83. package/dist/util/fastaUtils.test.d.ts +1 -0
  84. package/dist/util/fastaUtils.test.js +95 -0
  85. package/dist/util/fastaUtils.test.js.map +1 -0
  86. package/dist/util/fetchSequences.d.ts +18 -0
  87. package/dist/util/fetchSequences.js +39 -0
  88. package/dist/util/fetchSequences.js.map +1 -0
  89. package/dist/util/useSequences.d.ts +21 -0
  90. package/dist/util/useSequences.js +64 -0
  91. package/dist/util/useSequences.js.map +1 -0
  92. package/dist/util.d.ts +2 -2
  93. package/dist/util.js +5 -1
  94. package/dist/util.js.map +1 -1
  95. package/package.json +13 -13
  96. package/src/BigMafAdapter/BigMafAdapter.ts +52 -49
  97. package/src/LinearMafDisplay/components/Crosshairs.tsx +50 -0
  98. package/src/LinearMafDisplay/components/GetSequenceDialog/GetSequenceDialog.tsx +175 -0
  99. package/src/LinearMafDisplay/components/LinearMafDisplayComponent.tsx +257 -0
  100. package/src/LinearMafDisplay/components/MAFTooltip.tsx +59 -0
  101. package/src/LinearMafDisplay/components/SetRowHeightDialog/SetRowHeightDialog.tsx +83 -0
  102. package/src/LinearMafDisplay/components/{ColorLegend.tsx → Sidebar/ColorLegend.tsx} +11 -7
  103. package/src/LinearMafDisplay/components/{SvgWrapper.tsx → Sidebar/SvgWrapper.tsx} +5 -3
  104. package/src/LinearMafDisplay/components/{Tree.tsx → Sidebar/Tree.tsx} +5 -1
  105. package/src/LinearMafDisplay/components/Sidebar/YScaleBars.tsx +23 -0
  106. package/src/LinearMafDisplay/index.ts +1 -1
  107. package/src/LinearMafDisplay/renderSvg.tsx +1 -1
  108. package/src/LinearMafDisplay/stateModel.ts +71 -18
  109. package/src/LinearMafDisplay/types.ts +4 -24
  110. package/src/LinearMafDisplay/util.ts +27 -0
  111. package/src/LinearMafRenderer/LinearMafRenderer.ts +10 -14
  112. package/src/LinearMafRenderer/makeImageData.ts +33 -22
  113. package/src/MafAddTrackWorkflow/index.ts +0 -1
  114. package/src/{MafRPC/index.ts → MafGetSamples/MafGetSamples.ts} +1 -8
  115. package/src/MafGetSamples/index.ts +9 -0
  116. package/src/MafGetSequences/MafGetSequences.ts +47 -0
  117. package/src/MafGetSequences/index.ts +9 -0
  118. package/src/MafTabixAdapter/MafTabixAdapter.ts +13 -12
  119. package/src/MafTabixAdapter/configSchema.ts +29 -1
  120. package/src/index.ts +4 -2
  121. package/src/util/__snapshots__/fastaUtils.test.ts.snap +22 -0
  122. package/src/util/extractSubsequence.test.ts +54 -0
  123. package/src/util/extractSubsequence.ts +74 -0
  124. package/src/util/fastaUtils.test.ts +99 -0
  125. package/src/util/fastaUtils.ts +102 -0
  126. package/src/util/fetchSequences.ts +57 -0
  127. package/src/util/useSequences.ts +90 -0
  128. package/src/util.ts +6 -2
  129. package/dist/LinearMafDisplay/components/ColorLegend.d.ts +0 -8
  130. package/dist/LinearMafDisplay/components/ColorLegend.js.map +0 -1
  131. package/dist/LinearMafDisplay/components/ReactComponent.js +0 -50
  132. package/dist/LinearMafDisplay/components/ReactComponent.js.map +0 -1
  133. package/dist/LinearMafDisplay/components/RectBg.js.map +0 -1
  134. package/dist/LinearMafDisplay/components/SetRowHeight.js +0 -36
  135. package/dist/LinearMafDisplay/components/SetRowHeight.js.map +0 -1
  136. package/dist/LinearMafDisplay/components/SvgWrapper.js.map +0 -1
  137. package/dist/LinearMafDisplay/components/Tree.js.map +0 -1
  138. package/dist/LinearMafDisplay/components/YScaleBars.js +0 -20
  139. package/dist/LinearMafDisplay/components/YScaleBars.js.map +0 -1
  140. package/dist/MafRPC/index.js.map +0 -1
  141. package/src/LinearMafDisplay/components/ReactComponent.tsx +0 -91
  142. package/src/LinearMafDisplay/components/SetRowHeight.tsx +0 -83
  143. package/src/LinearMafDisplay/components/YScaleBars.tsx +0 -41
  144. /package/dist/LinearMafDisplay/components/{SetRowHeight.d.ts → SetRowHeightDialog/SetRowHeightDialog.d.ts} +0 -0
  145. /package/dist/LinearMafDisplay/components/{RectBg.d.ts → Sidebar/RectBg.d.ts} +0 -0
  146. /package/dist/LinearMafDisplay/components/{RectBg.js → Sidebar/RectBg.js} +0 -0
  147. /package/src/LinearMafDisplay/components/{RectBg.tsx → Sidebar/RectBg.tsx} +0 -0
@@ -26,12 +26,13 @@ export default class BigMafAdapter extends BaseFeatureDataAdapter {
26
26
  if (!this.getSubAdapter) {
27
27
  throw new Error('no getSubAdapter available')
28
28
  }
29
- const adapter = await this.getSubAdapter({
30
- ...getSnapshot(this.config),
31
- type: 'BigBedAdapter',
32
- })
33
29
  return {
34
- adapter: adapter.dataAdapter as BaseFeatureDataAdapter,
30
+ adapter: (
31
+ await this.getSubAdapter({
32
+ ...getSnapshot(this.config),
33
+ type: 'BigBedAdapter',
34
+ })
35
+ ).dataAdapter as BaseFeatureDataAdapter,
35
36
  }
36
37
  }
37
38
  async setupPre() {
@@ -59,59 +60,61 @@ export default class BigMafAdapter extends BaseFeatureDataAdapter {
59
60
  return ObservableCreate<Feature>(async observer => {
60
61
  const { adapter } = await this.setup()
61
62
  const features = await updateStatus(
62
- 'Downloading alignment',
63
+ 'Downloading alignments',
63
64
  statusCallback,
64
65
  () => firstValueFrom(adapter.getFeatures(query).pipe(toArray())),
65
66
  )
66
- for (const feature of features) {
67
- const maf = feature.get('mafBlock') as string
68
- const blocks = maf.split(';')
69
- let aln: string | undefined
70
- const alns = [] as string[]
71
- const alignments = {} as Record<string, OrganismRecord>
72
- const blocks2 = [] as string[]
73
- for (const block of blocks) {
74
- if (block.startsWith('s')) {
75
- if (aln) {
76
- alns.push(block.split(/ +/)[6]!)
77
- blocks2.push(block)
78
- } else {
79
- aln = block.split(/ +/)[6]
80
- alns.push(aln!)
81
- blocks2.push(block)
67
+ await updateStatus('Processing alignments', statusCallback, () => {
68
+ for (const feature of features) {
69
+ const maf = feature.get('mafBlock') as string
70
+ const blocks = maf.split(';')
71
+ let aln: string | undefined
72
+ const alns = [] as string[]
73
+ const alignments = {} as Record<string, OrganismRecord>
74
+ const blocks2 = [] as string[]
75
+ for (const block of blocks) {
76
+ if (block.startsWith('s')) {
77
+ if (aln) {
78
+ alns.push(block.split(/ +/)[6]!)
79
+ blocks2.push(block)
80
+ } else {
81
+ aln = block.split(/ +/)[6]
82
+ alns.push(aln!)
83
+ blocks2.push(block)
84
+ }
82
85
  }
83
86
  }
84
- }
85
87
 
86
- for (let i = 0; i < blocks2.length; i++) {
87
- const elt = blocks2[i]!
88
- const ad = elt.split(/ +/)
89
- const y = ad[1]!.split('.')
90
- const org = y[0]!
91
- const chr = y[1]!
88
+ for (let i = 0; i < blocks2.length; i++) {
89
+ const elt = blocks2[i]!
90
+ const ad = elt.split(/ +/)
91
+ const y = ad[1]!.split('.')
92
+ const org = y[0]!
93
+ const chr = y[1]!
92
94
 
93
- alignments[org] = {
94
- chr: chr,
95
- start: +ad[1]!,
96
- srcSize: +ad[2]!,
97
- strand: ad[3] === '+' ? 1 : -1,
98
- unknown: +ad[4]!,
99
- data: alns[i]!,
95
+ alignments[org] = {
96
+ chr: chr,
97
+ start: +ad[1]!,
98
+ srcSize: +ad[2]!,
99
+ strand: ad[3] === '+' ? 1 : -1,
100
+ unknown: +ad[4]!,
101
+ data: alns[i]!,
102
+ }
100
103
  }
104
+ observer.next(
105
+ new SimpleFeature({
106
+ id: feature.id(),
107
+ data: {
108
+ start: feature.get('start'),
109
+ end: feature.get('end'),
110
+ refName: feature.get('refName'),
111
+ seq: alns[0],
112
+ alignments: alignments,
113
+ },
114
+ }),
115
+ )
101
116
  }
102
- observer.next(
103
- new SimpleFeature({
104
- id: feature.id(),
105
- data: {
106
- start: feature.get('start'),
107
- end: feature.get('end'),
108
- refName: feature.get('refName'),
109
- seq: alns[0],
110
- alignments: alignments,
111
- },
112
- }),
113
- )
114
- }
117
+ })
115
118
  observer.complete()
116
119
  })
117
120
  }
@@ -0,0 +1,50 @@
1
+ import React from 'react'
2
+
3
+ import { makeStyles } from 'tss-react/mui'
4
+
5
+ const useStyles = makeStyles()({
6
+ cursor: {
7
+ pointerEvents: 'none',
8
+ },
9
+ })
10
+
11
+ interface CrosshairsProps {
12
+ width: number
13
+ height: number
14
+ scrollTop: number
15
+ mouseX?: number
16
+ mouseY: number
17
+ }
18
+
19
+ const Crosshairs = ({
20
+ width,
21
+ height,
22
+ scrollTop,
23
+ mouseX,
24
+ mouseY,
25
+ }: CrosshairsProps) => {
26
+ const { classes } = useStyles()
27
+
28
+ return (
29
+ <svg
30
+ className={classes.cursor}
31
+ width={width}
32
+ height={height}
33
+ style={{
34
+ position: 'absolute',
35
+ top: scrollTop,
36
+ }}
37
+ >
38
+ <line
39
+ x1={0}
40
+ x2={width}
41
+ y1={mouseY - scrollTop}
42
+ y2={mouseY - scrollTop}
43
+ stroke="black"
44
+ />
45
+ <line x1={mouseX} x2={mouseX} y1={0} y2={height} stroke="black" />
46
+ </svg>
47
+ )
48
+ }
49
+
50
+ export default Crosshairs
@@ -0,0 +1,175 @@
1
+ import React, { useState } from 'react'
2
+
3
+ import { Dialog, ErrorMessage, LoadingEllipses } from '@jbrowse/core/ui'
4
+ import { getSession } from '@jbrowse/core/util'
5
+ import {
6
+ Button,
7
+ DialogActions,
8
+ DialogContent,
9
+ TextField,
10
+ ToggleButton,
11
+ ToggleButtonGroup,
12
+ } from '@mui/material'
13
+ import { observer } from 'mobx-react'
14
+ import { makeStyles } from 'tss-react/mui'
15
+
16
+ import { useSequences } from '../../../util/useSequences'
17
+
18
+ import type { LinearMafDisplayModel } from '../../stateModel'
19
+
20
+ const useStyles = makeStyles()({
21
+ dialogContent: {
22
+ width: '80em',
23
+ },
24
+ textAreaInput: {
25
+ fontFamily: 'monospace',
26
+ whiteSpace: 'pre',
27
+ overflowX: 'auto',
28
+ },
29
+ ml: {
30
+ marginLeft: 10,
31
+ },
32
+ })
33
+
34
+ const GetSequenceDialog = observer(function ({
35
+ onClose,
36
+ model,
37
+ selectionCoords,
38
+ }: {
39
+ onClose: () => void
40
+ model: LinearMafDisplayModel
41
+ selectionCoords?: {
42
+ dragStartX: number
43
+ dragEndX: number
44
+ }
45
+ }) {
46
+ const [showAllLetters, setShowAllLetters] = useState(true)
47
+ const { classes } = useStyles()
48
+ const { sequence, loading, error } = useSequences({
49
+ model,
50
+ selectionCoords,
51
+ showAllLetters,
52
+ })
53
+ const sequenceTooLarge = sequence ? sequence.length > 1_000_000 : false
54
+
55
+ return (
56
+ <Dialog open onClose={onClose} title="Subsequence Data" maxWidth="xl">
57
+ <DialogContent>
58
+ <div
59
+ style={{
60
+ display: 'flex',
61
+ alignItems: 'center',
62
+ marginBottom: '16px',
63
+ }}
64
+ >
65
+ <ToggleButtonGroup
66
+ value={showAllLetters}
67
+ exclusive
68
+ size="small"
69
+ onChange={(_event, newDisplayMode) => {
70
+ if (newDisplayMode !== null) {
71
+ setShowAllLetters(newDisplayMode)
72
+ }
73
+ }}
74
+ >
75
+ <ToggleButton value={true}>Show All Letters</ToggleButton>
76
+ <ToggleButton value={false}>Show Only Differences</ToggleButton>
77
+ </ToggleButtonGroup>
78
+ <div style={{ flexGrow: 1 }} />
79
+ <Button
80
+ variant="contained"
81
+ color="primary"
82
+ disabled={loading || !sequence}
83
+ onClick={() => {
84
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
85
+ ;(async () => {
86
+ try {
87
+ await navigator.clipboard.writeText(sequence)
88
+ getSession(model).notify(
89
+ 'Sequence copied to clipboard',
90
+ 'info',
91
+ )
92
+ } catch (e) {
93
+ console.error(e)
94
+ getSession(model).notifyError(`${e}`, e)
95
+ }
96
+ })()
97
+ }}
98
+ >
99
+ Copy to Clipboard
100
+ </Button>
101
+ <Button
102
+ variant="contained"
103
+ color="secondary"
104
+ disabled={loading || !sequence}
105
+ onClick={() => {
106
+ try {
107
+ const url = URL.createObjectURL(
108
+ new Blob([sequence], { type: 'text/plain' }),
109
+ )
110
+
111
+ // Create a temporary anchor element
112
+ const a = document.createElement('a')
113
+ a.href = url
114
+ a.download = 'sequence.fasta'
115
+
116
+ // Trigger the download
117
+ document.body.append(a)
118
+ a.click()
119
+
120
+ // Clean up
121
+ a.remove()
122
+ URL.revokeObjectURL(url)
123
+ getSession(model).notify('Sequence downloaded', 'info')
124
+ } catch (e) {
125
+ console.error(e)
126
+ getSession(model).notifyError(`${e}`, e)
127
+ }
128
+ }}
129
+ >
130
+ Download
131
+ </Button>
132
+ </div>
133
+
134
+ {error ? (
135
+ <ErrorMessage error={error} />
136
+ ) : (
137
+ <>
138
+ {loading ? <LoadingEllipses /> : null}
139
+ <TextField
140
+ variant="outlined"
141
+ multiline
142
+ minRows={5}
143
+ maxRows={10}
144
+ disabled={sequenceTooLarge}
145
+ className={classes.dialogContent}
146
+ fullWidth
147
+ value={
148
+ loading
149
+ ? 'Loading...'
150
+ : sequenceTooLarge
151
+ ? 'Reference sequence too large to display, use the download FASTA button'
152
+ : sequence
153
+ }
154
+ slotProps={{
155
+ input: {
156
+ readOnly: true,
157
+ classes: {
158
+ input: classes.textAreaInput,
159
+ },
160
+ },
161
+ }}
162
+ />
163
+ </>
164
+ )}
165
+ </DialogContent>
166
+ <DialogActions>
167
+ <Button color="primary" variant="outlined" onClick={onClose}>
168
+ Close
169
+ </Button>
170
+ </DialogActions>
171
+ </Dialog>
172
+ )
173
+ })
174
+
175
+ export default GetSequenceDialog
@@ -0,0 +1,257 @@
1
+ import React, { useEffect, useRef, useState } from 'react'
2
+
3
+ import { Menu } from '@jbrowse/core/ui'
4
+ import { getContainingView, getEnv } from '@jbrowse/core/util'
5
+ import { useTheme } from '@mui/material'
6
+ import { observer } from 'mobx-react'
7
+
8
+ import Crosshairs from './Crosshairs'
9
+ import SequenceDialog from './GetSequenceDialog/GetSequenceDialog'
10
+ import MAFTooltip from './MAFTooltip'
11
+ import YScaleBars from './Sidebar/YScaleBars'
12
+
13
+ import type { LinearMafDisplayModel } from '../stateModel'
14
+ import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
15
+
16
+ const LinearMafDisplay = observer(function (props: {
17
+ model: LinearMafDisplayModel
18
+ }) {
19
+ const { model } = props
20
+ const { pluginManager } = getEnv(model)
21
+ const { rowHeight, height, scrollTop, samples: sources } = model
22
+ const ref = useRef<HTMLDivElement>(null)
23
+ const theme = useTheme()
24
+
25
+ const LinearGenomePlugin = pluginManager.getPlugin(
26
+ 'LinearGenomeViewPlugin',
27
+ ) as import('@jbrowse/plugin-linear-genome-view').default
28
+ const { BaseLinearDisplayComponent } = LinearGenomePlugin.exports
29
+
30
+ const [mouseY, setMouseY] = useState<number>()
31
+ const [mouseX, setMouseX] = useState<number>()
32
+ const [isDragging, setIsDragging] = useState(false)
33
+ const [dragStartX, setDragStartX] = useState<number>()
34
+ const [dragEndX, setDragEndX] = useState<number>()
35
+ const [showSelectionBox, setShowSelectionBox] = useState(false)
36
+ const [contextCoord, setContextCoord] = useState<{
37
+ coord: [number, number]
38
+ dragStartX: number
39
+ dragEndX: number
40
+ }>()
41
+ const [showSequenceDialog, setShowSequenceDialog] = useState(false)
42
+ const [selectionCoords, setSelectionCoords] = useState<
43
+ | {
44
+ dragStartX: number
45
+ dragEndX: number
46
+ }
47
+ | undefined
48
+ >()
49
+ const { width } = getContainingView(model) as LinearGenomeViewModel
50
+
51
+ const handleMouseDown = (event: React.MouseEvent) => {
52
+ const rect = ref.current?.getBoundingClientRect()
53
+ const left = rect?.left || 0
54
+ const clientX = event.clientX - left
55
+
56
+ // Clear the previous selection box when starting a new drag
57
+ setShowSelectionBox(false)
58
+ setIsDragging(true)
59
+ setDragStartX(clientX)
60
+ setDragEndX(clientX)
61
+ event.stopPropagation()
62
+ }
63
+
64
+ const handleMouseMove = (event: React.MouseEvent) => {
65
+ const rect = ref.current?.getBoundingClientRect()
66
+ const top = rect?.top || 0
67
+ const left = rect?.left || 0
68
+ const clientX = event.clientX - left
69
+ const clientY = event.clientY - top
70
+
71
+ setMouseY(clientY)
72
+ setMouseX(clientX)
73
+
74
+ if (isDragging) {
75
+ setDragEndX(clientX)
76
+ }
77
+ }
78
+
79
+ const handleMouseUp = (event: React.MouseEvent) => {
80
+ if (isDragging && dragStartX !== undefined && dragEndX !== undefined) {
81
+ // Calculate the drag distance
82
+ const dragDistanceX = Math.abs(dragEndX - dragStartX)
83
+
84
+ // Only show context menu if the drag distance is at least 2 pixels in either direction
85
+ if (dragDistanceX >= 2) {
86
+ setContextCoord({
87
+ coord: [event.clientX, event.clientY],
88
+ dragEndX: event.clientX,
89
+ dragStartX: dragStartX,
90
+ })
91
+
92
+ // Set showSelectionBox to true to keep the selection visible
93
+ setShowSelectionBox(true)
94
+ } else {
95
+ // For very small drags (less than 2px), don't show selection box or context menu
96
+ clearSelectionBox()
97
+ }
98
+ }
99
+
100
+ // Only set isDragging to false, but keep the coordinates
101
+ setIsDragging(false)
102
+ }
103
+
104
+ // Function to clear the selection box
105
+ const clearSelectionBox = () => {
106
+ setShowSelectionBox(false)
107
+ setDragStartX(undefined)
108
+ setDragEndX(undefined)
109
+ }
110
+
111
+ // Add keydown event handler to clear selection box when Escape key is pressed
112
+ useEffect(() => {
113
+ const handleKeyDown = (event: KeyboardEvent) => {
114
+ if (event.key === 'Escape' && showSelectionBox) {
115
+ clearSelectionBox()
116
+ }
117
+ }
118
+
119
+ // Add click handler to clear selection box when clicking outside of it
120
+ const handleClickOutside = (event: MouseEvent) => {
121
+ if (
122
+ ref.current &&
123
+ !ref.current.contains(event.target as Node) &&
124
+ showSelectionBox
125
+ ) {
126
+ clearSelectionBox()
127
+ }
128
+ }
129
+
130
+ document.addEventListener('keydown', handleKeyDown)
131
+ document.addEventListener('click', handleClickOutside)
132
+
133
+ return () => {
134
+ document.removeEventListener('keydown', handleKeyDown)
135
+ document.removeEventListener('click', handleClickOutside)
136
+ }
137
+ }, [showSelectionBox, clearSelectionBox])
138
+
139
+ return (
140
+ <div
141
+ ref={ref}
142
+ onMouseDown={handleMouseDown}
143
+ onMouseMove={handleMouseMove}
144
+ onMouseUp={handleMouseUp}
145
+ onDoubleClick={() => {
146
+ // Clear selection box on double click
147
+ if (showSelectionBox) {
148
+ clearSelectionBox()
149
+ }
150
+ }}
151
+ onMouseLeave={() => {
152
+ setMouseY(undefined)
153
+ setMouseX(undefined)
154
+ setIsDragging(false)
155
+ }}
156
+ >
157
+ <BaseLinearDisplayComponent {...props} />
158
+ <YScaleBars model={model} />
159
+ {mouseY && mouseX && sources && !contextCoord && !showSequenceDialog ? (
160
+ <div style={{ position: 'relative' }}>
161
+ <Crosshairs
162
+ width={width}
163
+ height={height}
164
+ scrollTop={scrollTop}
165
+ mouseX={mouseX}
166
+ mouseY={mouseY}
167
+ />
168
+ <MAFTooltip
169
+ model={model}
170
+ mouseX={mouseX}
171
+ mouseY={mouseY}
172
+ origMouseX={dragStartX}
173
+ rowHeight={rowHeight}
174
+ sources={sources}
175
+ />
176
+ </div>
177
+ ) : null}
178
+ {(isDragging || showSelectionBox) &&
179
+ dragStartX !== undefined &&
180
+ dragEndX !== undefined ? (
181
+ <div
182
+ style={{
183
+ position: 'absolute',
184
+ left: Math.min(dragStartX, dragEndX),
185
+ top: 0,
186
+ width: Math.abs(dragEndX - dragStartX),
187
+ height,
188
+ backgroundColor: 'rgba(0, 0, 255, 0.2)',
189
+ border: '1px solid rgba(0, 0, 255, 0.5)',
190
+ pointerEvents: 'none',
191
+ }}
192
+ />
193
+ ) : null}
194
+ <Menu
195
+ open={Boolean(contextCoord)}
196
+ onMenuItemClick={(_, callback) => {
197
+ callback()
198
+ setContextCoord(undefined)
199
+ }}
200
+ onClose={() => {
201
+ setContextCoord(undefined)
202
+ }}
203
+ slotProps={{
204
+ transition: {
205
+ onExit: () => {
206
+ setContextCoord(undefined)
207
+ },
208
+ },
209
+ }}
210
+ anchorReference="anchorPosition"
211
+ anchorPosition={
212
+ contextCoord
213
+ ? { top: contextCoord.coord[1], left: contextCoord.coord[0] }
214
+ : undefined
215
+ }
216
+ style={{
217
+ zIndex: theme.zIndex.tooltip,
218
+ }}
219
+ menuItems={[
220
+ {
221
+ label: 'View subsequence',
222
+ onClick: () => {
223
+ if (!contextCoord) {
224
+ return
225
+ }
226
+
227
+ // Store the selection coordinates for the SequenceDialog to use
228
+ setSelectionCoords({
229
+ dragStartX: contextCoord.dragStartX,
230
+ dragEndX: contextCoord.dragEndX,
231
+ })
232
+
233
+ // Show the dialog
234
+ setShowSequenceDialog(true)
235
+
236
+ // Close the context menu
237
+ setContextCoord(undefined)
238
+ },
239
+ },
240
+ ]}
241
+ />
242
+
243
+ {showSequenceDialog ? (
244
+ <SequenceDialog
245
+ model={model}
246
+ selectionCoords={selectionCoords}
247
+ onClose={() => {
248
+ setShowSequenceDialog(false)
249
+ setSelectionCoords(undefined)
250
+ }}
251
+ />
252
+ ) : null}
253
+ </div>
254
+ )
255
+ })
256
+
257
+ export default LinearMafDisplay
@@ -0,0 +1,59 @@
1
+ import React from 'react'
2
+
3
+ import { SanitizedHTML } from '@jbrowse/core/ui'
4
+ import BaseTooltip from '@jbrowse/core/ui/BaseTooltip'
5
+ import {
6
+ getBpDisplayStr,
7
+ getContainingView,
8
+ toLocale,
9
+ } from '@jbrowse/core/util'
10
+ import { observer } from 'mobx-react'
11
+
12
+ import type { LinearMafDisplayModel } from '../stateModel'
13
+ import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
14
+
15
+ interface MAFTooltipProps {
16
+ mouseY: number
17
+ mouseX: number
18
+ rowHeight: number
19
+ sources: Record<string, any>[]
20
+ model: LinearMafDisplayModel
21
+ origMouseX?: number
22
+ }
23
+
24
+ const MAFTooltip = observer(function ({
25
+ model,
26
+ mouseY,
27
+ mouseX,
28
+ origMouseX,
29
+ rowHeight,
30
+ sources,
31
+ }: MAFTooltipProps) {
32
+ const view = getContainingView(model) as LinearGenomeViewModel
33
+ const ret = Object.entries(sources[Math.floor(mouseY / rowHeight)] || {})
34
+ .filter(([key]) => key !== 'color' && key !== 'id')
35
+ .map(([key, value]) => `${key}:${value}`)
36
+ .join('\n')
37
+ const p1 = origMouseX ? view.pxToBp(origMouseX) : undefined
38
+ const p2 = view.pxToBp(mouseX)
39
+ return ret ? (
40
+ <BaseTooltip>
41
+ <SanitizedHTML
42
+ html={[
43
+ ret,
44
+ ...(p1
45
+ ? [
46
+ `Start: ${p1.refName}:${toLocale(p1.coord)}`,
47
+ `End: ${p2.refName}:${toLocale(p2.coord)}`,
48
+ `Length: ${getBpDisplayStr(Math.abs(p1.coord - p2.coord))}`,
49
+ ]
50
+ : [`${p2.refName}:${toLocale(p2.coord)}`]),
51
+ ]
52
+ .filter(f => !!f)
53
+ .join('<br/>')}
54
+ />
55
+ </BaseTooltip>
56
+ ) : null
57
+ })
58
+
59
+ export default MAFTooltip