@apollo-annotation/jbrowse-plugin-apollo 0.3.6 → 0.3.8

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 (84) hide show
  1. package/dist/index.esm.js +4603 -2045
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +4611 -2039
  4. package/dist/jbrowse-plugin-apollo.cjs.development.js.map +1 -1
  5. package/dist/jbrowse-plugin-apollo.cjs.production.min.js +1 -1
  6. package/dist/jbrowse-plugin-apollo.cjs.production.min.js.map +1 -1
  7. package/dist/jbrowse-plugin-apollo.umd.development.js +9387 -4016
  8. package/dist/jbrowse-plugin-apollo.umd.development.js.map +1 -1
  9. package/dist/jbrowse-plugin-apollo.umd.production.min.js +1 -1
  10. package/dist/jbrowse-plugin-apollo.umd.production.min.js.map +1 -1
  11. package/package.json +15 -15
  12. package/src/ApolloInternetAccount/model.ts +48 -13
  13. package/src/BackendDrivers/CollaborationServerDriver.ts +23 -2
  14. package/src/ChangeManager.ts +42 -18
  15. package/src/FeatureDetailsWidget/ApolloTranscriptDetailsWidget.tsx +64 -5
  16. package/src/FeatureDetailsWidget/Attributes.tsx +8 -3
  17. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +70 -81
  18. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +946 -190
  19. package/src/FeatureDetailsWidget/TranscriptWidgetSummary.tsx +4 -0
  20. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +61 -73
  21. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +55 -211
  22. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +562 -108
  23. package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +78 -14
  24. package/src/LinearApolloDisplay/glyphs/Glyph.ts +15 -9
  25. package/src/LinearApolloDisplay/stateModel/base.ts +63 -43
  26. package/src/LinearApolloDisplay/stateModel/layouts.ts +3 -2
  27. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +79 -292
  28. package/src/LinearApolloDisplay/stateModel/rendering.ts +45 -344
  29. package/src/LinearApolloReferenceSequenceDisplay/components/LinearApolloReferenceSequenceDisplay.tsx +87 -0
  30. package/src/LinearApolloReferenceSequenceDisplay/components/index.ts +1 -0
  31. package/src/LinearApolloReferenceSequenceDisplay/configSchema.ts +7 -0
  32. package/src/LinearApolloReferenceSequenceDisplay/index.ts +3 -0
  33. package/src/LinearApolloReferenceSequenceDisplay/stateModel/base.ts +227 -0
  34. package/src/LinearApolloReferenceSequenceDisplay/stateModel/index.ts +25 -0
  35. package/src/LinearApolloReferenceSequenceDisplay/stateModel/rendering.ts +481 -0
  36. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +102 -40
  37. package/src/LinearApolloSixFrameDisplay/components/TrackLines.tsx +12 -20
  38. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +382 -243
  39. package/src/LinearApolloSixFrameDisplay/glyphs/Glyph.ts +12 -8
  40. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +83 -4
  41. package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +23 -11
  42. package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +118 -123
  43. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +53 -63
  44. package/src/OntologyManager/index.ts +4 -1
  45. package/src/TabularEditor/HybridGrid/Feature.tsx +20 -14
  46. package/src/TabularEditor/HybridGrid/HybridGrid.tsx +7 -5
  47. package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +108 -16
  48. package/src/components/AddAssembly.tsx +1 -1
  49. package/src/components/AddAssemblyAliases.tsx +114 -0
  50. package/src/components/AddChildFeature.tsx +7 -7
  51. package/src/components/AddFeature.tsx +20 -15
  52. package/src/components/AddRefSeqAliases.tsx +9 -9
  53. package/src/components/CopyFeature.tsx +4 -4
  54. package/src/components/CreateApolloAnnotation.tsx +335 -151
  55. package/src/components/DeleteAssembly.tsx +1 -1
  56. package/src/components/DeleteFeature.tsx +358 -11
  57. package/src/components/DownloadGFF3.tsx +20 -1
  58. package/src/components/EditZoomThresholdDialog.tsx +69 -0
  59. package/src/components/FilterFeatures.tsx +7 -7
  60. package/src/components/FilterTranscripts.tsx +86 -0
  61. package/src/components/ImportFeatures.tsx +1 -1
  62. package/src/components/ManageChecks.tsx +1 -1
  63. package/src/components/MergeExons.tsx +193 -0
  64. package/src/components/MergeTranscripts.tsx +182 -0
  65. package/src/components/OntologyTermMultiSelect.tsx +11 -11
  66. package/src/components/OpenLocalFile.tsx +11 -7
  67. package/src/components/SplitExon.tsx +134 -0
  68. package/src/components/ViewCheckResults.tsx +1 -1
  69. package/src/components/index.ts +4 -0
  70. package/src/config.ts +11 -0
  71. package/src/extensions/annotationFromJBrowseFeature.ts +2 -0
  72. package/src/extensions/annotationFromPileup.ts +99 -89
  73. package/src/index.ts +42 -105
  74. package/src/makeDisplayComponent.tsx +0 -1
  75. package/src/menus/index.ts +1 -0
  76. package/src/{ApolloInternetAccount/addMenuItems.ts → menus/topLevelMenu.ts} +60 -33
  77. package/src/menus/topLevelMenuAdmin.ts +154 -0
  78. package/src/session/session.ts +163 -104
  79. package/src/util/annotationFeatureUtils.ts +59 -0
  80. package/src/util/copyToClipboard.ts +21 -0
  81. package/src/util/displayUtils.ts +149 -0
  82. package/src/util/glyphUtils.ts +201 -0
  83. package/src/util/index.ts +2 -0
  84. package/src/util/mouseEventsUtils.ts +145 -0
