@apollo-annotation/jbrowse-plugin-apollo 0.3.9 → 0.3.11

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 (31) hide show
  1. package/dist/index.esm.js +235 -120
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +233 -118
  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 +562 -298
  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/components/AuthTypeSelector.tsx +1 -1
  13. package/src/ApolloInternetAccount/model.ts +6 -2
  14. package/src/BackendDrivers/CollaborationServerDriver.ts +11 -5
  15. package/src/ChangeManager.ts +19 -4
  16. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +29 -9
  17. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +2 -2
  18. package/src/LinearApolloDisplay/glyphs/util.ts +17 -0
  19. package/src/LinearApolloReferenceSequenceDisplay/drawSequenceOverlay.ts +18 -25
  20. package/src/LinearApolloReferenceSequenceDisplay/drawSequenceTrack.ts +41 -59
  21. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +33 -2
  22. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +101 -3
  23. package/src/components/AddAssembly.tsx +1 -1
  24. package/src/components/ImportFeatures.tsx +1 -1
  25. package/src/components/OntologyTermAutocomplete.tsx +2 -2
  26. package/src/components/OntologyTermMultiSelect.tsx +2 -2
  27. package/src/makeDisplayComponent.tsx +1 -1
  28. package/src/session/ClientDataStore.ts +1 -1
  29. package/src/session/session.ts +4 -0
  30. package/src/util/displayUtils.ts +28 -0
  31. package/src/util/glyphUtils.ts +18 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apollo-annotation/jbrowse-plugin-apollo",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
4
4
  "description": "Apollo plugin for JBrowse 2",
5
5
  "keywords": [
6
6
  "jbrowse",
@@ -48,9 +48,9 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "@apollo-annotation/common": "^0.3.9",
52
- "@apollo-annotation/mst": "^0.3.9",
53
- "@apollo-annotation/shared": "^0.3.9",
51
+ "@apollo-annotation/common": "^0.3.11",
52
+ "@apollo-annotation/mst": "^0.3.11",
53
+ "@apollo-annotation/shared": "^0.3.11",
54
54
  "@emotion/react": "^11.10.6",
55
55
  "@emotion/styled": "^11.10.6",
56
56
  "@gmod/gff": "^2.0.0",
@@ -57,7 +57,7 @@ export const AuthTypeSelector = ({
57
57
  }
58
58
  })
59
59
  return () => {
60
- controller.abort()
60
+ controller.abort('AuthTypeSelector')
61
61
  }
62
62
  }, [baseURL])
63
63
 
@@ -489,8 +489,12 @@ const stateModelFactory = (configSchema: ApolloInternetAccountConfigModel) => {
489
489
  return
490
490
  }
491
491
  if (self.role) {
492
- await self.initialize(self.role)
493
- reaction.dispose()
492
+ try {
493
+ await self.initialize(self.role)
494
+ reaction.dispose()
495
+ } catch {
496
+ // if initialize fails, do nothing so the autorun runs again
497
+ }
494
498
  }
495
499
  },
496
500
  { name: 'ApolloInternetAccount' },
