@apollo-annotation/jbrowse-plugin-apollo 0.3.9 → 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.
- package/dist/index.esm.js +235 -120
- package/dist/index.esm.js.map +1 -1
- package/dist/jbrowse-plugin-apollo.cjs.development.js +233 -118
- package/dist/jbrowse-plugin-apollo.cjs.development.js.map +1 -1
- package/dist/jbrowse-plugin-apollo.cjs.production.min.js +1 -1
- package/dist/jbrowse-plugin-apollo.cjs.production.min.js.map +1 -1
- package/dist/jbrowse-plugin-apollo.umd.development.js +391 -195
- package/dist/jbrowse-plugin-apollo.umd.development.js.map +1 -1
- package/dist/jbrowse-plugin-apollo.umd.production.min.js +1 -1
- package/dist/jbrowse-plugin-apollo.umd.production.min.js.map +1 -1
- package/package.json +4 -4
- package/src/ApolloInternetAccount/components/AuthTypeSelector.tsx +1 -1
- package/src/ApolloInternetAccount/model.ts +6 -2
- package/src/BackendDrivers/CollaborationServerDriver.ts +11 -5
- package/src/ChangeManager.ts +19 -4
- package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +29 -9
- package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +2 -2
- package/src/LinearApolloDisplay/glyphs/util.ts +17 -0
- package/src/LinearApolloReferenceSequenceDisplay/drawSequenceOverlay.ts +18 -25
- package/src/LinearApolloReferenceSequenceDisplay/drawSequenceTrack.ts +41 -59
- package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +33 -2
- package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +101 -3
- package/src/components/AddAssembly.tsx +1 -1
- package/src/components/ImportFeatures.tsx +1 -1
- package/src/components/OntologyTermAutocomplete.tsx +2 -2
- package/src/components/OntologyTermMultiSelect.tsx +2 -2
- package/src/makeDisplayComponent.tsx +1 -1
- package/src/session/ClientDataStore.ts +1 -1
- package/src/session/session.ts +4 -0
- package/src/util/displayUtils.ts +28 -0
- 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.
|
|
3
|
+
"version": "0.3.10",
|
|
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.
|
|
52
|
-
"@apollo-annotation/mst": "^0.3.
|
|
53
|
-
"@apollo-annotation/shared": "^0.3.
|
|
51
|
+
"@apollo-annotation/common": "^0.3.10",
|
|
52
|
+
"@apollo-annotation/mst": "^0.3.10",
|
|
53
|
+
"@apollo-annotation/shared": "^0.3.10",
|
|
54
54
|
"@emotion/react": "^11.10.6",
|
|
55
55
|
"@emotion/styled": "^11.10.6",
|
|
56
56
|
"@gmod/gff": "^2.0.0",
|
|
@@ -489,8 +489,12 @@ const stateModelFactory = (configSchema: ApolloInternetAccountConfigModel) => {
|
|
|
489
489
|
return
|
|
490
490
|
}
|
|
491
491
|
if (self.role) {
|
|
492
|
-
|
|
493
|
-
|
|
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
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
}
|
package/src/ChangeManager.ts
CHANGED
|
@@ -42,21 +42,31 @@ export class ChangeManager {
|
|
|
42
42
|
const session = getSession(this.dataStore)
|
|
43
43
|
const controller = new AbortController()
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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('*')
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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.
|
|
978
|
-
label: 'Open transcript
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
ctx
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
ctx
|
|
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
|
|
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
|
-
|
|
149
|
-
ctx
|
|
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
|
-
|
|
195
|
-
|
|
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 {
|
|
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 <=
|
|
360
|
+
if (self.lgv.bpPerPx <= self.zoomThreshold) {
|
|
330
361
|
void (
|
|
331
362
|
self.session as unknown as ApolloSessionModel
|
|
332
363
|
).apolloDataStore.loadRefSeq(self.regions)
|