@@ -0,0 +1,149 @@
1
+ import { type CheckResultIdsType } from '@apollo-annotation/mst'
2
+ import { makeStyles } from 'tss-react/mui'
3
+
4
+ export { default as EditZoomThresholdDialog } from '../components/EditZoomThresholdDialog'
5
+
6
+ export type Coord = [number, number]
7
+
8
+ export const useStyles = makeStyles()((theme) => ({
9
+ canvasContainer: {
10
+ position: 'relative',
11
+ left: 0,
12
+ },
13
+ canvas: {
14
+ position: 'absolute',
15
+ left: 0,
16
+ },
17
+ center: {
18
+ display: 'flex',
19
+ justifyContent: 'center',
20
+ },
21
+ ellipses: {
22
+ textOverflow: 'ellipsis',
23
+ overflow: 'hidden',
24
+ },
25
+ avatar: {
26
+ position: 'static',
27
+ height: '100%',
28
+ width: '100%',
29
+ overflow: 'visible',
30
+ color: theme.palette.warning.light,
31
+ backgroundColor: theme.palette.warning.contrastText,
32
+ },
33
+ box: {
34
+ position: 'absolute',
35
+ overflow: 'visible',
36
+ },
37
+ badge: {
38
+ display: 'inline-block',
39
+ },
40
+ loading: {
41
+ position: 'absolute',
42
+ right: theme.spacing(3),
43
+ zIndex: 10,
44
+ pointerEvents: 'none',
45
+ textAlign: 'right',
46
+ },
47
+ locked: {
48
+ position: 'absolute',
49
+ right: theme.spacing(3),
50
+ top: theme.spacing(6),
51
+ zIndex: 1,
52
+ pointerEvents: 'none',
53
+ textAlign: 'right',
54
+ },
55
+ }))
56
+
57
+ export interface CheckResultCluster<T> {
58
+ _id: string
59
+ message: string
60
+ start: number
61
+ count: number
62
+ members: T[]
63
+ range: { min: number; max: number }
64
+ featureIds: CheckResultIdsType
65
+ }
66
+
67
+ export function clusterResultByMessage<
68
+ T extends {
69
+ _id: string
70
+ start: number
71
+ end: number
72
+ message: string
73
+ ids: CheckResultIdsType
74
+ },
75
+ >(
76
+ items: readonly T[],
77
+ width: number,
78
+ touchesAsOverlap: boolean,
79
+ ): CheckResultCluster<T>[] {
80
+ const byMsg = new Map<string, T[]>()
81
+ for (const it of items) {
82
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
83
+ ;(byMsg.get(it.message) ?? byMsg.set(it.message, []).get(it.message)!).push(
84
+ it,
85
+ )
86
+ }
87
+
88
+ const clusters: CheckResultCluster<T>[] = []
89
+ const overlaps = (aEnd: number, bStart: number) =>
90
+ touchesAsOverlap ? bStart <= aEnd : bStart < aEnd
91
+
92
+ for (const [message, arr] of byMsg.entries()) {
93
+ if (arr.length === 0) {
94
+ continue
95
+ }
96
+
97
+ arr.sort((a, b) => a.start - b.start)
98
+
99
+ let group: T[] = [arr[0]]
100
+ let curMin = arr[0].start
101
+ let curMax = arr[0].start + width
102
+
103
+ const pushResult = () => {
104
+ const starts = group.map((d) => d.start).sort((a, b) => a - b)
105
+ const mid = Math.floor(starts.length / 2)
106
+ const median: number =
107
+ starts.length % 2 ? starts[mid] : (starts[mid - 1] + starts[mid]) / 2
108
+ const clusterId = group[0]._id
109
+ const featureIds = group[0].ids
110
+
111
+ clusters.push({
112
+ _id: clusterId,
113
+ message,
114
+ start: median,
115
+ count: group.length,
116
+ members: [...group],
117
+ range: { min: curMin, max: curMax },
118
+ featureIds,
119
+ })
120
+ }
121
+
122
+ for (let i = 1; i < arr.length; i++) {
123
+ const it = arr[i]
124
+ const itStart = it.start
125
+ const itEnd = itStart + width
126
+
127
+ if (overlaps(curMax, itStart)) {
128
+ group.push(it)
129
+ if (itStart < curMin) {
130
+ curMin = itStart
131
+ }
132
+ if (itEnd > curMax) {
133
+ curMax = itEnd
134
+ }
135
+ } else {
136
+ pushResult()
137
+ group = [it]
138
+ curMin = itStart
139
+ curMax = itEnd
140
+ }
141
+ }
142
+ pushResult()
143
+ }
144
+
145
+ clusters.sort(
146
+ (a, b) => a.message.localeCompare(b.message) || a.start - b.start,
147
+ )
148
+ return clusters
149
+ }
@@ -0,0 +1,201 @@
1
+ import {
2
+ type AnnotationFeature,
3
+ type TranscriptPartCoding,
4
+ } from '@apollo-annotation/mst'
5
+ import { type MenuItem } from '@jbrowse/core/ui'
6
+ import { type AbstractSessionModel } from '@jbrowse/core/util'
7
+ import { type LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
8
+
9
+ import { type LinearApolloDisplayMouseEvents } from '../LinearApolloDisplay/stateModel/mouseEvents'
10
+ import { type LinearApolloSixFrameDisplayMouseEvents } from '../LinearApolloSixFrameDisplay/stateModel/mouseEvents'
11
+ import { AddChildFeature, CopyFeature, DeleteFeature } from '../components'
12
+
13
+ type NavLocation = Parameters<LinearGenomeViewModel['navTo']>[0]
14
+
15
+ export function getMinAndMaxPx(
16
+ feature: AnnotationFeature | TranscriptPartCoding,
17
+ refName: string,
18
+ regionNumber: number,
19
+ lgv: LinearGenomeViewModel,
20
+ ): [number, number] | undefined {
21
+ const minPxInfo = lgv.bpToPx({
22
+ refName,
23
+ coord: feature.min,
24
+ regionNumber,
25
+ })
26
+ const maxPxInfo = lgv.bpToPx({
27
+ refName,
28
+ coord: feature.max,
29
+ regionNumber,
30
+ })
31
+ if (minPxInfo === undefined || maxPxInfo === undefined) {
32
+ return
33
+ }
34
+ const { offsetPx } = lgv
35
+ const minPx = minPxInfo.offsetPx - offsetPx
36
+ const maxPx = maxPxInfo.offsetPx - offsetPx
37
+ return [minPx, maxPx]
38
+ }
39
+
40
+ export function getOverlappingEdge(
41
+ feature: AnnotationFeature,
42
+ x: number,
43
+ minMax: [number, number],
44
+ ): { feature: AnnotationFeature; edge: 'min' | 'max' } | undefined {
45
+ const [minPx, maxPx] = minMax
46
+ // Feature is too small to tell if we're overlapping an edge
47
+ if (Math.abs(maxPx - minPx) < 8) {
48
+ return
49
+ }
50
+ if (Math.abs(minPx - x) < 4) {
51
+ return { feature, edge: 'min' }
52
+ }
53
+ if (Math.abs(maxPx - x) < 4) {
54
+ return { feature, edge: 'max' }
55
+ }
56
+ return
57
+ }
58
+
59
+ export function isSelectedFeature(
60
+ feature: AnnotationFeature,
61
+ selectedFeature: AnnotationFeature | undefined,
62
+ ) {
63
+ return Boolean(selectedFeature && feature._id === selectedFeature._id)
64
+ }
65
+
66
+ export function containsSelectedFeature(
67
+ feature: AnnotationFeature,
68
+ selectedFeature: AnnotationFeature | undefined,
69
+ ): boolean {
70
+ if (!selectedFeature) {
71
+ return false
72
+ }
73
+ if (feature._id === selectedFeature._id) {
74
+ return true
75
+ }
76
+ return feature.hasDescendant(selectedFeature._id)
77
+ }
78
+
79
+ function makeFeatureLabel(feature: AnnotationFeature) {
80
+ let name: string | undefined
81
+ if (feature.attributes.get('gff_name')) {
82
+ name = feature.attributes.get('gff_name')?.join(',')
83
+ } else if (feature.attributes.get('gff_id')) {
84
+ name = feature.attributes.get('gff_id')?.join(',')
85
+ } else {
86
+ name = feature._id
87
+ }
88
+ const coords = `(${(feature.min + 1).toLocaleString('en')}..${feature.max.toLocaleString('en')})`
89
+ const maxLen = 60
90
+ if (name && name.length + coords.length > maxLen + 5) {
91
+ const trim = maxLen - coords.length
92
+ name = trim > 0 ? name.slice(0, trim) : ''
93
+ name = `${name}[...]`
94
+ }
95
+ return `${name} ${coords}`
96
+ }
97
+
98
+ export function getContextMenuItemsForFeature(
99
+ display:
100
+ | LinearApolloSixFrameDisplayMouseEvents
101
+ | LinearApolloDisplayMouseEvents,
102
+ sourceFeature: AnnotationFeature,
103
+ ): MenuItem[] {
104
+ const {
105
+ apolloInternetAccount: internetAccount,
106
+ changeManager,
107
+ regions,
108
+ selectedFeature,
109
+ session,
110
+ } = display
111
+ const menuItems: MenuItem[] = []
112
+ const role = internetAccount ? internetAccount.role : 'admin'
113
+ const admin = role === 'admin'
114
+ const readOnly = !(role && ['admin', 'user'].includes(role))
115
+ const [region] = regions
116
+ const sourceAssemblyId = display.getAssemblyId(region.assemblyName)
117
+ const currentAssemblyId = display.getAssemblyId(region.assemblyName)
118
+ menuItems.push(
119
+ {
120
+ label: makeFeatureLabel(sourceFeature),
121
+ type: 'subHeader',
122
+ },
123
+ {
124
+ label: 'Add child feature',
125
+ disabled: readOnly,
126
+ onClick: () => {
127
+ ;(session as unknown as AbstractSessionModel).queueDialog(
128
+ (doneCallback) => [
129
+ AddChildFeature,
130
+ {
131
+ session,
132
+ handleClose: () => {
133
+ doneCallback()
134
+ },
135
+ changeManager,
136
+ sourceFeature,
137
+ sourceAssemblyId,
138
+ internetAccount,
139
+ },
140
+ ],
141
+ )
142
+ },
143
+ },
144
+ {
145
+ label: 'Copy features and annotations',
146
+ disabled: readOnly,
147
+ onClick: () => {
148
+ ;(session as unknown as AbstractSessionModel).queueDialog(
149
+ (doneCallback) => [
150
+ CopyFeature,
151
+ {
152
+ session,
153
+ handleClose: () => {
154
+ doneCallback()
155
+ },
156
+ changeManager,
157
+ sourceFeature,
158
+ sourceAssemblyId: currentAssemblyId,
159
+ },
160
+ ],
161
+ )
162
+ },
163
+ },
164
+ {
165
+ label: 'Delete feature',
166
+ disabled: !admin,
167
+ onClick: () => {
168
+ ;(session as unknown as AbstractSessionModel).queueDialog(
169
+ (doneCallback) => [
170
+ DeleteFeature,
171
+ {
172
+ session,
173
+ handleClose: () => {
174
+ doneCallback()
175
+ },
176
+ changeManager,
177
+ sourceFeature,
178
+ sourceAssemblyId: currentAssemblyId,
179
+ selectedFeature,
180
+ setSelectedFeature: (feature?: AnnotationFeature) => {
181
+ display.setSelectedFeature(feature)
182
+ },
183
+ },
184
+ ],
185
+ )
186
+ },
187
+ },
188
+ )
189
+ return menuItems
190
+ }
191
+
192
+ export function navToFeatureCenter(
193
+ feature: AnnotationFeature,
194
+ paddingPct: number,
195
+ refSeqLength: number,
196
+ ): NavLocation {
197
+ const paddingBp = (feature.max - feature.min) * paddingPct
198
+ const start = Math.max(feature.min - paddingBp, 1)
199
+ const end = Math.min(feature.max + paddingBp, refSeqLength)
200
+ return { refName: feature.refSeq, start, end }
201
+ }
package/src/util/index.ts CHANGED
@@ -30,3 +30,5 @@ export function getApolloInternetAccount(session: ApolloSessionModel) {
30
30
 
31
31
  export * from './loadAssemblyIntoClient'
32
32
  export * from './annotationFeatureUtils'
33
+ export * from './glyphUtils'
34
+ export * from './mouseEventsUtils'
@@ -0,0 +1,145 @@
1
+ import { type AnnotationFeature } from '@apollo-annotation/mst'
2
+ import { type LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
3
+
4
+ type MinEdge = 'min'
5
+ type MaxEdge = 'max'
6
+ export type Edge = MinEdge | MaxEdge
7
+
8
+ interface LocationChange {
9
+ featureId: string
10
+ oldLocation: number
11
+ newLocation: number
12
+ }
13
+
14
+ function expandFeatures(
15
+ feature: AnnotationFeature,
16
+ newLocation: number,
17
+ edge: Edge,
18
+ ): LocationChange[] {
19
+ const featureId = feature._id
20
+ const oldLocation = feature[edge]
21
+ const changes: LocationChange[] = [{ featureId, oldLocation, newLocation }]
22
+ const { parent } = feature
23
+ if (
24
+ parent &&
25
+ ((edge === 'min' && parent[edge] > newLocation) ||
26
+ (edge === 'max' && parent[edge] < newLocation))
27
+ ) {
28
+ changes.push(...expandFeatures(parent, newLocation, edge))
29
+ }
30
+ return changes
31
+ }
32
+
33
+ function shrinkFeatures(
34
+ feature: AnnotationFeature,
35
+ newLocation: number,
36
+ edge: Edge,
37
+ shrinkParent: boolean,
38
+ childIdToSkip?: string,
39
+ ): LocationChange[] {
40
+ const featureId = feature._id
41
+ const oldLocation = feature[edge]
42
+ const changes: LocationChange[] = [{ featureId, oldLocation, newLocation }]
43
+ const { parent, children } = feature
44
+ if (children) {
45
+ for (const [, child] of children) {
46
+ if (child._id === childIdToSkip) {
47
+ continue
48
+ }
49
+ if (
50
+ (edge === 'min' && child[edge] < newLocation) ||
51
+ (edge === 'max' && child[edge] > newLocation)
52
+ ) {
53
+ changes.push(...shrinkFeatures(child, newLocation, edge, shrinkParent))
54
+ }
55
+ }
56
+ }
57
+ if (parent && shrinkParent) {
58
+ const siblings: AnnotationFeature[] = []
59
+ if (parent.children) {
60
+ for (const [, c] of parent.children) {
61
+ if (c._id === featureId) {
62
+ continue
63
+ }
64
+ siblings.push(c)
65
+ }
66
+ }
67
+ if (siblings.length === 0) {
68
+ changes.push(
69
+ ...shrinkFeatures(parent, newLocation, edge, shrinkParent, featureId),
70
+ )
71
+ } else {
72
+ const oldLocation = parent[edge]
73
+ const boundedLocation = Math[edge](
74
+ ...siblings.map((s) => s[edge]),
75
+ newLocation,
76
+ )
77
+ if (boundedLocation !== oldLocation) {
78
+ changes.push(
79
+ ...shrinkFeatures(
80
+ parent,
81
+ boundedLocation,
82
+ edge,
83
+ shrinkParent,
84
+ featureId,
85
+ ),
86
+ )
87
+ }
88
+ }
89
+ }
90
+ return changes
91
+ }
92
+
93
+ export function getPropagatedLocationChanges(
94
+ feature: AnnotationFeature,
95
+ newLocation: number,
96
+ edge: Edge,
97
+ shrinkParent = false,
98
+ ): LocationChange[] {
99
+ const oldLocation = feature[edge]
100
+ if (newLocation === oldLocation) {
101
+ throw new Error(`New and existing locations are the same: "${newLocation}"`)
102
+ }
103
+ if (edge === 'min') {
104
+ if (newLocation > oldLocation) {
105
+ // shrinking feature, may need to shrink children and/or parents
106
+ return shrinkFeatures(feature, newLocation, edge, shrinkParent)
107
+ }
108
+ return expandFeatures(feature, newLocation, edge)
109
+ }
110
+ if (newLocation < oldLocation) {
111
+ return shrinkFeatures(feature, newLocation, edge, shrinkParent)
112
+ }
113
+ return expandFeatures(feature, newLocation, edge)
114
+ }
115
+
116
+ /** extended information about the position of the mouse on the canvas, including the refName, bp, and displayedRegion number */
117
+ export interface MousePosition {
118
+ x: number
119
+ y: number
120
+ refName: string
121
+ bp: number
122
+ regionNumber: number
123
+ feature?: AnnotationFeature
124
+ }
125
+
126
+ export type MousePositionWithFeature = Required<MousePosition>
127
+
128
+ export function isMousePositionWithFeature(
129
+ mousePosition: MousePosition,
130
+ ): mousePosition is MousePositionWithFeature {
131
+ return 'feature' in mousePosition
132
+ }
133
+
134
+ export function getMousePosition(
135
+ event: React.MouseEvent,
136
+ lgv: LinearGenomeViewModel,
137
+ ): MousePosition {
138
+ const canvas = event.currentTarget
139
+ const { clientX, clientY } = event
140
+ const { left, top } = canvas.getBoundingClientRect()
141
+ const x = clientX - left
142
+ const y = clientY - top
143
+ const { coord: bp, index: regionNumber, refName } = lgv.pxToBp(x)
144
+ return { x, y, refName, bp, regionNumber }
145
+ }