@@ -18,6 +18,7 @@ import {
18
18
  import {
19
19
  type ChangeMessage,
20
20
  ValidationResultSet,
21
+ makeUserSessionId,
21
22
  } from '@apollo-annotation/shared'
22
23
  import { getConf } from '@jbrowse/core/configuration'
23
24
  import { type BaseInternetAccountModel } from '@jbrowse/core/pluggableElementTypes'
@@ -154,6 +155,10 @@ export class CollaborationServerDriver extends BackendDriver {
154
155
  ) {
155
156
  const { socket } = internetAccount
156
157
  const token = internetAccount.retrieveToken()
158
+ if (!token) {
159
+ return
160
+ }
161
+ const localSessionId = makeUserSessionId(token)
157
162
  const channel = `${assembly}-${refSeq}`
158
163
  const changeManager = new ChangeManager(this.clientStore)
159
164
 
@@ -163,11 +168,12 @@ export class CollaborationServerDriver extends BackendDriver {
163
168
  internetAccount.setLastChangeSequenceNumber(
164
169
  Number(message.changeSequence),
165
170
  )
166
- if (message.userSessionId !== token && message.channel === channel) {
167
- const change = Change.fromJSON(message.changeInfo)
168
- if (isFeatureChange(change) && this.haveDataForChange(change)) {
169
- await changeManager.submit(change, { submitToBackend: false })
170
- }
171
+ if (message.userSessionId === localSessionId) {
172
+ return // we did this change, no need to apply it again
173
+ }
174
+ const change = Change.fromJSON(message.changeInfo)
175
+ if (isFeatureChange(change) && this.haveDataForChange(change)) {
176
+ await changeManager.submit(change, { submitToBackend: false })
171
177
  }
172
178
  })
173
179
  }
@@ -42,21 +42,31 @@ export class ChangeManager {
42
42
  const session = getSession(this.dataStore)
43
43
  const controller = new AbortController()
44
44
 
45
- const { jobsManager, isLocked } = getSession(
46
- this.dataStore,
47
- ) as unknown as ApolloSessionModel
45
+ // eslint-disable-next-line @typescript-eslint/unbound-method
46
+ const { jobsManager, isLocked, changeInProgress, setChangeInProgress } =
47
+ getSession(this.dataStore) as unknown as ApolloSessionModel
48
48
 
49
49
  if (isLocked) {
50
50
  session.notify('Cannot submit changes in locked mode')
51
+ setChangeInProgress(false)
51
52
  return
52
53
  }
53
54
 
55
+ if (changeInProgress) {
56
+ session.notify(
57
+ 'Could not submit change, there is another change still in progress',
58
+ )
59
+ return
60
+ }
61
+
62
+ setChangeInProgress(true)
63
+
54
64
  const job = {
55
65
  name: change.typeName,
56
66
  statusMessage: 'Pre-validating',
57
67
  progressPct: 0,
58
68
  cancelCallback: () => {
59
- controller.abort()
69
+ controller.abort('ChangeManager')
60
70
  },
61
71
  }
62
72
 
@@ -71,6 +81,7 @@ export class ChangeManager {
71
81
  jobsManager.abortJob(job.name, msg)
72
82
  }
73
83
  session.notify(msg, 'error')
84
+ setChangeInProgress(false)
74
85
  return
75
86
  }
76
87
 
@@ -86,6 +97,7 @@ export class ChangeManager {
86
97
  `Error encountered in client: ${String(error)}. Data may be out of sync, please refresh the page`,
87
98
  'error',
88
99
  )
100
+ setChangeInProgress(false)
89
101
  return
90
102
  }
91
103
 
@@ -120,6 +132,7 @@ export class ChangeManager {
120
132
  console.error(error)
121
133
  session.notify(String(error), 'error')
122
134
  await this.undo(change, false)
135
+ setChangeInProgress(false)
123
136
  return
124
137
  }
125
138
  if (!backendResult.ok) {
@@ -129,6 +142,7 @@ export class ChangeManager {
129
142
  }
130
143
  session.notify(msg, 'error')
131
144
  await this.undo(change, false)
145
+ setChangeInProgress(false)
132
146
  return
133
147
  }
