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

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 (54) hide show
  1. package/dist/index.esm.js +10914 -10799
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +10979 -10865
  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 +8799 -11326
  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 +7 -7
  12. package/src/ApolloInternetAccount/components/AuthTypeSelector.tsx +1 -1
  13. package/src/ApolloInternetAccount/model.ts +87 -65
  14. package/src/ApolloRefNameAliasAdapter/ApolloRefNameAliasAdapter.ts +4 -4
  15. package/src/ApolloSequenceAdapter/ApolloSequenceAdapter.ts +9 -7
  16. package/src/BackendDrivers/CollaborationServerDriver.ts +60 -23
  17. package/src/BackendDrivers/DesktopFileDriver.ts +2 -2
  18. package/src/ChangeManager.ts +22 -5
  19. package/src/FeatureDetailsWidget/BasicInformation.tsx +6 -4
  20. package/src/FeatureDetailsWidget/NumberTextField.tsx +5 -2
  21. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +68 -212
  22. package/src/LinearApolloDisplay/components/CheckResultWarnings.tsx +92 -0
  23. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +6 -102
  24. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +33 -232
  25. package/src/LinearApolloDisplay/glyphs/util.ts +36 -0
  26. package/src/LinearApolloReferenceSequenceDisplay/drawSequenceOverlay.ts +174 -0
  27. package/src/LinearApolloReferenceSequenceDisplay/drawSequenceTrack.ts +200 -0
  28. package/src/LinearApolloReferenceSequenceDisplay/stateModel/rendering.ts +62 -386
  29. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +6 -0
  30. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +122 -70
  31. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +33 -2
  32. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +101 -3
  33. package/src/components/AddAssembly.tsx +34 -38
  34. package/src/components/AddFeature.tsx +21 -18
  35. package/src/components/AddRefSeqAliases.tsx +56 -42
  36. package/src/components/CopyFeature.tsx +1 -1
  37. package/src/components/CreateApolloAnnotation.tsx +22 -10
  38. package/src/components/DeleteAssembly.tsx +2 -9
  39. package/src/components/DownloadGFF3.tsx +2 -2
  40. package/src/components/ImportFeatures.tsx +1 -1
  41. package/src/components/ManageChecks.tsx +2 -9
  42. package/src/components/ManageUsers.tsx +23 -22
  43. package/src/components/OntologyTermAutocomplete.tsx +3 -10
  44. package/src/components/OntologyTermMultiSelect.tsx +2 -2
  45. package/src/components/ViewChangeLog.tsx +25 -50
  46. package/src/components/ViewCheckResults.tsx +1 -7
  47. package/src/config.ts +3 -3
  48. package/src/index.ts +17 -16
  49. package/src/makeDisplayComponent.tsx +9 -13
  50. package/src/session/ClientDataStore.ts +33 -15
  51. package/src/session/session.ts +23 -27
  52. package/src/util/displayUtils.ts +28 -0
  53. package/src/util/glyphUtils.ts +196 -1
  54. package/src/util/loadAssemblyIntoClient.ts +3 -2
@@ -11,6 +11,7 @@ import {
11
11
  } from '@jbrowse/core/util'
12
12
  import { type LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
13
13
  import ErrorIcon from '@mui/icons-material/Error'
14
+ import LockIcon from '@mui/icons-material/Lock'
14
15
  import { Alert, Avatar, Badge, Box, Tooltip, useTheme } from '@mui/material'
15
16
  import { observer } from 'mobx-react'
16
17
  import React, { useEffect, useState } from 'react'
@@ -88,6 +89,11 @@ export const LinearApolloSixFrameDisplay = observer(
88
89
  }
89
90
  }}
90
91
  >
