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

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 (56) hide show
  1. package/dist/index.esm.js +2679 -850
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +2676 -847
  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 +5194 -1258
  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 +4 -4
  12. package/src/ApolloInternetAccount/addMenuItems.ts +18 -0
  13. package/src/ChangeManager.ts +10 -6
  14. package/src/FeatureDetailsWidget/Attributes.tsx +8 -3
  15. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +12 -20
  16. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +929 -175
  17. package/src/FeatureDetailsWidget/TranscriptWidgetSummary.tsx +4 -0
  18. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +1 -1
  19. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +48 -60
  20. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +244 -51
  21. package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +46 -1
  22. package/src/LinearApolloDisplay/glyphs/Glyph.ts +9 -1
  23. package/src/LinearApolloDisplay/stateModel/base.ts +29 -0
  24. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +51 -35
  25. package/src/LinearApolloDisplay/stateModel/rendering.ts +2 -1
  26. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +7 -2
  27. package/src/LinearApolloSixFrameDisplay/components/TrackLines.tsx +12 -20
  28. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +243 -124
  29. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +42 -1
  30. package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +19 -3
  31. package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +53 -34
  32. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +4 -2
  33. package/src/OntologyManager/index.ts +4 -1
  34. package/src/TabularEditor/HybridGrid/Feature.tsx +4 -0
  35. package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +108 -16
  36. package/src/components/AddAssemblyAliases.tsx +114 -0
  37. package/src/components/AddChildFeature.tsx +3 -6
  38. package/src/components/AddFeature.tsx +14 -15
  39. package/src/components/CopyFeature.tsx +2 -4
  40. package/src/components/CreateApolloAnnotation.tsx +334 -151
  41. package/src/components/DeleteFeature.tsx +358 -11
  42. package/src/components/DownloadGFF3.tsx +20 -1
  43. package/src/components/FilterTranscripts.tsx +86 -0
  44. package/src/components/MergeExons.tsx +193 -0
  45. package/src/components/MergeTranscripts.tsx +185 -0
  46. package/src/components/SplitExon.tsx +134 -0
  47. package/src/components/index.ts +3 -0
  48. package/src/config.ts +5 -0
  49. package/src/extensions/annotationFromJBrowseFeature.ts +2 -0
  50. package/src/extensions/annotationFromPileup.ts +99 -89
  51. package/src/session/session.ts +26 -13
  52. package/src/util/annotationFeatureUtils.ts +65 -0
  53. package/src/util/copyToClipboard.ts +21 -0
  54. package/src/util/glyphUtils.ts +49 -0
  55. package/src/util/index.ts +2 -0
  56. package/src/util/mouseEventsUtils.ts +113 -0