134
148
  if (change.notification) {
@@ -143,6 +157,7 @@ export class ChangeManager {
143
157
  if (updateJobsManager) {
144
158
  jobsManager.done(job)
145
159
  }
160
+ setChangeInProgress(false)
146
161
  }
147
162
 
148
163
  async undo(change: Change, submitToBackend = true) {
@@ -113,6 +113,7 @@ export const TranscriptWidgetEditLocation = observer(
113
113
  const refData = currentAssembly?.getByRefName(refName)
114
114
  const { changeManager } = session.apolloDataStore
115
115
  const seqRef = useRef<HTMLDivElement>(null)
116
+ const { changeInProgress } = session
116
117
 
117
118
  if (!refData) {
118
119
  return null
@@ -724,12 +725,15 @@ export const TranscriptWidgetEditLocation = observer(
724
725
  <Typography
725
726
  component={'span'}
726
727
  style={{
727
- backgroundColor: 'yellow',
728
+ backgroundColor: changeInProgress ? 'lightgray' : 'yellow',
728
729
  cursor: 'pointer',
729
730
  border: '1px solid black',
730
731
  }}
731
732
  key={codonGenomicPos}
732
733
  onClick={() => {
734
+ if (changeInProgress) {
735
+ return
736
+ }
733
737
  // NOTE: codonGenomicPos is important here for calculating the genomic location
734
738
  // of the start codon. We are using the codonGenomicPos as the key in the typography
735
739
  // elements to maintain the genomic postion of the codon start
@@ -848,7 +852,7 @@ export const TranscriptWidgetEditLocation = observer(
848
852
 
849
853
  // Trim any sequence before first start codon and after stop codon
850
854
  const startCodonIndex = translationSequence.indexOf('M')
851
- const stopCodonIndex = translationSequence.indexOf('*') + 1
855
+ const stopCodonIndex = translationSequence.indexOf('*')
852
856
 
853
857
  const startCodonPos =
854
858
  translSeqCodonStartGenomicPosArr[startCodonIndex].codonGenomicPos
@@ -861,7 +865,7 @@ export const TranscriptWidgetEditLocation = observer(
861
865
  const startCodonGenomicLoc = getCodonGenomicLocation(
862
866
  startCodonPos as unknown as number,
863
867
  )
864
- const stopCodonGenomicLoc = getCodonGenomicLocation(
868
+ let stopCodonGenomicLoc = getCodonGenomicLocation(
865
869
  stopCodonPos as unknown as number,
866
870
  )
867
871
 
@@ -874,6 +878,7 @@ export const TranscriptWidgetEditLocation = observer(
874
878
  return
875
879
  }
876
880
  let promise
881
+ stopCodonGenomicLoc += 3 // move to end of stop codon
877
882
  if (startCodonGenomicLoc !== cdsMin) {
878
883
  promise = new Promise((resolve) => {
879
884
  updateCDSLocation(
@@ -909,6 +914,7 @@ export const TranscriptWidgetEditLocation = observer(
909
914
  return
910
915
  }
911
916
  let promise
917
+ stopCodonGenomicLoc -= 3 // move to end of stop codon
912
918
  if (startCodonGenomicLoc !== cdsMax) {
913
919
  promise = new Promise((resolve) => {
914
920
  updateCDSLocation(
@@ -978,16 +984,22 @@ export const TranscriptWidgetEditLocation = observer(
978
984
  }}
979
985
  >
980
986
  <Tooltip title="Copy">
981
- <ContentCopyIcon
982
- style={{ fontSize: 15, cursor: 'pointer' }}
987
+ <button
983
988
  onClick={onCopyClick}
984
- />
989
+ style={{ border: 'none', background: 'none', padding: 0 }}
990
+ disabled={changeInProgress}
991
+ >
992
+ <ContentCopyIcon style={{ fontSize: 15 }} />
993
+ </button>
985
994
  </Tooltip>
986
995
  <Tooltip title="Trim">
987
- <ContentCutIcon
988
- style={{ fontSize: 15, cursor: 'pointer' }}
996
+ <button
989
997
  onClick={trimTranslationSequence}
990
- />
998
+ style={{ border: 'none', background: 'none', padding: 0 }}
999
+ disabled={changeInProgress}
1000
+ >
1001
+ <ContentCutIcon style={{ fontSize: 15 }} />
1002
+ </button>
991
1003
  </Tooltip>
992
1004
  </div>
993
1005
  </AccordionDetails>
@@ -1014,6 +1026,7 @@ export const TranscriptWidgetEditLocation = observer(
1014
1026
  )
1015
1027
  }}
1016
1028
  style={{ border: '1px solid black', borderRadius: 5 }}
1029
+ disabled={changeInProgress}
1017
1030
  />
1018
1031
  </Grid>
1019
1032
  ) : (
@@ -1031,6 +1044,7 @@ export const TranscriptWidgetEditLocation = observer(
1031
1044
  )
1032
1045
  }}
1033
1046
  style={{ border: '1px solid black', borderRadius: 5 }}
1047
+ disabled={changeInProgress}
1034
1048
  />
1035
1049
  </Grid>
1036
1050
  )}
@@ -1052,6 +1066,7 @@ export const TranscriptWidgetEditLocation = observer(
1052
1066
  )
1053
1067
  }}
1054
1068
  style={{ border: '1px solid black', borderRadius: 5 }}
1069
+ disabled={changeInProgress}
1055
1070
  />
1056
1071
  </Grid>
1057
1072
  ) : (
@@ -1069,6 +1084,7 @@ export const TranscriptWidgetEditLocation = observer(
1069
1084
  )
1070
1085
  }}
1071
1086
  style={{ border: '1px solid black', borderRadius: 5 }}
1087
+ disabled={changeInProgress}
1072
1088
  />
1073
1089
  </Grid>
1074
1090
  )}
@@ -1113,6 +1129,7 @@ export const TranscriptWidgetEditLocation = observer(
1113
1129
  true,
1114
1130
  )
1115
1131
  }}
1132
+ disabled={changeInProgress}
1116
1133
  />
1117
1134
  </Grid>
1118
1135
  ) : (
@@ -1129,6 +1146,7 @@ export const TranscriptWidgetEditLocation = observer(
1129
1146
  false,
1130
1147
  )
1131
1148
  }}
1149
+ disabled={changeInProgress}
1132
1150
  />
1133
1151
  </Grid>
1134
1152
  )}