92
+ {session.isLocked ? (
93
+ <div className={classes.locked} data-testid="lock-icon">
94
+ <LockIcon />
95
+ </div>
96
+ ) : null}
91
97
  {message ? (
92
98
  <Alert
93
99
  severity="warning"
@@ -2,13 +2,16 @@ import {
2
2
  type AnnotationFeature,
3
3
  type TranscriptPartCoding,
4
4
  } from '@apollo-annotation/mst'
5
+ import { type BaseDisplayModel } from '@jbrowse/core/pluggableElementTypes'
5
6
  import { type MenuItem } from '@jbrowse/core/ui'
6
7
  import {
7
8
  type AbstractSessionModel,
9
+ getContainingView,
8
10
  getFrame,
9
11
  intersection2,
10
12
  measureText,
11
13
  } from '@jbrowse/core/util'
14
+ import { type LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
12
15
  import { alpha } from '@mui/material'
13
16
  import equal from 'fast-deep-equal/es6'
14
17
  import { getSnapshot } from 'mobx-state-tree'
@@ -18,12 +21,19 @@ import { FilterTranscripts } from '../../components/FilterTranscripts'
18
21
  import {
19
22
  type MousePosition,
20
23
  type MousePositionWithFeature,
24
+ getAdjacentExons,
21
25
  getContextMenuItemsForFeature,
22
26
  getMinAndMaxPx,
23
27
  getOverlappingEdge,
24
28
  getRelatedFeatures,
29
+ getStreamIcon,
30
+ isCDSFeature,
31
+ isExonFeature,
25
32
  isMousePositionWithFeature,
33
+ // isTranscriptFeature,
26
34
  isSelectedFeature,
35
+ navToFeatureCenter,
36
+ selectFeatureAndOpenWidget,
27
37
  } from '../../util'
28
38
  import { type LinearApolloSixFrameDisplay } from '../stateModel'
29
39
  import { type LinearApolloSixFrameDisplayMouseEvents } from '../stateModel/mouseEvents'
@@ -109,14 +119,14 @@ function drawTextLabels(
109
119
  for (let i = labelArray.length - 1; i >= 0; --i) {
110
120
  const label = labelArray[i]
111
121
  ctx.fillStyle = label.color
112
- const labelRowX = Math.max(label.x + 1, 0)
122
+ const labelRowX = label.x + 1
113
123
  const labelRowY = label.y + label.h
114
124
  const textWidth = measureText(label.text, 10)
115
125
  if (label.isSelected) {
116
- ctx.clearRect(labelRowX - 5, labelRowY, textWidth + 10, label.h)
117
126
  ctx.font = 'bold '.concat(font)
118
127
  }
119
128
  if (label.text) {
129
+ ctx.clearRect(labelRowX - 5, labelRowY, textWidth + 10, label.h)
120
130
  ctx.fillText(label.text, labelRowX, labelRowY + 11, textWidth)
121
131
  ctx.font = font
122
132
  }
@@ -341,6 +351,30 @@ function draw(
341
351
  (frame < 0 ? -1 * frame + 5 : frame) * featureLabelSpacer
342
352
  cdsTop = (frameAdjust - featureLabelSpacer) * rowHeight
343
353
  ctx.fillRect(cdsStartPx, cdsTop, cdsWidthPx, cdsHeight)
354
+
355
+ // Draw lines to connect CDS features with shared mRNA parent
356
+ if (counter > 1) {
357
+ // Mid-point for intron line "hat"
358
+ const midPoint: [number, number] = [
359
+ (cdsStartPx - prevCDSEndPx) / 2 + prevCDSEndPx,
360
+ Math.max(
361
+ frame < 0 ? rowHeight * featureLabelSpacer * highestRow + 1 : 1, // Avoid render ceiling
362
+ Math.min(prevCDSTop, cdsTop) - rowHeight / 2,
363
+ ),
364
+ ]
365
+ ctx.strokeStyle = 'rgb(0, 128, 128)'
366
+ ctx.beginPath()
367
+ ctx.moveTo(prevCDSEndPx, prevCDSTop)
368
+ ctx.lineTo(...midPoint)
369
+ ctx.stroke()
370
+ ctx.moveTo(...midPoint)
371
+ ctx.lineTo(cdsStartPx, cdsTop + rowHeight / 2)
372
+ ctx.stroke()
373
+ }
374
+ prevCDSEndPx = cdsStartPx + cdsWidthPx
375
+ prevCDSTop = cdsTop + rowHeight / 2
376
+ counter += 1
377
+
344
378
  if (cdsWidthPx > 2) {
345
379
  ctx.clearRect(
346
380
  cdsStartPx + 1,
@@ -365,31 +399,6 @@ function draw(
365
399
  cdsHeight - 2,
366
400
  )
367
401
 
368
- // Draw lines to connect CDS features with shared mRNA parent
369
- if (counter > 1) {
370
- // Mid-point for intron line "hat"
371
- const midPoint: [number, number] = [
372
- (cdsStartPx - prevCDSEndPx) / 2 + prevCDSEndPx,
373
- Math.max(
374
- frame < 0
375
- ? rowHeight * featureLabelSpacer * highestRow + 1
376
- : 1, // Avoid render ceiling
377
- Math.min(prevCDSTop, cdsTop) - rowHeight / 2,
378
- ),
379
- ]
380
- ctx.strokeStyle = 'rgb(0, 128, 128)'
381
- ctx.beginPath()
382
- ctx.moveTo(prevCDSEndPx, prevCDSTop)
383
- ctx.lineTo(...midPoint)
384
- ctx.stroke()
385
- ctx.moveTo(...midPoint)
386
- ctx.lineTo(cdsStartPx, cdsTop + rowHeight / 2)
387
- ctx.stroke()
388
- }
389
- prevCDSEndPx = cdsStartPx + cdsWidthPx
390
- prevCDSTop = cdsTop + rowHeight / 2
391
- counter += 1
392
-
393
402
  if (topFill && bottomFill) {
394
403
  ctx.fillStyle = topFill
395
404
  ctx.fillRect(
@@ -508,43 +517,42 @@ function drawHover(
508
517
  let counter = 1
509
518
  for (const cds of cdsRow.sort((a, b) => a.max - b.max)) {
510
519
  const cdsWidthPx = (cds.max - cds.min) / bpPerPx
520
+ const minX =
521
+ (lgv.bpToPx({
522
+ refName,
523
+ coord: cds.min,
524
+ regionNumber: layoutIndex,
525
+ })?.offsetPx ?? 0) - offsetPx
526
+ const cdsStartPx = reversed ? minX - cdsWidthPx : minX
527
+ const frame = getFrame(cds.min, cds.max, strand ?? 1, cds.phase)
528
+ const frameAdjust =
529
+ (frame < 0 ? -1 * frame + 5 : frame) * featureLabelSpacer
530
+ const cdsTop = (frameAdjust - featureLabelSpacer) * rowHeight
531
+ if (counter > 1) {
532
+ // Mid-point for intron line "hat"
533
+ const midPoint: [number, number] = [
534
+ (cdsStartPx - prevCDSEndPx) / 2 + prevCDSEndPx,
535
+ Math.max(
536
+ frame < 0 ? rowHeight * featureLabelSpacer * highestRow + 1 : 1, // Avoid render ceiling
537
+ Math.min(prevCDSTop, cdsTop) - rowHeight / 2,
538
+ ),
539
+ ]
540
+ ctx.strokeStyle = 'rgb(0, 0, 0)'
541
+ ctx.lineWidth = 2
542
+ ctx.beginPath()
543
+ ctx.moveTo(prevCDSEndPx, prevCDSTop)
544
+ ctx.lineTo(...midPoint)
545
+ ctx.stroke()
546
+ ctx.moveTo(...midPoint)
547
+ ctx.lineTo(cdsStartPx, cdsTop + rowHeight / 2)
548
+ ctx.stroke()
549
+ }
550
+ prevCDSEndPx = cdsStartPx + cdsWidthPx
551
+ prevCDSTop = cdsTop + rowHeight / 2
552
+ counter += 1
511
553
  if (cdsWidthPx > 2) {
512
- const minX =
513
- (lgv.bpToPx({
514
- refName,
515
- coord: cds.min,
516
- regionNumber: layoutIndex,
517
- })?.offsetPx ?? 0) - offsetPx
518
- const cdsStartPx = reversed ? minX - cdsWidthPx : minX
519
- const frame = getFrame(cds.min, cds.max, strand ?? 1, cds.phase)
520
- const frameAdjust =
521
- (frame < 0 ? -1 * frame + 5 : frame) * featureLabelSpacer
522
- const cdsTop = (frameAdjust - featureLabelSpacer) * rowHeight
523
554
  ctx.fillStyle = 'rgba(255,0,0,0.6)'
524
555
  ctx.fillRect(cdsStartPx, cdsTop, cdsWidthPx, cdsHeight)
525
-
526
- if (counter > 1) {
527
- // Mid-point for intron line "hat"
528
- const midPoint: [number, number] = [
529
- (cdsStartPx - prevCDSEndPx) / 2 + prevCDSEndPx,
530
- Math.max(
531
- frame < 0 ? rowHeight * featureLabelSpacer * highestRow + 1 : 1, // Avoid render ceiling
532
- Math.min(prevCDSTop, cdsTop) - rowHeight / 2,
533
- ),
534
- ]
535
- ctx.strokeStyle = 'rgb(0, 0, 0)'
536
- ctx.lineWidth = 2
537
- ctx.beginPath()
538
- ctx.moveTo(prevCDSEndPx, prevCDSTop)
539
- ctx.lineTo(...midPoint)
540
- ctx.stroke()
541
- ctx.moveTo(...midPoint)
542
- ctx.lineTo(cdsStartPx, cdsTop + rowHeight / 2)
543
- ctx.stroke()
544
- }
545
- prevCDSEndPx = cdsStartPx + cdsWidthPx
546
- prevCDSTop = cdsTop + rowHeight / 2
547
- counter += 1
548
556
  }
549
557
  }
550
558
  }
@@ -848,8 +856,13 @@ function getContextMenuItems(
848
856
  }
849
857
  if (isMousePositionWithFeature(mousePosition)) {
850
858
  const { bp, feature } = mousePosition
851
- for (const relatedFeature of getRelatedFeatures(feature, bp)) {
852
- const featureID: string | undefined = relatedFeature.attributes
859
+ let featuresUnderClick = getRelatedFeatures(feature, bp)
860
+ if (isCDSFeature(feature, session)) {
861
+ featuresUnderClick = getRelatedFeatures(feature, bp, true)
862
+ }
863
+
864
+ for (const feature of featuresUnderClick) {
865
+ const featureID: string | undefined = feature.attributes
853
866
  .get('gff_id')
854
867
  ?.toString()
855
868
  if (featureID && filteredTranscripts.includes(featureID)) {
@@ -857,9 +870,48 @@ function getContextMenuItems(
857
870
  }
858
871
  const contextMenuItemsForFeature = getContextMenuItemsForFeature(
859
872
  display,
860
- relatedFeature,
873
+ feature,
861
874
  )
862
- if (featureTypeOntology.isTypeOf(relatedFeature.type, 'exon')) {
875
+ if (isExonFeature(feature, session)) {
876
+ const adjacentExons = getAdjacentExons(
877
+ feature,
878
+ display,
879
+ mousePosition,
880
+ session,
881
+ )
882
+ const lgv = getContainingView(
883
+ display as BaseDisplayModel,
884
+ ) as unknown as LinearGenomeViewModel
885
+ if (adjacentExons.upstream) {
886
+ const exon = adjacentExons.upstream
887
+ contextMenuItemsForFeature.push({
888
+ label: 'Go to upstream exon',
889
+ icon: getStreamIcon(
890
+ feature.strand,
891
+ true,
892
+ lgv.displayedRegions.at(0)?.reversed,
893
+ ),
894
+ onClick: () => {
895
+ lgv.navTo(navToFeatureCenter(exon, 0.1, lgv.totalBp))
896
+ selectFeatureAndOpenWidget(display, exon)
897
+ },
898
+ })
899
+ }
900
+ if (adjacentExons.downstream) {
901
+ const exon = adjacentExons.downstream
902
+ contextMenuItemsForFeature.push({
903
+ label: 'Go to downstream exon',
904
+ icon: getStreamIcon(
905
+ feature.strand,
906
+ false,
907
+ lgv.displayedRegions.at(0)?.reversed,
908
+ ),
909
+ onClick: () => {
910
+ lgv.navTo(navToFeatureCenter(exon, 0.1, lgv.totalBp))
911
+ selectFeatureAndOpenWidget(display, exon)
912
+ },
913
+ })
914
+ }
863
915
  contextMenuItemsForFeature.push(
864
916
  {
865
917
  label: 'Merge exons',
@@ -874,7 +926,7 @@ function getContextMenuItems(
874
926
  doneCallback()
875
927
  },
876
928
  changeManager,
877
- sourceFeature: relatedFeature,
929
+ sourceFeature: feature,
878
930
  sourceAssemblyId: currentAssemblyId,
879
931
  selectedFeature,
880
932
  setSelectedFeature: (feature?: AnnotationFeature) => {
@@ -898,7 +950,7 @@ function getContextMenuItems(
898
950
  doneCallback()
899
951
  },
900
952
  changeManager,
901
- sourceFeature: relatedFeature,
953
+ sourceFeature: feature,
902
954
  sourceAssemblyId: currentAssemblyId,
903
955
  selectedFeature,
904
956
  setSelectedFeature: (feature?: AnnotationFeature) => {
@@ -911,7 +963,7 @@ function getContextMenuItems(
911
963
  },
912
964
  )
913
965
  }
914
- if (featureTypeOntology.isTypeOf(relatedFeature.type, 'gene')) {
966
+ if (featureTypeOntology.isTypeOf(feature.type, 'gene')) {
915
967
  contextMenuItemsForFeature.push({
916
968
  label: 'Filter alternate transcripts',
917
969
  onClick: () => {
@@ -922,7 +974,7 @@ function getContextMenuItems(
922
974
  handleClose: () => {
923
975
  doneCallback()
924
976
  },
925
- sourceFeature: relatedFeature,
977
+ sourceFeature: feature,
926
978
  filteredTranscripts: getSnapshot(filteredTranscripts),
927
979
  onUpdate: (forms: string[]) => {
928
980
  display.updateFilteredTranscripts(forms)
@@ -934,7 +986,7 @@ function getContextMenuItems(
934
986
  })
935
987
  }
936
988
  menuItems.push({
937
- label: relatedFeature.type,
989
+ label: feature.type,
938
990
  subMenu: contextMenuItemsForFeature,
939
991
  })
940
992
  }
@@ -39,6 +39,8 @@ export function baseModelFactory(
39
39
  graphical: true,
40
40
  table: false,
41
41
  showFeatureLabels: true,
42
+ showStartCodons: false,
43
+ showStopCodons: true,
42
44
  showCheckResults: true,
43
45
  zoomThreshold: 200,
44
46
  heightPreConfig: types.maybe(
@@ -179,6 +181,12 @@ export function baseModelFactory(
179
181
  toggleShowFeatureLabels() {
180
182
  self.showFeatureLabels = !self.showFeatureLabels
181
183
  },
184
+ toggleShowStartCodons() {
185
+ self.showStartCodons = !self.showStartCodons
186
+ },
187
+ toggleShowStopCodons() {
188
+ self.showStopCodons = !self.showStopCodons
189
+ },
182
190
  toggleShowCheckResults() {
183
191
  self.showCheckResults = !self.showCheckResults
184
192
  },
@@ -193,7 +201,14 @@ export function baseModelFactory(
193
201
  const { filteredFeatureTypes, trackMenuItems: superTrackMenuItems } = self
194
202
  return {
195
203
  trackMenuItems() {
196
- const { graphical, table, showFeatureLabels, showCheckResults } = self
204
+ const {
205
+ graphical,
206
+ table,
207
+ showFeatureLabels,
208
+ showStartCodons,
209
+ showStopCodons,
210
+ showCheckResults,
211
+ } = self
197
212
  return [
198
213
  ...superTrackMenuItems(),
199
214
  {
@@ -232,6 +247,22 @@ export function baseModelFactory(
232
247
  self.toggleShowFeatureLabels()
233
248
  },
234
249
  },
250
+ {
251
+ label: 'Show start codons',
252
+ type: 'checkbox',
253
+ checked: showStartCodons,
254
+ onClick: () => {
255
+ self.toggleShowStartCodons()
256
+ },
257
+ },
258
+ {
259
+ label: 'Show stop codons',
260
+ type: 'checkbox',
261
+ checked: showStopCodons,
262
+ onClick: () => {
263
+ self.toggleShowStopCodons()
264
+ },
265
+ },
235
266
  {
236
267
  label: 'Check Results',
237
268
  type: 'checkbox',
@@ -326,7 +357,7 @@ export function baseModelFactory(
326
357
  void (
327
358
  self.session as unknown as ApolloSessionModel
328
359
  ).apolloDataStore.loadFeatures(self.regions)
329
- if (self.lgv.bpPerPx <= 3) {
360
+ if (self.lgv.bpPerPx <= self.zoomThreshold) {
330
361
  void (
331
362
  self.session as unknown as ApolloSessionModel
332
363
  ).apolloDataStore.loadRefSeq(self.regions)
@@ -1,15 +1,62 @@
1
1
  /* eslint-disable @typescript-eslint/no-unnecessary-condition */
2
2
  import type PluginManager from '@jbrowse/core/PluginManager'
3
3
  import { type AnyConfigurationSchemaType } from '@jbrowse/core/configuration/configurationSchema'
4
- import { doesIntersect2 } from '@jbrowse/core/util'
4
+ import {
5
+ defaultCodonTable,
6
+ doesIntersect2,
7
+ getFrame,
8
+ revcom,
9
+ } from '@jbrowse/core/util'
5
10
  import { type Theme, createTheme } from '@mui/material'
6
11
  import { autorun } from 'mobx'
7
12
  import { type Instance, addDisposer, types } from 'mobx-state-tree'
8
13
 
9
14
  import { type ApolloSessionModel } from '../../session'
15
+ import { codonColorCode } from '../../util/displayUtils'
10
16
 
11
17
  import { layoutsModelFactory } from './layouts'
12
18
 
19
+ function drawCodon(
20
+ ctx: CanvasRenderingContext2D,
21
+ codon: string,
22
+ leftPx: number,
23
+ index: number,
24
+ theme: Theme,
25
+ highContrast: boolean,
26
+ bpPerPx: number,
27
+ bp: number,
28
+ rowHeight: number,
29
+ showFeatureLabels: boolean,
30
+ showStartCodons: boolean,
31
+ showStopCodons: boolean,
32
+ ) {
33
+ const frameOffsets = (
34
+ showFeatureLabels ? [0, 4, 2, 0, 14, 12, 10] : [0, 2, 1, 0, 7, 6, 5]
35
+ ).map((b) => b * rowHeight)
36
+ const strands = [-1, 1] as const
37
+ for (const strand of strands) {
38
+ const frame = getFrame(bp, bp + 3, strand, 0)
39
+ const top = frameOffsets.at(frame)
40
+ if (top === undefined) {
41
+ continue
42
+ }
43
+ const left = Math.round(leftPx + index / bpPerPx)
44
+ const width = Math.round(3 / bpPerPx) === 0 ? 1 : Math.round(3 / bpPerPx)
45
+ const codonCode = strand === 1 ? codon : revcom(codon)
46
+ const aminoAcidCode =
47
+ defaultCodonTable[codonCode as keyof typeof defaultCodonTable]
48
+ const fillColor = codonColorCode(aminoAcidCode, theme, highContrast)
49
+ if (
50
+ fillColor &&
51
+ ((showStopCodons && aminoAcidCode == '*') ||
52
+ (showStartCodons && aminoAcidCode != '*'))
53
+ ) {
54
+ ctx.fillStyle = fillColor
55
+ ctx.fillRect(left, top, width, rowHeight)
56
+ }
57
+ }
58
+ }
59
+
13
60
  export function renderingModelFactory(
14
61
  pluginManager: PluginManager,
15
62
  configSchema: AnyConfigurationSchemaType,
@@ -135,17 +182,29 @@ export function renderingModelFactory(
135
182
  self,
136
183
  autorun(
137
184
  () => {
138
- const { canvas, featureLayouts, featuresHeight, lgv } = self
185
+ const {
186
+ apolloRowHeight,
187
+ canvas,
188
+ featureLayouts,
189
+ featuresHeight,
190
+ lgv,
191
+ session,
192
+ theme,
193
+ showFeatureLabels,
194
+ showStartCodons,
195
+ showStopCodons,
196
+ } = self
139
197
  if (!lgv.initialized || self.regionCannotBeRendered()) {
140
198
  return
141
199
  }
142
- const { displayedRegions, dynamicBlocks } = lgv
200
+ const { bpPerPx, offsetPx, displayedRegions, dynamicBlocks } = lgv
143
201
 
144
202
  const ctx = canvas?.getContext('2d')
145
203
  if (!ctx) {
146
204
  return
147
205
  }
148
206
  ctx.clearRect(0, 0, dynamicBlocks.totalWidthPx, featuresHeight)
207
+
149
208
  for (const [idx, featureLayout] of featureLayouts.entries()) {
150
209
  const displayedRegion = displayedRegions[idx]
151
210
  for (const [row, featureLayoutRow] of featureLayout.entries()) {
@@ -171,6 +230,45 @@ export function renderingModelFactory(
171
230
  }
172
231
  }
173
232
  }
233
+
234
+ if (showStartCodons || showStopCodons) {
235
+ const { apolloDataStore } = session
236
+ for (const block of dynamicBlocks.contentBlocks) {
237
+ const assembly = apolloDataStore.assemblies.get(
238
+ block.assemblyName,
239
+ )
240
+ const ref = assembly?.getByRefName(block.refName)
241
+ const roundedStart = Math.floor(block.start)
242
+ const roundedEnd = Math.ceil(block.end)
243
+ let seq = ref?.getSequence(roundedStart, roundedEnd)
244
+ if (!seq) {
245
+ break
246
+ }
247
+ seq = seq.toUpperCase()
248
+ const baseOffsetPx = (block.start - roundedStart) / bpPerPx
249
+ const seqLeftPx = Math.round(
250
+ block.offsetPx - offsetPx - baseOffsetPx,
251
+ )
252
+ for (let i = 0; i < seq.length; i++) {
253
+ const bp = roundedStart + i
254
+ const codon = seq.slice(i, i + 3)
255
+ drawCodon(
256
+ ctx,
257
+ codon,
258
+ seqLeftPx,
259
+ i,
260
+ theme,
261
+ true,
262
+ bpPerPx,
263
+ bp,
264
+ apolloRowHeight,
265
+ showFeatureLabels,
266
+ showStartCodons,
267
+ showStopCodons,
268
+ )
269
+ }
270
+ }
271
+ }
174
272
  },
175
273
  { name: 'LinearApolloSixFrameDisplayRenderFeatures' },
176
274
  ),
@@ -39,7 +39,7 @@ import {
39
39
  } from '@mui/material'
40
40
  import ObjectID from 'bson-objectid'
41
41
  import { getRoot } from 'mobx-state-tree'
42
- import React, { useEffect, useState } from 'react'
42
+ import React, { useState } from 'react'
43
43
  import { makeStyles } from 'tss-react/mui'
44
44
 
45
45
  import { type ApolloInternetAccountModel } from '../ApolloInternetAccount/model'
@@ -149,25 +149,8 @@ export function AddAssembly({
149
149
  const [fastaGziIndexUrl, setFastaGziIndexUrl] = useState<string>('')
150
150
 
151
151
  const [loading, setLoading] = useState(false)
152
- const [isGzip, setIsGzip] = useState<boolean>(false)
153
-
154
- useEffect(() => {
155
- setFastaIndexUrl(fastaUrl ? `${fastaUrl}.fai` : '')
156
- }, [fastaUrl])
157
-
158
- useEffect(() => {
159
- setFastaGziIndexUrl(fastaUrl ? `${fastaUrl}.gzi` : '')
160
- }, [fastaUrl])
161
-
162
- useEffect(() => {
163
- if (sequenceIsEditable || fileType === FileType.GFF3) {
164
- setIsGzip(
165
- fastaFile?.name.toLocaleLowerCase().endsWith('.gz') ? true : false,
166
- )
167
- } else {
168
- setIsGzip(true)
169
- }
170
- }, [fastaFile, sequenceIsEditable, fileType])
152
+ const [fastaGzipChecked, setFastaGzipChecked] = useState<boolean>(false)
153
+ const [gff3GzipChecked, setGff3GzipChecked] = useState<boolean>(false)
171
154
 
172
155
  function checkAssemblyName(assembly: string) {
173
156
  const { assemblies } = session as unknown as AbstractSessionModel
@@ -194,10 +177,13 @@ export function AddAssembly({
194
177
  const uri = url.href
195
178
  const formData = new FormData()
196
179
  let filename = file.name
180
+ const isGzip =
181
+ fileType === FileType.BGZIP_FASTA ||
182
+ (fileType === FileType.FASTA &&
183
+ (!sequenceIsEditable || fastaGzipChecked)) ||
184
+ (fileType === FileType.GFF3 && gff3GzipChecked)
197
185
 
198
- if (fileType === FileType.FAI || fileType === FileType.GZI) {
199
- filename = `${filename}.txt`
200
- } else if (isGzip && !file.name.toLocaleLowerCase().endsWith('.gz')) {
186
+ if (isGzip && !file.name.toLocaleLowerCase().endsWith('.gz')) {
201
187
  filename = `${filename}.gz`
202
188
  } else if (!isGzip && file.name.toLocaleLowerCase().endsWith('.gz')) {
203
189
  filename = `${filename}.txt`
@@ -214,7 +200,7 @@ export function AddAssembly({
214
200
  statusMessage: 'Pre-validating',
215
201
  progressPct: 0,
216
202
  cancelCallback: () => {
217
- controller.abort()
203
+ controller.abort('AddAssembly')
218
204
  jobsManager.abortJob(job.name)
219
205
  },
220
206
  }
@@ -370,11 +356,6 @@ export function AddAssembly({
370
356
  if (newExpanded) {
371
357
  setExpanded(panel)
372
358
  }
373
- if (panel === 'panelGffInput') {
374
- setIsGzip(false)
375
- } else {
376
- setIsGzip(true)
377
- }
378
359
  }
379
360
 
380
361
  return (
@@ -497,12 +478,12 @@ export function AddAssembly({
497
478
  data-testid="fasta-is-gzip-checkbox"
498
479
  control={
499
480
  <Checkbox
500
- checked={isGzip}
481
+ checked={!sequenceIsEditable || fastaGzipChecked}
501
482
  onChange={() => {
502
483
  if (sequenceIsEditable) {
503
- setIsGzip(!isGzip)
484
+ setFastaGzipChecked(!fastaGzipChecked)
504
485
  } else {
505
- setIsGzip(true)
486
+ setFastaGzipChecked(true)
506
487
  }
507
488
  }}
508
489
  disabled={!sequenceIsEditable}
@@ -533,7 +514,13 @@ export function AddAssembly({
533
514
  onChange={(
534
515
  e: React.ChangeEvent<HTMLInputElement>,
535
516
  ) => {
536
- setFastaFile(e.target.files?.item(0) ?? null)
517
+ const file = e.target.files?.item(0)
518
+ if (file) {
519
+ setFastaFile(file)
520
+ if (file.name.endsWith('.gz')) {
521
+ setFastaGzipChecked(true)
522
+ }
523
+ }
537
524
  }}
538
525
  disabled={submitted && !errorMessage}
539
526
  />
@@ -606,7 +593,10 @@ export function AddAssembly({
606
593
  onChange={(
607
594
  e: React.ChangeEvent<HTMLInputElement>,
608
595
  ) => {
609
- setFastaUrl(e.target.value)
596
+ const { value } = e.target
597
+ setFastaUrl(value)
598
+ setFastaIndexUrl(value ? `${value}.fai` : '')
599
+ setFastaGziIndexUrl(value ? `${value}.gzi` : '')
610
600
  }}
611
601
  disabled={submitted && !errorMessage}
612
602
  slotProps={{
@@ -727,8 +717,14 @@ export function AddAssembly({
727
717
  type="file"
728
718
  disabled={submitted && !errorMessage}
729
719
  onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
730
- setFastaFile(e.target.files?.item(0) ?? null)
731
- setFileType(FileType.GFF3)
720
+ const file = e.target.files?.item(0)
721
+ if (file) {
722
+ setFastaFile(file)
723
+ setFileType(FileType.GFF3)
724
+ if (file.name.endsWith('.gz')) {
725
+ setGff3GzipChecked(true)
726
+ }
727
+ }
732
728
  }}
733
729
  />
734
730
  <FormGroup style={{ display: 'grid' }}>
@@ -748,9 +744,9 @@ export function AddAssembly({
748
744
  data-testid="gff3-is-gzip-checkbox"
749
745
  control={
750
746
  <Checkbox
751
- checked={isGzip}
747
+ checked={gff3GzipChecked}
752
748
  onChange={() => {
753
- setIsGzip(!isGzip)
749
+ setGff3GzipChecked(!gff3GzipChecked)
754
750
  }}
755
751
  disabled={submitted && !errorMessage}
756
752
  />