@@ -5,15 +5,17 @@ import {
5
5
  import { type MenuItem } from '@jbrowse/core/ui'
6
6
  import {
7
7
  type AbstractSessionModel,
8
- type SessionWithWidgets,
9
8
  getFrame,
10
9
  intersection2,
11
- isSessionModelWithWidgets,
10
+ measureText,
12
11
  } from '@jbrowse/core/util'
13
12
  import { alpha } from '@mui/material'
14
13
  import equal from 'fast-deep-equal/es6'
14
+ import { getSnapshot } from 'mobx-state-tree'
15
15
 
16
16
  import { AddChildFeature, CopyFeature, DeleteFeature } from '../../components'
17
+ import { FilterTranscripts } from '../../components/FilterTranscripts'
18
+ import { getMinAndMaxPx, getOverlappingEdge } from '../../util'
17
19
  import { type LinearApolloSixFrameDisplay } from '../stateModel'
18
20
  import {
19
21
  type LinearApolloSixFrameDisplayMouseEvents,
@@ -30,7 +32,10 @@ let forwardFillLight: CanvasPattern | null = null
30
32
  let backwardFillLight: CanvasPattern | null = null
31
33
  let forwardFillDark: CanvasPattern | null = null
32
34
  let backwardFillDark: CanvasPattern | null = null
33
- if ('document' in globalThis) {
35
+ const canvas = globalThis.document.createElement('canvas')
36
+ // @ts-expect-error getContext is undefined in the web worker
37
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
38
+ if (canvas?.getContext) {
34
39
  for (const direction of ['forward', 'backward']) {
35
40
  for (const themeMode of ['light', 'dark']) {
36
41
  const canvas = document.createElement('canvas')
@@ -83,6 +88,37 @@ function deepSetHas<T>(set: Set<T>, item: T): boolean {
83
88
  return false
84
89
  }
85
90
 
91
+ interface Label {
92
+ x: number
93
+ y: number
94
+ h: number
95
+ text: string | undefined
96
+ color: string
97
+ isSelected: boolean
98
+ }
99
+
100
+ function drawTextLabels(
101
+ ctx: CanvasRenderingContext2D,
102
+ labelArray: Label[],
103
+ font = '10px sans-serif',
104
+ ) {
105
+ for (let i = labelArray.length - 1; i >= 0; --i) {
106
+ const label = labelArray[i]
107
+ ctx.fillStyle = label.color
108
+ const labelRowX = Math.max(label.x + 1, 0)
109
+ const labelRowY = label.y + label.h
110
+ const textWidth = measureText(label.text, 10)
111
+ if (label.isSelected) {
112
+ ctx.clearRect(labelRowX - 5, labelRowY, textWidth + 10, label.h)
113
+ ctx.font = 'bold '.concat(font)
114
+ }
115
+ if (label.text) {
116
+ ctx.fillText(label.text, labelRowX, labelRowY + 11, textWidth)
117
+ ctx.font = font
118
+ }
119
+ }
120
+ }
121
+
86
122
  function draw(
87
123
  ctx: CanvasRenderingContext2D,
88
124
  topLevelFeature: AnnotationFeature,
@@ -90,14 +126,25 @@ function draw(
90
126
  stateModel: LinearApolloSixFrameDisplayRendering,
91
127
  displayedRegionIndex: number,
92
128
  ): void {
93
- const { apolloRowHeight, lgv, session, theme, highestRow } = stateModel
129
+ const {
130
+ apolloRowHeight,
131
+ lgv,
132
+ session,
133
+ theme,
134
+ highestRow,
135
+ filteredTranscripts,
136
+ showFeatureLabels,
137
+ } = stateModel
94
138
  const { bpPerPx, displayedRegions, offsetPx } = lgv
95
139
  const displayedRegion = displayedRegions[displayedRegionIndex]
96
140
  const { refName, reversed } = displayedRegion
97
141
  const rowHeight = apolloRowHeight
98
- const exonHeight = Math.round(0.6 * rowHeight)
99
- const cdsHeight = Math.round(0.7 * rowHeight)
100
- const { children, min, strand, _id } = topLevelFeature
142
+ const exonHeight = rowHeight
143
+ const cdsHeight = rowHeight
144
+ const topLevelFeatureHeight = rowHeight
145
+ const featureLabelSpacer = showFeatureLabels ? 2 : 1
146
+ const textColor = theme?.palette.text.primary ?? 'black'
147
+ const { attributes, children, min, strand } = topLevelFeature
101
148
  if (!children) {
102
149
  return
103
150
  }
@@ -107,6 +154,7 @@ function draw(
107
154
  if (!featureTypeOntology) {
108
155
  throw new Error('featureTypeOntology is undefined')
109
156
  }
157
+ const labelArray: Label[] = []
110
158
 
111
159
  // Draw background for gene
112
160
  const topLevelFeatureMinX =
@@ -119,10 +167,8 @@ function draw(
119
167
  const topLevelFeatureStartPx = reversed
120
168
  ? topLevelFeatureMinX - topLevelFeatureWidthPx
121
169
  : topLevelFeatureMinX
122
- const topLevelRow = strand == 1 ? 3 : 4
170
+ const topLevelRow = (strand == 1 ? 3 : 4) * featureLabelSpacer
123
171
  const topLevelFeatureTop = topLevelRow * rowHeight
124
- const topLevelFeatureHeight = Math.round(0.7 * rowHeight)
125
-
126
172
  ctx.fillStyle = theme?.palette.text.primary ?? 'black'
127
173
  ctx.fillRect(
128
174
  topLevelFeatureStartPx,
@@ -131,10 +177,9 @@ function draw(
131
177
  topLevelFeatureHeight,
132
178
  )
133
179
 
134
- ctx.fillStyle =
135
- apolloSelectedFeature && _id === apolloSelectedFeature._id
136
- ? alpha('rgb(0,0,0)', 0.7)
137
- : alpha(theme?.palette.background.paper ?? '#ffffff', 0.7)
180
+ ctx.fillStyle = isSelectedFeature(topLevelFeature, apolloSelectedFeature)
181
+ ? alpha('rgb(0,0,0)', 0.7)
182
+ : alpha(theme?.palette.background.paper ?? '#ffffff', 0.7)
138
183
  ctx.fillRect(
139
184
  topLevelFeatureStartPx + 1,
140
185
  topLevelFeatureTop + 1,
@@ -142,6 +187,21 @@ function draw(
142
187
  topLevelFeatureHeight - 2,
143
188
  )
144
189
 
190
+ const isSelected = isSelectedFeature(topLevelFeature, apolloSelectedFeature)
191
+ const label: Label = {
192
+ x: topLevelFeatureStartPx,
193
+ y: topLevelFeatureTop,
194
+ h: topLevelFeatureHeight,
195
+ text: attributes.get('gff_id')?.toString(),
196
+ color: textColor,
197
+ isSelected,
198
+ }
199
+ if (isSelected) {
200
+ labelArray.unshift(label)
201
+ } else {
202
+ labelArray.push(label)
203
+ }
204
+
145
205
  const forwardFill =
146
206
  theme?.palette.mode === 'dark' ? forwardFillDark : forwardFillLight
147
207
  const backwardFill =
@@ -184,10 +244,16 @@ function draw(
184
244
  ) {
185
245
  continue
186
246
  }
187
- const { children: childrenOfmRNA, cdsLocations, _id } = child
247
+ const { children: childrenOfmRNA, cdsLocations } = child
188
248
  if (!childrenOfmRNA) {
189
249
  continue
190
250
  }
251
+ const childID: string | undefined = child.attributes
252
+ .get('gff_id')
253
+ ?.toString()
254
+ if (childID && filteredTranscripts.includes(childID)) {
255
+ continue
256
+ }
191
257
  for (const [, exon] of childrenOfmRNA) {
192
258
  if (!featureTypeOntology.isTypeOf(exon.type, 'exon')) {
193
259
  continue
@@ -203,14 +269,12 @@ function draw(
203
269
 
204
270
  const exonTop =
205
271
  topLevelFeatureTop + (topLevelFeatureHeight - exonHeight) / 2
272
+ const isSelected = isSelectedFeature(exon, apolloSelectedFeature)
206
273
  ctx.fillStyle = theme?.palette.text.primary ?? 'black'
207
274
  ctx.fillRect(startPx, exonTop, widthPx, exonHeight)
208
275
  if (widthPx > 2) {
209
276
  ctx.clearRect(startPx + 1, exonTop + 1, widthPx - 2, exonHeight - 2)
210
- ctx.fillStyle =
211
- apolloSelectedFeature && exon._id === apolloSelectedFeature._id
212
- ? 'rgb(0,0,0)'
213
- : alpha('#f5f500', 0.6)
277
+ ctx.fillStyle = isSelected ? 'rgb(0,0,0)' : alpha('#f5f500', 0.6)
214
278
  ctx.fillRect(startPx + 1, exonTop + 1, widthPx - 2, exonHeight - 2)
215
279
  if (topFill && bottomFill) {
216
280
  ctx.fillStyle = topFill
@@ -228,9 +292,26 @@ function draw(
228
292
  (exonHeight - 2) / 2,
229
293
  )
230
294
  }
295
+
296
+ const label: Label = {
297
+ x: startPx,
298
+ y: exonTop,
299
+ h: exonHeight,
300
+ text: exon.attributes.get('gff_id')?.toString(),
301
+ color: textColor,
302
+ isSelected,
303
+ }
304
+ if (isSelected) {
305
+ labelArray.unshift(label)
306
+ } else {
307
+ labelArray.push(label)
308
+ }
231
309
  }
232
310
  }
233
311
 
312
+ const isSelected = isSelectedFeature(child, apolloSelectedFeature?.parent)
313
+ let cdsStartPx = 0
314
+ let cdsTop = 0
234
315
  for (const cdsRow of cdsLocations) {
235
316
  let prevCDSTop = 0
236
317
  let prevCDSEndPx = 0
@@ -238,8 +319,8 @@ function draw(
238
319
  for (const cds of cdsRow.sort((a, b) => a.max - b.max)) {
239
320
  if (
240
321
  (apolloSelectedFeature &&
241
- featureTypeOntology.isTypeOf(apolloSelectedFeature.type, 'CDS') &&
242
- _id === apolloSelectedFeature.parent?._id) ||
322
+ isSelected &&
323
+ featureTypeOntology.isTypeOf(apolloSelectedFeature.type, 'CDS')) ||
243
324
  !deepSetHas(renderedCDS, cds)
244
325
  ) {
245
326
  const cdsWidthPx = (cds.max - cds.min) / bpPerPx
@@ -249,12 +330,12 @@ function draw(
249
330
  coord: cds.min,
250
331
  regionNumber: displayedRegionIndex,
251
332
  })?.offsetPx ?? 0) - offsetPx
252
- const cdsStartPx = reversed ? minX - cdsWidthPx : minX
333
+ cdsStartPx = reversed ? minX - cdsWidthPx : minX
253
334
  ctx.fillStyle = theme?.palette.text.primary ?? 'black'
254
335
  const frame = getFrame(cds.min, cds.max, child.strand ?? 1, cds.phase)
255
- const frameAdjust = frame < 0 ? -1 * frame + 5 : frame
256
- const cdsTop =
257
- (frameAdjust - 1) * rowHeight + (rowHeight - cdsHeight) / 2
336
+ const frameAdjust =
337
+ (frame < 0 ? -1 * frame + 5 : frame) * featureLabelSpacer
338
+ cdsTop = (frameAdjust - featureLabelSpacer) * rowHeight
258
339
  ctx.fillRect(cdsStartPx, cdsTop, cdsWidthPx, cdsHeight)
259
340
  if (cdsWidthPx > 2) {
260
341
  ctx.clearRect(
@@ -269,8 +350,8 @@ function draw(
269
350
  ctx.fillStyle = cdsColorCode
270
351
  ctx.fillStyle =
271
352
  apolloSelectedFeature &&
272
- featureTypeOntology.isTypeOf(apolloSelectedFeature.type, 'CDS') &&
273
- _id === apolloSelectedFeature.parent?._id
353
+ isSelected &&
354
+ featureTypeOntology.isTypeOf(apolloSelectedFeature.type, 'CDS')
274
355
  ? 'rgb(0,0,0)'
275
356
  : cdsColorCode
276
357
  ctx.fillRect(
@@ -286,7 +367,9 @@ function draw(
286
367
  const midPoint: [number, number] = [
287
368
  (cdsStartPx - prevCDSEndPx) / 2 + prevCDSEndPx,
288
369
  Math.max(
289
- frame < 0 ? rowHeight * highestRow + 1 : 1, // Avoid render ceiling
370
+ frame < 0
371
+ ? rowHeight * featureLabelSpacer * highestRow + 1
372
+ : 1, // Avoid render ceiling
290
373
  Math.min(prevCDSTop, cdsTop) - rowHeight / 2,
291
374
  ),
292
375
  ]
@@ -324,6 +407,22 @@ function draw(
324
407
  }
325
408
  }
326
409
  }
410
+ const label: Label = {
411
+ x: cdsStartPx,
412
+ y: cdsTop,
413
+ h: cdsHeight,
414
+ text: child.attributes.get('gff_id')?.toString(),
415
+ color: textColor,
416
+ isSelected,
417
+ }
418
+ if (isSelected) {
419
+ labelArray.unshift(label)
420
+ } else {
421
+ labelArray.push(label)
422
+ }
423
+ }
424
+ if (showFeatureLabels) {
425
+ drawTextLabels(ctx, labelArray)
327
426
  }
328
427
  }
329
428
 
@@ -360,7 +459,15 @@ function drawHover(
360
459
  stateModel: LinearApolloSixFrameDisplay,
361
460
  ctx: CanvasRenderingContext2D,
362
461
  ) {
363
- const { apolloHover, apolloRowHeight, lgv, highestRow, session } = stateModel
462
+ const {
463
+ apolloHover,
464
+ apolloRowHeight,
465
+ filteredTranscripts,
466
+ lgv,
467
+ highestRow,
468
+ session,
469
+ showFeatureLabels,
470
+ } = stateModel
364
471
  if (!apolloHover) {
365
472
  return
366
473
  }
@@ -373,6 +480,12 @@ function drawHover(
373
480
  if (!featureTypeOntology.isTypeOf(feature.type, 'transcript')) {
374
481
  return
375
482
  }
483
+ const featureID: string | undefined = feature.attributes
484
+ .get('gff_id')
485
+ ?.toString()
486
+ if (featureID && filteredTranscripts.includes(featureID)) {
487
+ return
488
+ }
376
489
  const position = stateModel.getFeatureLayoutPosition(feature)
377
490
  if (!position) {
378
491
  return
@@ -382,7 +495,8 @@ function drawHover(
382
495
  const displayedRegion = displayedRegions[layoutIndex]
383
496
  const { refName, reversed } = displayedRegion
384
497
  const rowHeight = apolloRowHeight
385
- const cdsHeight = Math.round(0.7 * rowHeight)
498
+ const cdsHeight = rowHeight
499
+ const featureLabelSpacer = showFeatureLabels ? 2 : 1
386
500
  const { cdsLocations, strand } = feature
387
501
  for (const cdsRow of cdsLocations) {
388
502
  let prevCDSTop = 0
@@ -399,9 +513,9 @@ function drawHover(
399
513
  })?.offsetPx ?? 0) - offsetPx
400
514
  const cdsStartPx = reversed ? minX - cdsWidthPx : minX
401
515
  const frame = getFrame(cds.min, cds.max, strand ?? 1, cds.phase)
402
- const frameAdjust = frame < 0 ? -1 * frame + 5 : frame
403
- const cdsTop =
404
- (frameAdjust - 1) * rowHeight + (rowHeight - cdsHeight) / 2
516
+ const frameAdjust =
517
+ (frame < 0 ? -1 * frame + 5 : frame) * featureLabelSpacer
518
+ const cdsTop = (frameAdjust - featureLabelSpacer) * rowHeight
405
519
  ctx.fillStyle = 'rgba(255,0,0,0.6)'
406
520
  ctx.fillRect(cdsStartPx, cdsTop, cdsWidthPx, cdsHeight)
407
521
 
@@ -410,7 +524,7 @@ function drawHover(
410
524
  const midPoint: [number, number] = [
411
525
  (cdsStartPx - prevCDSEndPx) / 2 + prevCDSEndPx,
412
526
  Math.max(
413
- frame < 0 ? rowHeight * highestRow + 1 : 1, // Avoid render ceiling
527
+ frame < 0 ? rowHeight * featureLabelSpacer * highestRow + 1 : 1, // Avoid render ceiling
414
528
  Math.min(prevCDSTop, cdsTop) - rowHeight / 2,
415
529
  ),
416
530
  ]
@@ -453,6 +567,7 @@ function onMouseDown(
453
567
  currentMousePosition,
454
568
  draggableFeature.feature,
455
569
  draggableFeature.edge,
570
+ true,
456
571
  )
457
572
  }
458
573
  }
@@ -490,33 +605,53 @@ function onMouseUp(
490
605
  const { session } = stateModel
491
606
  const { apolloDataStore } = session
492
607
  const { featureTypeOntology } = apolloDataStore.ontologyManager
493
- if (featureAndGlyphUnderMouse?.cds) {
494
- const { cds, feature } = featureAndGlyphUnderMouse
495
- if (!featureTypeOntology) {
496
- throw new Error('featureTypeOntology is undefined')
497
- }
498
- if (!feature.children) {
499
- return
500
- }
501
- for (const child of feature.children.values()) {
502
- const childIsCDS = featureTypeOntology.isTypeOf(child.type, 'CDS')
503
- if (childIsCDS && cds.max <= child.max && cds.min >= child.min) {
504
- stateModel.setSelectedFeature(child)
505
- break
506
- }
608
+ if (!featureAndGlyphUnderMouse) {
609
+ return
610
+ }
611
+ const { feature } = featureAndGlyphUnderMouse
612
+ stateModel.setSelectedFeature(feature)
613
+ if (!featureTypeOntology) {
614
+ throw new Error('featureTypeOntology is undefined')
615
+ }
616
+
617
+ let containsCDSOrExon = false
618
+ for (const [, child] of feature.children ?? []) {
619
+ if (
620
+ featureTypeOntology.isTypeOf(child.type, 'CDS') ||
621
+ featureTypeOntology.isTypeOf(child.type, 'exon')
622
+ ) {
623
+ containsCDSOrExon = true
624
+ break
507
625
  }
508
- } else if (featureAndGlyphUnderMouse?.feature) {
509
- stateModel.setSelectedFeature(featureAndGlyphUnderMouse.feature)
626
+ }
627
+ if (
628
+ (featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
629
+ featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')) &&
630
+ containsCDSOrExon
631
+ ) {
632
+ stateModel.showFeatureDetailsWidget(feature, [
633
+ 'ApolloTranscriptDetails',
634
+ 'apolloTranscriptDetails',
635
+ ])
636
+ } else {
637
+ stateModel.showFeatureDetailsWidget(feature)
510
638
  }
511
639
  }
512
640
 
641
+ export function isSelectedFeature(
642
+ feature: AnnotationFeature,
643
+ selectedFeature: AnnotationFeature | undefined,
644
+ ) {
645
+ return Boolean(selectedFeature && feature._id === selectedFeature._id)
646
+ }
647
+
513
648
  function getDraggableFeatureInfo(
514
649
  mousePosition: MousePosition,
515
650
  cds: TranscriptPartCoding | null,
516
651
  feature: AnnotationFeature,
517
652
  stateModel: LinearApolloSixFrameDisplay,
518
653
  ): { feature: AnnotationFeature; edge: 'min' | 'max' } | undefined {
519
- const { session } = stateModel
654
+ const { filteredTranscripts, session } = stateModel
520
655
  const { apolloDataStore } = session
521
656
  const { featureTypeOntology } = apolloDataStore.ontologyManager
522
657
  if (!featureTypeOntology) {
@@ -526,63 +661,57 @@ function getDraggableFeatureInfo(
526
661
  if (cds === null) {
527
662
  return
528
663
  }
529
- const { bp, refName, regionNumber, x } = mousePosition
530
- const { lgv } = stateModel
531
- const { offsetPx } = lgv
532
-
533
- const minPxInfo = lgv.bpToPx({ refName, coord: cds.min, regionNumber })
534
- const maxPxInfo = lgv.bpToPx({ refName, coord: cds.max, regionNumber })
535
- if (minPxInfo === undefined || maxPxInfo === undefined) {
536
- return
537
- }
538
- const minPx = minPxInfo.offsetPx - offsetPx
539
- const maxPx = maxPxInfo.offsetPx - offsetPx
540
- if (Math.abs(maxPx - minPx) < 8) {
664
+ const featureID: string | undefined = feature.attributes
665
+ .get('gff_id')
666
+ ?.toString()
667
+ if (featureID && filteredTranscripts.includes(featureID)) {
541
668
  return
542
669
  }
670
+ const { bp, refName, regionNumber, x } = mousePosition
671
+ const { lgv } = stateModel
543
672
  if (isTranscript) {
544
673
  const transcript = feature
545
674
  if (!transcript.children) {
546
675
  return
547
676
  }
548
677
  const exonChildren: AnnotationFeature[] = []
678
+ const cdsChildren: AnnotationFeature[] = []
549
679
  for (const child of transcript.children.values()) {
550
680
  const childIsExon = featureTypeOntology.isTypeOf(child.type, 'exon')
681
+ const childIsCDS = featureTypeOntology.isTypeOf(child.type, 'CDS')
551
682
  if (childIsExon) {
552
683
  exonChildren.push(child)
684
+ } else if (childIsCDS) {
685
+ cdsChildren.push(child)
553
686
  }
554
687
  }
555
-
556
688
  const overlappingExon = exonChildren.find((child) => {
557
689
  const [start, end] = intersection2(bp, bp + 1, child.min, child.max)
558
690
  return start !== undefined && end !== undefined
559
691
  })
560
- if (!overlappingExon) {
561
- return
562
- }
563
- const minPxInfo = lgv.bpToPx({
564
- refName,
565
- coord: overlappingExon.min,
566
- regionNumber,
567
- })
568
- const maxPxInfo = lgv.bpToPx({
569
- refName,
570
- coord: overlappingExon.max,
571
- regionNumber,
572
- })
573
- if (minPxInfo === undefined || maxPxInfo === undefined) {
574
- return
575
- }
576
- const minPx = minPxInfo.offsetPx - offsetPx
577
- const maxPx = maxPxInfo.offsetPx - offsetPx
578
- if (Math.abs(maxPx - minPx) < 8) {
579
- return
580
- }
581
- if (Math.abs(minPx - x) < 4) {
582
- return { feature: overlappingExon, edge: 'min' }
692
+ if (overlappingExon) {
693
+ // We are on an exon, are we on the edge of it?
694
+ const minMax = getMinAndMaxPx(overlappingExon, refName, regionNumber, lgv)
695
+ if (minMax) {
696
+ const overlappingEdge = getOverlappingEdge(overlappingExon, x, minMax)
697
+ if (overlappingEdge) {
698
+ return overlappingEdge
699
+ }
700
+ }
583
701
  }
584
- if (Math.abs(maxPx - x) < 4) {
585
- return { feature: overlappingExon, edge: 'max' }
702
+ // End of special cases, let's see if we're on the edge of this CDS or exon
703
+ const minMax = getMinAndMaxPx(cds, refName, regionNumber, lgv)
704
+ if (minMax) {
705
+ const overlappingCDS = cdsChildren.find((child) => {
706
+ const [start, end] = intersection2(bp, bp + 1, child.min, child.max)
707
+ return start !== undefined && end !== undefined
708
+ })
709
+ if (overlappingCDS) {
710
+ const overlappingEdge = getOverlappingEdge(overlappingCDS, x, minMax)
711
+ if (overlappingEdge) {
712
+ return overlappingEdge
713
+ }
714
+ }
586
715
  }
587
716
  }
588
717
  return
@@ -592,7 +721,8 @@ function drawTooltip(
592
721
  display: LinearApolloSixFrameDisplayMouseEvents,
593
722
  context: CanvasRenderingContext2D,
594
723
  ): void {
595
- const { apolloHover, apolloRowHeight, lgv, theme } = display
724
+ const { apolloHover, apolloRowHeight, filteredTranscripts, lgv, theme } =
725
+ display
596
726
  if (!apolloHover) {
597
727
  return
598
728
  }
@@ -604,6 +734,12 @@ function drawTooltip(
604
734
  if (!position) {
605
735
  return
606
736
  }
737
+ const featureID: string | undefined = feature.attributes
738
+ .get('gff_id')
739
+ ?.toString()
740
+ if (featureID && filteredTranscripts.includes(featureID)) {
741
+ return
742
+ }
607
743
  const { layoutIndex } = position
608
744
  const { bpPerPx, displayedRegions, offsetPx } = lgv
609
745
  const displayedRegion = displayedRegions[layoutIndex]
@@ -675,6 +811,7 @@ function getContextMenuItems(
675
811
  apolloHover,
676
812
  apolloInternetAccount: internetAccount,
677
813
  changeManager,
814
+ filteredTranscripts,
678
815
  regions,
679
816
  selectedFeature,
680
817
  session,
@@ -756,48 +893,30 @@ function getContextMenuItems(
756
893
  )
757
894
  },
758
895
  },
759
- {
760
- label: 'Edit feature details',
761
- onClick: () => {
762
- const apolloFeatureWidget = (
763
- session as unknown as SessionWithWidgets
764
- ).addWidget(
765
- 'ApolloFeatureDetailsWidget',
766
- 'apolloFeatureDetailsWidget',
767
- {
768
- feature: sourceFeature,
769
- assembly: currentAssemblyId,
770
- refName: region.refName,
771
- },
772
- )
773
- ;(session as unknown as SessionWithWidgets).showWidget(
774
- apolloFeatureWidget,
775
- )
776
- },
777
- },
778
896
  )
779
897
  const { featureTypeOntology } = session.apolloDataStore.ontologyManager
780
898
  if (!featureTypeOntology) {
781
899
  throw new Error('featureTypeOntology is undefined')
782
900
  }
783
- if (
784
- featureTypeOntology.isTypeOf(sourceFeature.type, 'transcript') &&
785
- isSessionModelWithWidgets(session)
786
- ) {
901
+ if (featureTypeOntology.isTypeOf(sourceFeature.type, 'gene')) {
787
902
  menuItems.push({
788
- label: 'Edit transcript details',
903
+ label: 'Filter alternate transcripts',
789
904
  onClick: () => {
790
- const apolloTranscriptWidget = session.addWidget(
791
- 'ApolloTranscriptDetails',
792
- 'apolloTranscriptDetails',
793
- {
794
- feature: sourceFeature,
795
- assembly: currentAssemblyId,
796
- changeManager,
797
- refName: region.refName,
798
- },
905
+ ;(session as unknown as AbstractSessionModel).queueDialog(
906
+ (doneCallback) => [
907
+ FilterTranscripts,
908
+ {
909
+ handleClose: () => {
910
+ doneCallback()
911
+ },
912
+ sourceFeature,
913
+ filteredTranscripts: getSnapshot(filteredTranscripts),
914
+ onUpdate: (forms: string[]) => {
915
+ display.updateFilteredTranscripts(forms)
916
+ },
917
+ },
918
+ ],
799
919
  )
800
- session.showWidget(apolloTranscriptWidget)
801
920
  },
802
921
  })
803
922
  }
@@ -10,6 +10,7 @@ import { type AnyConfigurationSchemaType } from '@jbrowse/core/configuration/con
10
10
  import { BaseDisplay } from '@jbrowse/core/pluggableElementTypes'
11
11
  import {
12
12
  type AbstractSessionModel,
13
+ type SessionWithWidgets,
13
14
  getContainingView,
14
15
  getSession,
15
16
  } from '@jbrowse/core/util'
@@ -36,6 +37,7 @@ export function baseModelFactory(
36
37
  configuration: ConfigurationReference(configSchema),
37
38
  graphical: true,
38
39
  table: false,
40
+ showFeatureLabels: true,
39
41
  heightPreConfig: types.maybe(
40
42
  types.refinement(
41
43
  'displayHeight',
@@ -164,6 +166,9 @@ export function baseModelFactory(
164
166
  self.graphical = true
165
167
  self.table = true
166
168
  },
169
+ toggleShowFeatureLabels() {
170
+ self.showFeatureLabels = !self.showFeatureLabels
171
+ },
167
172
  updateFilteredFeatureTypes(types: string[]) {
168
173
  self.filteredFeatureTypes = cast(types)
169
174
  },
@@ -172,7 +177,7 @@ export function baseModelFactory(
172
177
  const { filteredFeatureTypes, trackMenuItems: superTrackMenuItems } = self
173
178
  return {
174
179
  trackMenuItems() {
175
- const { graphical, table } = self
180
+ const { graphical, table, showFeatureLabels } = self
176
181
  return [
177
182
  ...superTrackMenuItems(),
178
183
  {
@@ -203,6 +208,14 @@ export function baseModelFactory(
203
208
  self.showGraphicalAndTable()
204
209
  },
205
210
  },
211
+ {
212
+ label: 'Feature Labels',
213
+ type: 'checkbox',
214
+ checked: showFeatureLabels,
215
+ onClick: () => {
216
+ self.toggleShowFeatureLabels()
217
+ },
218
+ },
206
219
  ],
207
220
  },
208
221
  {
@@ -236,6 +249,34 @@ export function baseModelFactory(
236
249
  self.session as unknown as ApolloSessionModel
237
250
  ).apolloSetSelectedFeature(feature)
238
251
  },
252
+ showFeatureDetailsWidget(
253
+ feature: AnnotationFeature,
254
+ customWidgetNameAndId?: [string, string],
255
+ ) {
256
+ const [region] = self.regions
257
+ const { assemblyName, refName } = region
258
+ const assembly = self.getAssemblyId(assemblyName)
259
+ if (!assembly) {
260
+ return
261
+ }
262
+ const { session } = self
263
+ const { changeManager } = session.apolloDataStore
264
+ const [widgetName, widgetId] = customWidgetNameAndId ?? [
265
+ 'ApolloFeatureDetailsWidget',
266
+ 'apolloFeatureDetailsWidget',
267
+ ]
268
+ const apolloFeatureWidget = (
269
+ session as unknown as SessionWithWidgets
270
+ ).addWidget(widgetName, widgetId, {
271
+ feature,
272
+ assembly,
273
+ refName,
274
+ changeManager,
275
+ })
276
+ ;(session as unknown as SessionWithWidgets).showWidget(
277
+ apolloFeatureWidget,
278
+ )
279
+ },
239
280
  afterAttach() {
240
281
  addDisposer(
241
282
  self,