@@ -1149,6 +1167,7 @@ export const TranscriptWidgetEditLocation = observer(
1149
1167
  false,
1150
1168
  )
1151
1169
  }}
1170
+ disabled={changeInProgress}
1152
1171
  />
1153
1172
  </Grid>
1154
1173
  ) : (
@@ -1165,6 +1184,7 @@ export const TranscriptWidgetEditLocation = observer(
1165
1184
  true,
1166
1185
  )
1167
1186
  }}
1187
+ disabled={changeInProgress}
1168
1188
  />
1169
1189
  </Grid>
1170
1190
  )}
@@ -974,8 +974,8 @@ function getContextMenuItems(
974
974
  },
975
975
  })
976
976
  if (isSessionModelWithWidgets(session)) {
977
- contextMenuItemsForFeature.push({
978
- label: 'Open transcript details',
977
+ contextMenuItemsForFeature.splice(1, 0, {
978
+ label: 'Open transcript editor',
979
979
  onClick: () => {
980
980
  const apolloTranscriptWidget = session.addWidget(
981
981
  'ApolloTranscriptDetails',
@@ -17,3 +17,20 @@ export function getLeftPx(
17
17
  featureLeftBpDistanceFromBlockLeftBp / bpPerPx
18
18
  return blockLeftPx + featureLeftPxDistanceFromBlockLeftPx
19
19
  }
20
+
21
+ /**
22
+ * Perform a canvas strokeRect, but have the stroke be contained within the
23
+ * given rect instead of centered on it.
24
+ */
25
+ export function strokeRectInner(
26
+ ctx: CanvasRenderingContext2D,
27
+ left: number,
28
+ top: number,
29
+ width: number,
30
+ height: number,
31
+ color: string,
32
+ ) {
33
+ ctx.strokeStyle = color
34
+ ctx.lineWidth = 1
35
+ ctx.strokeRect(left + 0.5, top + 0.5, width - 1, height - 1)
36
+ }
@@ -8,35 +8,28 @@ import { type ApolloSessionModel, type HoveredFeature } from '../session'
8
8
  function getSeqRow(
9
9
  strand: 1 | -1 | undefined,
10
10
  bpPerPx: number,
11
+ reversed?: boolean,
11
12
  ): number | undefined {
12
13
  if (bpPerPx > 1 || strand === undefined) {
13
14
  return
14
15
  }
16
+ if (reversed) {
17
+ return strand === 1 ? 4 : 3
18
+ }
15
19
  return strand === 1 ? 3 : 4
16
20
  }
17
21
 
18
- function getTranslationRow(frame: Frame, bpPerPx: number) {
19
- const offset = bpPerPx <= 1 ? 2 : 0
20
- switch (frame) {
21
- case 3: {
22
- return 0
23
- }
24
- case 2: {
25
- return 1
26
- }
27
- case 1: {
28
- return 2
29
- }
30
- case -1: {
31
- return 3 + offset
32
- }
33
- case -2: {
34
- return 4 + offset
35
- }
36
- case -3: {
37
- return 5 + offset
38
- }
22
+ function getTranslationRow(frame: Frame, bpPerPx: number, reversed?: boolean) {
23
+ const frameRows = bpPerPx <= 1 ? [2, 1, 0, 7, 6, 5] : [2, 1, 0, 5, 4, 3]
24
+ if (reversed) {
25
+ frameRows.reverse()
26
+ }
27
+ frameRows.unshift(0)
28
+ const row = frameRows.at(frame)
29
+ if (row === undefined) {
30
+ throw new Error('could not find row')
39
31
  }
32
+ return row
40
33
  }
41
34
 
42
35
  function getLeftPx(
@@ -84,7 +77,7 @@ function drawHighlight(
84
77
  theme: Theme,
85
78
  selected = false,
86
79
  ) {
87
- const row = getSeqRow(feature.strand, bpPerPx)
80
+ const row = getSeqRow(feature.strand, bpPerPx, block.reversed)
88
81
  if (!row) {
89
82
  return
90
83
  }
@@ -118,7 +111,7 @@ function drawCDSHighlight(
118
111
  }
119
112
  for (const loc of cdsLocs) {
120
113
  const frame = getFrame(loc.min, loc.max, feature.strand ?? 1, loc.phase)
121
- const row = getTranslationRow(frame, bpPerPx)
114
+ const row = getTranslationRow(frame, bpPerPx, block.reversed)
122
115
  const left = getLeftPx(loc, bpPerPx, offsetPx, block)
123
116
  const top = row * rowHeight
124
117
  const width = (loc.max - loc.min) / bpPerPx
@@ -161,7 +154,7 @@ export function drawSequenceOverlay(
161
154
  rowHeight,
162
155
  block,
163
156
  theme,
164
- true,
157
+ feature._id === selectedFeature?._id,
165
158
  )
166
159
  } else {
167
160
  drawHighlight(
@@ -172,7 +165,7 @@ export function drawSequenceOverlay(
172
165
  rowHeight,
173
166
  block,
174
167
  theme,
175
- true,
168
+ feature._id === selectedFeature?._id,
176
169
  )
177
170
  }
178
171
  }
@@ -2,30 +2,9 @@ import { defaultCodonTable, getFrame, revcom } from '@jbrowse/core/util'
2
2
  import { type BlockSet } from '@jbrowse/core/util/blockTypes'
3
3
  import { type Theme } from '@mui/material'
4
4
 
5
+ import { strokeRectInner } from '../LinearApolloDisplay/glyphs/util'
5
6
  import { type ApolloSessionModel } from '../session'
6
-
7
- function colorCode(letter: string, theme: Theme) {
8
- const letterUpper = letter.toUpperCase()
9
- if (
10
- letterUpper === 'A' ||
11
- letterUpper === 'C' ||
12
- letterUpper === 'G' ||
13
- letterUpper === 'T'
14
- ) {
15
- return theme.palette.bases[letterUpper].main.toString()
16
- }
17
- return 'lightgray'
18
- }
19
-
20
- function codonColorCode(letter: string, theme: Theme, highContrast?: boolean) {
21
- if (letter === 'M') {
22
- return theme.palette.startCodon
23
- }
24
- if (letter === '*') {
25
- return highContrast ? theme.palette.text.primary : theme.palette.stopCodon
26
- }
27
- return
28
- }
7
+ import { codonColorCode, colorCode } from '../util/displayUtils'
29
8
 
30
9
  function drawLetter(
31
10
  seqTrackctx: CanvasRenderingContext2D,
@@ -38,43 +17,37 @@ function drawLetter(
38
17
  seqTrackctx.fillStyle = '#000'
39
18
  seqTrackctx.font = `${fontSize}px`
40
19
  const textWidth = seqTrackctx.measureText(letter).width
41
- const textX = left + (width - textWidth) / 2
20
+ const textX = Math.round(left + (width - textWidth) / 2)
42
21
  seqTrackctx.fillText(letter, textX, top + 10)
43
22
  }
44
23
 
45
24
  function drawTranslationFrameBackgrounds(
46
- canvas: HTMLCanvasElement,
47
25
  ctx: CanvasRenderingContext2D,
48
26
  bpPerPx: number,
49
27
  theme: Theme,
50
- dynamicBlocks: BlockSet,
51
28
  highContrast: boolean,
29
+ left: number,
30
+ width: number,
52
31
  sequenceRowHeight: number,
32
+ reversed?: boolean,
53
33
  ) {
54
34
  const frames =
55
35
  bpPerPx <= 1 ? [3, 2, 1, 0, 0, -1, -2, -3] : [3, 2, 1, -1, -2, -3]
36
+ if (reversed) {
37
+ frames.reverse()
38
+ }
56
39
  for (const [idx, frame] of frames.entries()) {
57
40
  const frameColor = theme.palette.framesCDS.at(frame)?.main
58
41
  if (!frameColor) {
59
42
  continue
60
43
  }
61
44
  const top = idx * sequenceRowHeight
62
- const { offsetPx } = dynamicBlocks
63
- const left = Math.max(0, -offsetPx)
64
- const width = dynamicBlocks.totalWidthPx
65
45
  ctx.fillStyle = highContrast ? theme.palette.background.default : frameColor
66
46
  ctx.fillRect(left, top, width, sequenceRowHeight)
67
47
  if (highContrast) {
68
48
  // eslint-disable-next-line prefer-destructuring
69
- ctx.strokeStyle = theme.palette.grey[200]
70
- ctx.strokeRect(left, top, width, sequenceRowHeight)
71
- }
72
- }
73
- // allows inter-region padding lines to show through
74
- for (const block of dynamicBlocks.getBlocks()) {
75
- if (block.type === 'InterRegionPaddingBlock') {
76
- const left = block.offsetPx - dynamicBlocks.offsetPx
77
- ctx.clearRect(left, 0, block.widthPx, canvas.height)
49
+ const strokeStyle = theme.palette.grey[200]
50
+ strokeRectInner(ctx, left, top, width, sequenceRowHeight, strokeStyle)
78
51
  }
79
52
  }
80
53
  }
@@ -88,11 +61,12 @@ function drawBase(
88
61
  rowHeight: number,
89
62
  theme: Theme,
90
63
  ) {
91
- const width = 1 / bpPerPx
92
- if (width < 1) {
64
+ if (1 / bpPerPx < 1) {
93
65
  return
94
66
  }
95
- const left = leftPx + index / bpPerPx
67
+ const left = Math.round(leftPx + index / bpPerPx)
68
+ const nextLeft = Math.round(leftPx + (index + 1) / bpPerPx)
69
+ const width = nextLeft - left
96
70
  const strands = [-1, 1] as const
97
71
  for (const strand of strands) {
98
72
  const top = (strand === 1 ? 3 : 4) * rowHeight
@@ -100,8 +74,8 @@ function drawBase(
100
74
  ctx.fillStyle = colorCode(baseCode, theme)
101
75
  ctx.fillRect(left, top, width, rowHeight)
102
76
  if (1 / bpPerPx >= 12) {
103
- ctx.strokeStyle = theme.palette.text.disabled
104
- ctx.strokeRect(left, top, width, rowHeight)
77
+ const strokeStyle = theme.palette.text.disabled
78
+ strokeRectInner(ctx, left, top, width, rowHeight, strokeStyle)
105
79
  drawLetter(ctx, left, top, width, baseCode)
106
80
  }
107
81
  }
@@ -131,7 +105,8 @@ function drawCodon(
131
105
  continue
132
106
  }
133
107
  const left = Math.round(leftPx + index / bpPerPx)
134
- const width = Math.round(3 / bpPerPx)
108
+ const nextLeft = Math.round(leftPx + (index + 3) / bpPerPx)
109
+ const width = nextLeft - left
135
110
  const codonCode = strand === 1 ? codon : revcom(codon)
136
111
  const aminoAcidCode =
137
112
  defaultCodonTable[codonCode as keyof typeof defaultCodonTable]
@@ -145,8 +120,8 @@ function drawCodon(
145
120
  ctx.fillRect(left, top, width, rowHeight)
146
121
  }
147
122
  if (1 / bpPerPx >= 4) {
148
- ctx.strokeStyle = theme.palette.text.disabled
149
- ctx.strokeRect(left, top, width, rowHeight)
123
+ const strokeStyle = theme.palette.text.disabled
124
+ strokeRectInner(ctx, left, top, width, rowHeight, strokeStyle)
150
125
  drawLetter(ctx, left, top, width, aminoAcidCode)
151
126
  }
152
127
  }
@@ -170,18 +145,20 @@ export function drawSequenceTrack(
170
145
  }
171
146
  ctx.clearRect(0, 0, canvas.width, canvas.height)
172
147
 
173
- drawTranslationFrameBackgrounds(
174
- canvas,
175
- ctx,
176
- bpPerPx,
177
- theme,
178
- dynamicBlocks,
179
- highContrast,
180
- sequenceRowHeight,
181
- )
182
-
183
148
  const { apolloDataStore } = session
184
149
  for (const block of dynamicBlocks.contentBlocks) {
150
+ const totalOffsetPx = block.offsetPx - offsetPx
151
+
152
+ drawTranslationFrameBackgrounds(
153
+ ctx,
154
+ bpPerPx,
155
+ theme,
156
+ highContrast,
157
+ totalOffsetPx,
158
+ block.widthPx,
159
+ sequenceRowHeight,
160
+ block.reversed,
161
+ )
185
162
  const assembly = apolloDataStore.assemblies.get(block.assemblyName)
186
163
  const ref = assembly?.getByRefName(block.refName)
187
164
  const roundedStart = Math.floor(block.start)
@@ -191,10 +168,15 @@ export function drawSequenceTrack(
191
168
  return
192
169
  }
193
170
  seq = seq.toUpperCase()
194
- const baseOffsetPx = (block.start - roundedStart) / bpPerPx
195
- const seqLeftPx = Math.round(block.offsetPx - offsetPx - baseOffsetPx)
171
+ if (block.reversed) {
172
+ seq = revcom(seq)
173
+ }
174
+ const baseOffsetPx =
175
+ (block.reversed ? roundedEnd - block.end : block.start - roundedStart) /
176
+ bpPerPx
177
+ const seqLeftPx = totalOffsetPx - baseOffsetPx
196
178
  for (let i = 0; i < seq.length; i++) {
197
- const bp = roundedStart + i
179
+ const bp = block.reversed ? roundedEnd - i : roundedStart + i
198
180
  const codon = seq.slice(i, i + 3)
199
181
  drawBase(ctx, seq[i], i, seqLeftPx, bpPerPx, sequenceRowHeight, theme)
200
182
  if (codon.length !== 3) {
@@ -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)