jbrowse-plugin-graphgenomeviewer 7.4.3 → 7.4.4
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/README.md +10 -7
- package/dist/chunks/{AddTrackWorkflow-LK25EC3H.js → AddTrackWorkflow-NLSZYK56.js} +2 -2
- package/dist/chunks/{GbzBaseSyntenyAdapter-EKLUEECW.js → GbzBaseSyntenyAdapter-A5MNPCKX.js} +2 -2
- package/dist/chunks/{GbzBaseSyntenyAdapter-EKLUEECW.js.map → GbzBaseSyntenyAdapter-A5MNPCKX.js.map} +1 -1
- package/dist/chunks/{GraphGenomeView-P3XMSKUQ.js → GraphGenomeView-SEAPDRXS.js} +2 -2
- package/dist/chunks/{GraphTrackSettingsDialog-UTYH6HEC.js → GraphTrackSettingsDialog-EVIIGC5G.js} +2 -2
- package/dist/chunks/{HighlightColorDialog-G6AFRYUD.js → HighlightColorDialog-QAYWLO5I.js} +2 -2
- package/dist/chunks/{LinearGraphDisplay-O4SQSXJJ.js → LinearGraphDisplay-ZC4BQ27J.js} +2 -2
- package/dist/chunks/MinigraphBubbleAdapter-3QM4XV47.js +2 -0
- package/dist/chunks/{MinigraphBubbleAdapter-3NJ2VLN2.js.map → MinigraphBubbleAdapter-3QM4XV47.js.map} +1 -1
- package/dist/chunks/{RgfaTabixAdapter-55DGDID2.js → RgfaTabixAdapter-XSOIEWBK.js} +2 -2
- package/dist/chunks/WalkFormsAdapter-FT2SW4H6.js +2 -0
- package/dist/chunks/WalkFormsAdapter-FT2SW4H6.js.map +7 -0
- package/dist/chunks/{WalkTabixSyntenyAdapter-NHNSZZBX.js → WalkTabixSyntenyAdapter-3WRVNDLB.js} +2 -2
- package/dist/chunks/{chunk-CXOMX2I6.js → chunk-2KVE7YAS.js} +1 -1
- package/dist/chunks/{chunk-CXOMX2I6.js.map → chunk-2KVE7YAS.js.map} +1 -1
- package/dist/chunks/{chunk-K4TFAHGC.js → chunk-6SWXMRAV.js} +1 -1
- package/dist/chunks/{chunk-K4TFAHGC.js.map → chunk-6SWXMRAV.js.map} +1 -1
- package/dist/chunks/{chunk-ZAYC5OMQ.js → chunk-6VNU7SD4.js} +2 -2
- package/dist/chunks/{chunk-ZAYC5OMQ.js.map → chunk-6VNU7SD4.js.map} +1 -1
- package/dist/chunks/{chunk-MCE6STZR.js → chunk-6WUWQX2P.js} +2 -2
- package/dist/chunks/{chunk-MCE6STZR.js.map → chunk-6WUWQX2P.js.map} +1 -1
- package/dist/chunks/{chunk-TGSDXZXI.js → chunk-7RJMG6I2.js} +2 -2
- package/dist/chunks/{chunk-TGSDXZXI.js.map → chunk-7RJMG6I2.js.map} +1 -1
- package/dist/chunks/chunk-FTXDTU27.js +3 -0
- package/dist/chunks/chunk-FTXDTU27.js.map +7 -0
- package/dist/chunks/chunk-G53BI7VK.js +2 -0
- package/dist/chunks/{chunk-6OXXT2YP.js.map → chunk-G53BI7VK.js.map} +4 -4
- package/dist/chunks/chunk-GRTN25B6.js +3 -0
- package/dist/chunks/chunk-GRTN25B6.js.map +7 -0
- package/dist/chunks/{chunk-OKRGO7XA.js → chunk-HCDKP3IP.js} +2 -2
- package/dist/chunks/{chunk-OKRGO7XA.js.map → chunk-HCDKP3IP.js.map} +1 -1
- package/dist/chunks/{chunk-F2DMEJEK.js → chunk-HF4H6LQI.js} +2 -2
- package/dist/chunks/{chunk-MVVXRZXV.js → chunk-JRJHXLGP.js} +2 -2
- package/dist/chunks/{chunk-MVVXRZXV.js.map → chunk-JRJHXLGP.js.map} +2 -2
- package/dist/chunks/{chunk-DUPBXBYE.js → chunk-KIYRMZWP.js} +2 -2
- package/dist/chunks/{chunk-DUPBXBYE.js.map → chunk-KIYRMZWP.js.map} +1 -1
- package/dist/chunks/{chunk-XQFBLJU5.js → chunk-LE43I4PT.js} +2 -2
- package/dist/chunks/{chunk-CNNZDE6I.js → chunk-LE7FZM6Y.js} +2 -2
- package/dist/chunks/{chunk-CNNZDE6I.js.map → chunk-LE7FZM6Y.js.map} +1 -1
- package/dist/chunks/chunk-MUONTMYZ.js +2 -0
- package/dist/chunks/chunk-MUONTMYZ.js.map +7 -0
- package/dist/chunks/{chunk-MNCPMOHJ.js → chunk-OJWBLPPH.js} +2 -2
- package/dist/chunks/{chunk-UOFWYS6T.js → chunk-P3FBI5ML.js} +1 -1
- package/dist/chunks/{chunk-UOFWYS6T.js.map → chunk-P3FBI5ML.js.map} +1 -1
- package/dist/chunks/chunk-QV7SSOMJ.js +2 -0
- package/dist/chunks/chunk-S333HOG5.js +2 -0
- package/dist/chunks/chunk-S333HOG5.js.map +7 -0
- package/dist/chunks/{chunk-QJ43NDYS.js → chunk-TYV6UXJA.js} +2 -2
- package/dist/chunks/chunk-VZQ6VOUP.js +2 -0
- package/dist/chunks/chunk-VZQ6VOUP.js.map +7 -0
- package/dist/chunks/chunk-WLRRI35A.js +3 -0
- package/dist/chunks/chunk-WLRRI35A.js.map +7 -0
- package/dist/chunks/{chunk-NPV7SRJD.js → chunk-XXBE4IFZ.js} +2 -2
- package/dist/chunks/{chunk-NPV7SRJD.js.map → chunk-XXBE4IFZ.js.map} +1 -1
- package/dist/chunks/{renderSvg-7G6NWBXF.js → renderSvg-ZIUOPWJQ.js} +2 -2
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +3 -4
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +4 -4
- package/package.json +3 -3
- package/src/GraphGenomeView/model.test.ts +2 -109
- package/src/GraphGenomeView/pane/fitViews.ts +0 -109
- package/src/GraphGenomeView/pane/graphViews.ts +0 -4
- package/src/GraphGenomeView/pane/launchActions.ts +0 -22
- package/src/GraphGenomeView/pane/paneBase.ts +0 -7
- package/src/GraphGenomeView/viewModel.ts +0 -3
- package/src/RgfaTabixAdapter/configSchema.ts +3 -3
- package/src/RgfaTabixAdapter/walkFiles.test.ts +6 -6
- package/src/WalkFormsAdapter/WalkFormsAdapter.test.ts +204 -0
- package/src/WalkFormsAdapter/WalkFormsAdapter.ts +232 -0
- package/src/WalkFormsAdapter/configSchema.ts +127 -0
- package/src/WalkFormsAdapter/formRows.test.ts +258 -0
- package/src/WalkFormsAdapter/formRows.ts +231 -0
- package/src/WalkFormsAdapter/formsMenu.test.ts +237 -0
- package/src/WalkFormsAdapter/formsMenu.ts +202 -0
- package/src/WalkFormsAdapter/index.ts +21 -0
- package/src/WalkFormsAdapter/pinned.ts +33 -0
- package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.test.ts +5 -5
- package/src/WalkTabixSyntenyAdapter/configSchema.ts +2 -2
- package/src/index.ts +4 -0
- package/src/panSNAliases/panSNAliases.test.ts +2 -2
- package/src/version.ts +1 -1
- package/dist/chunks/MinigraphBubbleAdapter-3NJ2VLN2.js +0 -2
- package/dist/chunks/chunk-4I3K4RHE.js +0 -2
- package/dist/chunks/chunk-4I3K4RHE.js.map +0 -7
- package/dist/chunks/chunk-6OXXT2YP.js +0 -2
- package/dist/chunks/chunk-NFH2U74B.js +0 -2
- package/dist/chunks/chunk-P7VMOTUM.js +0 -3
- package/dist/chunks/chunk-P7VMOTUM.js.map +0 -7
- package/dist/chunks/chunk-RU4WSKBO.js +0 -3
- package/dist/chunks/chunk-RU4WSKBO.js.map +0 -7
- package/dist/chunks/chunk-WGTLNVST.js +0 -2
- package/dist/chunks/chunk-WGTLNVST.js.map +0 -7
- package/dist/chunks/chunk-Y5DNBFDY.js +0 -3
- package/dist/chunks/chunk-Y5DNBFDY.js.map +0 -7
- /package/dist/chunks/{AddTrackWorkflow-LK25EC3H.js.map → AddTrackWorkflow-NLSZYK56.js.map} +0 -0
- /package/dist/chunks/{GraphGenomeView-P3XMSKUQ.js.map → GraphGenomeView-SEAPDRXS.js.map} +0 -0
- /package/dist/chunks/{GraphTrackSettingsDialog-UTYH6HEC.js.map → GraphTrackSettingsDialog-EVIIGC5G.js.map} +0 -0
- /package/dist/chunks/{HighlightColorDialog-G6AFRYUD.js.map → HighlightColorDialog-QAYWLO5I.js.map} +0 -0
- /package/dist/chunks/{LinearGraphDisplay-O4SQSXJJ.js.map → LinearGraphDisplay-ZC4BQ27J.js.map} +0 -0
- /package/dist/chunks/{RgfaTabixAdapter-55DGDID2.js.map → RgfaTabixAdapter-XSOIEWBK.js.map} +0 -0
- /package/dist/chunks/{WalkTabixSyntenyAdapter-NHNSZZBX.js.map → WalkTabixSyntenyAdapter-3WRVNDLB.js.map} +0 -0
- /package/dist/chunks/{chunk-F2DMEJEK.js.map → chunk-HF4H6LQI.js.map} +0 -0
- /package/dist/chunks/{chunk-XQFBLJU5.js.map → chunk-LE43I4PT.js.map} +0 -0
- /package/dist/chunks/{chunk-MNCPMOHJ.js.map → chunk-OJWBLPPH.js.map} +0 -0
- /package/dist/chunks/{chunk-NFH2U74B.js.map → chunk-QV7SSOMJ.js.map} +0 -0
- /package/dist/chunks/{chunk-QJ43NDYS.js.map → chunk-TYV6UXJA.js.map} +0 -0
- /package/dist/chunks/{renderSvg-7G6NWBXF.js.map → renderSvg-ZIUOPWJQ.js.map} +0 -0
- /package/src/RgfaTabixAdapter/test_data/{walks4_follow.GRCh38.walks.bed.gz → walks5_follow.GRCh38.walks.bed.gz} +0 -0
- /package/src/RgfaTabixAdapter/test_data/{walks4_follow.GRCh38.walks.bed.gz.tbi → walks5_follow.GRCh38.walks.bed.gz.tbi} +0 -0
- /package/src/RgfaTabixAdapter/test_data/{walks4_header.GRCh38.walks.bed.gz → walks5_header.GRCh38.walks.bed.gz} +0 -0
- /package/src/RgfaTabixAdapter/test_data/{walks4_header.GRCh38.walks.bed.gz.tbi → walks5_header.GRCh38.walks.bed.gz.tbi} +0 -0
- /package/src/RgfaTabixAdapter/test_data/{walks4_inversion.GRCh38.walks.bed.gz → walks5_inversion.GRCh38.walks.bed.gz} +0 -0
- /package/src/RgfaTabixAdapter/test_data/{walks4_inversion.GRCh38.walks.bed.gz.tbi → walks5_inversion.GRCh38.walks.bed.gz.tbi} +0 -0
- /package/src/RgfaTabixAdapter/test_data/{walks4_lanepairs.GRCh38.walks.bed.gz → walks5_lanepairs.GRCh38.walks.bed.gz} +0 -0
- /package/src/RgfaTabixAdapter/test_data/{walks4_lanepairs.GRCh38.walks.bed.gz.tbi → walks5_lanepairs.GRCh38.walks.bed.gz.tbi} +0 -0
- /package/src/RgfaTabixAdapter/test_data/{walks4_maxnode.GRCh38.walks.bed.gz → walks5_maxnode.GRCh38.walks.bed.gz} +0 -0
- /package/src/RgfaTabixAdapter/test_data/{walks4_maxnode.GRCh38.walks.bed.gz.tbi → walks5_maxnode.GRCh38.walks.bed.gz.tbi} +0 -0
- /package/src/RgfaTabixAdapter/test_data/{walks4_rejoin.GRCh38.walks.bed.gz → walks5_rejoin.GRCh38.walks.bed.gz} +0 -0
- /package/src/RgfaTabixAdapter/test_data/{walks4_rejoin.GRCh38.walks.bed.gz.tbi → walks5_rejoin.GRCh38.walks.bed.gz.tbi} +0 -0
- /package/src/RgfaTabixAdapter/test_data/{walks4_twopart.GRCh38.walks.bed.gz → walks5_twopart.GRCh38.walks.bed.gz} +0 -0
- /package/src/RgfaTabixAdapter/test_data/{walks4_twopart.GRCh38.walks.bed.gz.tbi → walks5_twopart.GRCh38.walks.bed.gz.tbi} +0 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NO_CALL_STATE,
|
|
3
|
+
RARER_STATE,
|
|
4
|
+
REFERENCE_STATE,
|
|
5
|
+
formStateAt,
|
|
6
|
+
formTag,
|
|
7
|
+
markTag,
|
|
8
|
+
} from '@jbrowse/graph-walks'
|
|
9
|
+
|
|
10
|
+
import type { Mark, MarkKind, StructuralForms } from '@jbrowse/graph-walks'
|
|
11
|
+
|
|
12
|
+
export const CONSEQUENCES = [
|
|
13
|
+
'loss',
|
|
14
|
+
'gain',
|
|
15
|
+
'shift',
|
|
16
|
+
'inversion',
|
|
17
|
+
'net',
|
|
18
|
+
] as const
|
|
19
|
+
|
|
20
|
+
export type Consequence = (typeof CONSEQUENCES)[number]
|
|
21
|
+
|
|
22
|
+
// `svType` in core's vocabulary (UNIVERSAL_FIELD_PRESETS) and `type` as a
|
|
23
|
+
// Sequence Ontology term, which VcfFeature gives `<DUP:TANDEM>` too
|
|
24
|
+
const KINDS: Record<
|
|
25
|
+
MarkKind,
|
|
26
|
+
{ svType: string; type: string; alt: string; consequence?: Consequence }
|
|
27
|
+
> = {
|
|
28
|
+
deletion: { svType: 'DEL', type: 'deletion', alt: '<DEL>' },
|
|
29
|
+
insertion: { svType: 'INS', type: 'insertion', alt: '<INS>' },
|
|
30
|
+
duplication: {
|
|
31
|
+
svType: 'DUP',
|
|
32
|
+
type: 'tandem_duplication',
|
|
33
|
+
alt: '<DUP:TANDEM>',
|
|
34
|
+
},
|
|
35
|
+
replacement: { svType: 'CPX', type: 'complex_substitution', alt: '<CPX>' },
|
|
36
|
+
inversion: {
|
|
37
|
+
svType: 'INV',
|
|
38
|
+
type: 'inversion',
|
|
39
|
+
alt: '<INV>',
|
|
40
|
+
consequence: 'inversion',
|
|
41
|
+
},
|
|
42
|
+
shift: {
|
|
43
|
+
svType: 'CPX',
|
|
44
|
+
type: 'complex_structural_alteration',
|
|
45
|
+
alt: '<CPX>',
|
|
46
|
+
consequence: 'shift',
|
|
47
|
+
},
|
|
48
|
+
net: {
|
|
49
|
+
svType: 'CPX',
|
|
50
|
+
type: 'complex_structural_alteration',
|
|
51
|
+
alt: '<CPX>',
|
|
52
|
+
consequence: 'net',
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// A lopsided replacement colours by the side graph-walks names, which its net
|
|
57
|
+
// sign agrees with; any other change by its net sign
|
|
58
|
+
const sideOf = (m: Mark) =>
|
|
59
|
+
m.lopsided ?? (m.delta < 0 ? 'deletion' : 'insertion')
|
|
60
|
+
|
|
61
|
+
const consequenceOf = (m: Mark): Consequence =>
|
|
62
|
+
KINDS[m.kind].consequence ?? (sideOf(m) === 'deletion' ? 'loss' : 'gain')
|
|
63
|
+
|
|
64
|
+
export const formName = (i: number) => `F${i + 1}`
|
|
65
|
+
|
|
66
|
+
export const OTHER_FORMS = 'other'
|
|
67
|
+
|
|
68
|
+
export const DEFAULT_MAX_FORMS = 8
|
|
69
|
+
|
|
70
|
+
function percent(n: number, of: number) {
|
|
71
|
+
const p = (100 * n) / of
|
|
72
|
+
return p > 0 && p < 1 ? '<1%' : `${Math.round(p)}%`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// The forms drawn as rows of their own, largest first; one more than
|
|
76
|
+
// `maxForms` gets its own row rather than an "other forms" row
|
|
77
|
+
const shownCount = ({ forms }: StructuralForms, maxForms: number) =>
|
|
78
|
+
forms.length > maxForms + 1 ? Math.max(0, maxForms) : forms.length
|
|
79
|
+
|
|
80
|
+
const sum = (forms: StructuralForms['forms']) =>
|
|
81
|
+
forms.reduce((n, f) => n + f.members.length, 0)
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* One row per form, largest first, labelled "66% · del 18.4kb". Past
|
|
85
|
+
* `maxForms` the rest share one empty "other forms" row. A row's `forms`
|
|
86
|
+
* counts the forms it holds, so the rows sum to the window's forms.
|
|
87
|
+
*/
|
|
88
|
+
export function formSources(
|
|
89
|
+
result: StructuralForms,
|
|
90
|
+
maxForms = DEFAULT_MAX_FORMS,
|
|
91
|
+
) {
|
|
92
|
+
const shown = shownCount(result, maxForms)
|
|
93
|
+
const rest = result.forms.slice(shown)
|
|
94
|
+
return [
|
|
95
|
+
...result.forms.slice(0, shown).map((f, i) => ({
|
|
96
|
+
name: formName(i),
|
|
97
|
+
label: `${percent(f.members.length, result.placed)} · ${formTag(f, result)}`,
|
|
98
|
+
haplotypes: f.members.length,
|
|
99
|
+
forms: 1,
|
|
100
|
+
})),
|
|
101
|
+
...(rest.length > 0
|
|
102
|
+
? [
|
|
103
|
+
{
|
|
104
|
+
name: OTHER_FORMS,
|
|
105
|
+
label: `other forms · ${percent(sum(rest), result.placed)}`,
|
|
106
|
+
haplotypes: sum(rest),
|
|
107
|
+
forms: rest.length,
|
|
108
|
+
},
|
|
109
|
+
]
|
|
110
|
+
: []),
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const LISTED_NAMES = 5
|
|
115
|
+
|
|
116
|
+
function names(haplotypes: string[]) {
|
|
117
|
+
const listed = haplotypes.slice(0, LISTED_NAMES).join(', ')
|
|
118
|
+
const more = haplotypes.length - LISTED_NAMES
|
|
119
|
+
return more > 0 ? `${listed} and ${more} more` : listed
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const haplotypeCount = (n: number) => `${n} haplotype${n === 1 ? '' : 's'}`
|
|
123
|
+
|
|
124
|
+
export function formWarnings(
|
|
125
|
+
result: StructuralForms,
|
|
126
|
+
maxForms = DEFAULT_MAX_FORMS,
|
|
127
|
+
) {
|
|
128
|
+
const { abridged, unplaced, forms } = result
|
|
129
|
+
const shown = shownCount(result, maxForms)
|
|
130
|
+
const rest = forms.slice(shown)
|
|
131
|
+
return [
|
|
132
|
+
...(rest.length > 0
|
|
133
|
+
? [
|
|
134
|
+
`${rest.length} smaller forms, ${haplotypeCount(sum(rest))}, share the empty "other forms" row; maxForms sets how many forms get a row of their own`,
|
|
135
|
+
]
|
|
136
|
+
: []),
|
|
137
|
+
...(abridged.length > 0
|
|
138
|
+
? [
|
|
139
|
+
`${haplotypeCount(abridged.length)} kept an abridged walk past the forms cut's step budget, which shows as a net change over its span: ${names(abridged)}`,
|
|
140
|
+
]
|
|
141
|
+
: []),
|
|
142
|
+
...(unplaced.length > 0
|
|
143
|
+
? [
|
|
144
|
+
`${haplotypeCount(unplaced.length)} ${unplaced.length === 1 ? 'has' : 'have'} no walk over the pinned window, so no form: ${names(unplaced)}`,
|
|
145
|
+
]
|
|
146
|
+
: []),
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Genotype `0` in every form with a row of its own
|
|
151
|
+
export function referenceGenotypes(
|
|
152
|
+
result: StructuralForms,
|
|
153
|
+
maxForms = DEFAULT_MAX_FORMS,
|
|
154
|
+
): Record<string, string> {
|
|
155
|
+
return Object.fromEntries(
|
|
156
|
+
result.forms
|
|
157
|
+
.slice(0, shownCount(result, maxForms))
|
|
158
|
+
.map((_, j) => [formName(j), '0']),
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface FormVariant {
|
|
163
|
+
id: string
|
|
164
|
+
start: number
|
|
165
|
+
end: number
|
|
166
|
+
name: string
|
|
167
|
+
description: string
|
|
168
|
+
type: string
|
|
169
|
+
svType: string
|
|
170
|
+
consequence: Consequence
|
|
171
|
+
alt: string
|
|
172
|
+
mark: Mark
|
|
173
|
+
genotypes: Record<string, string>
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// A duplication draws over the stretch it copies rather than its zero-width
|
|
177
|
+
// junction, as a VCF `<DUP>` spans its copy
|
|
178
|
+
function drawnSpan({ copied, refStart, refEnd }: Mark) {
|
|
179
|
+
const [start, end] = copied ?? [refStart, refEnd]
|
|
180
|
+
return { start, end: Math.max(end, start + 1) }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const genotypeOf = (formState: string | undefined, state: string) =>
|
|
184
|
+
formState === state
|
|
185
|
+
? '1'
|
|
186
|
+
: formState === NO_CALL_STATE || formState === RARER_STATE
|
|
187
|
+
? '.'
|
|
188
|
+
: '0'
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The marks of each changed state at a site that is not an edge, one variant
|
|
192
|
+
* each, with genotype `1` in the forms in that state, `0` in the others and
|
|
193
|
+
* `.` where a form has no call or only a rarer change. Only the forms with a
|
|
194
|
+
* row of their own get a genotype, so the "other forms" row draws empty, and
|
|
195
|
+
* a state none of them carries gives no variant.
|
|
196
|
+
*/
|
|
197
|
+
export function formVariants(
|
|
198
|
+
result: StructuralForms,
|
|
199
|
+
maxForms = DEFAULT_MAX_FORMS,
|
|
200
|
+
): FormVariant[] {
|
|
201
|
+
const { sites, forms, placed } = result
|
|
202
|
+
const shown = shownCount(result, maxForms)
|
|
203
|
+
return sites.flatMap((site, s) =>
|
|
204
|
+
site.edge
|
|
205
|
+
? []
|
|
206
|
+
: site.states.flatMap(state => {
|
|
207
|
+
if (state.key === REFERENCE_STATE) {
|
|
208
|
+
return []
|
|
209
|
+
}
|
|
210
|
+
const states = forms.map(f => formStateAt(result, f, s))
|
|
211
|
+
const genotypes = Object.fromEntries(
|
|
212
|
+
states
|
|
213
|
+
.slice(0, shown)
|
|
214
|
+
.map((f, j) => [formName(j), genotypeOf(f, state.key)]),
|
|
215
|
+
)
|
|
216
|
+
const carriers = sum(forms.filter((_, j) => states[j] === state.key))
|
|
217
|
+
return Object.values(genotypes).includes('1')
|
|
218
|
+
? state.marks.map((mark, k) => ({
|
|
219
|
+
id: `${s}:${state.key}:${k}`,
|
|
220
|
+
...drawnSpan(mark),
|
|
221
|
+
name: markTag(mark),
|
|
222
|
+
description: `${carriers} of ${haplotypeCount(placed)}`,
|
|
223
|
+
...KINDS[mark.kind],
|
|
224
|
+
consequence: consequenceOf(mark),
|
|
225
|
+
mark,
|
|
226
|
+
genotypes,
|
|
227
|
+
}))
|
|
228
|
+
: []
|
|
229
|
+
}),
|
|
230
|
+
)
|
|
231
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { types } from '@jbrowse/mobx-state-tree'
|
|
2
|
+
|
|
3
|
+
import { FLOORS, pinnedLabel, withFormsMenu } from './formsMenu.ts'
|
|
4
|
+
|
|
5
|
+
import type { MenuItem } from '@jbrowse/core/ui'
|
|
6
|
+
|
|
7
|
+
const LPA = { refName: 'chr6', start: 160_525_000, end: 160_655_000 }
|
|
8
|
+
|
|
9
|
+
const FakeDisplay = types
|
|
10
|
+
.model('FakeDisplay', { adapter: types.frozen<Record<string, unknown>>() })
|
|
11
|
+
.volatile(() => ({
|
|
12
|
+
sourcesBase: undefined as Record<string, unknown>[] | undefined,
|
|
13
|
+
}))
|
|
14
|
+
.views(self => ({
|
|
15
|
+
get parentTrack() {
|
|
16
|
+
return { configuration: { adapter: self.adapter } }
|
|
17
|
+
},
|
|
18
|
+
trackMenuItems(): MenuItem[] {
|
|
19
|
+
return [{ label: 'Core item', onClick: () => {} }]
|
|
20
|
+
},
|
|
21
|
+
}))
|
|
22
|
+
.actions(self => ({
|
|
23
|
+
setRows(rows: Record<string, unknown>[]) {
|
|
24
|
+
self.sourcesBase = rows
|
|
25
|
+
},
|
|
26
|
+
setSources(rows: Record<string, unknown>[]) {
|
|
27
|
+
self.sourcesBase = rows
|
|
28
|
+
},
|
|
29
|
+
}))
|
|
30
|
+
|
|
31
|
+
const block = (start: number, end: number) => ({
|
|
32
|
+
refName: 'chr6',
|
|
33
|
+
assemblyName: 'hg38',
|
|
34
|
+
start,
|
|
35
|
+
end,
|
|
36
|
+
offsetPx: 0,
|
|
37
|
+
displayedRegionIndex: 0,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const FakeView = types
|
|
41
|
+
.model('FakeView', {
|
|
42
|
+
display: withFormsMenu(FakeDisplay),
|
|
43
|
+
start: types.number,
|
|
44
|
+
end: types.number,
|
|
45
|
+
})
|
|
46
|
+
.volatile(() => ({
|
|
47
|
+
width: 1000,
|
|
48
|
+
bpPerPx: 1,
|
|
49
|
+
offsetPx: 0,
|
|
50
|
+
displayedRegions: [{ start: 0, end: 170_805_979 }],
|
|
51
|
+
coarseDynamicBlocks: [],
|
|
52
|
+
}))
|
|
53
|
+
.views(self => ({
|
|
54
|
+
get dynamicBlocks() {
|
|
55
|
+
return { contentBlocks: [block(self.start, self.end)] }
|
|
56
|
+
},
|
|
57
|
+
horizontalScroll: (d: number) => d,
|
|
58
|
+
zoomTo: (bpPerPx: number) => bpPerPx,
|
|
59
|
+
}))
|
|
60
|
+
.actions(self => ({
|
|
61
|
+
setWidth() {},
|
|
62
|
+
pan(start: number, end: number) {
|
|
63
|
+
self.start = start
|
|
64
|
+
self.end = end
|
|
65
|
+
},
|
|
66
|
+
}))
|
|
67
|
+
|
|
68
|
+
const FakeSession = types
|
|
69
|
+
.model('FakeSession', { view: FakeView })
|
|
70
|
+
.volatile(() => ({
|
|
71
|
+
rpcManager: {},
|
|
72
|
+
configuration: {},
|
|
73
|
+
snackbarMessages: [] as { message: string }[],
|
|
74
|
+
}))
|
|
75
|
+
.actions(self => ({
|
|
76
|
+
notify(message: string) {
|
|
77
|
+
self.snackbarMessages.push({ message })
|
|
78
|
+
},
|
|
79
|
+
removeSnackbarMessage(message: string) {
|
|
80
|
+
self.snackbarMessages = self.snackbarMessages.filter(
|
|
81
|
+
m => m.message !== message,
|
|
82
|
+
)
|
|
83
|
+
},
|
|
84
|
+
}))
|
|
85
|
+
|
|
86
|
+
function makeSession(adapter: Record<string, unknown>) {
|
|
87
|
+
return FakeSession.create({
|
|
88
|
+
view: { display: { adapter }, start: 160_600_000.4, end: 160_700_000.6 },
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const makeView = (adapter: Record<string, unknown>) => makeSession(adapter).view
|
|
93
|
+
|
|
94
|
+
const formsAdapter = {
|
|
95
|
+
type: 'WalkFormsAdapter',
|
|
96
|
+
walksLocation: { uri: 'x.walks.bed.gz', locationType: 'UriLocation' },
|
|
97
|
+
pinned: LPA,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface Item {
|
|
101
|
+
label?: unknown
|
|
102
|
+
onClick: () => void
|
|
103
|
+
disabled?: boolean
|
|
104
|
+
checked?: boolean
|
|
105
|
+
subMenu?: Item[]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const labels = (items: MenuItem[]) =>
|
|
109
|
+
(items as unknown as Item[]).map(i => i.label)
|
|
110
|
+
|
|
111
|
+
function item(items: MenuItem[], label: string) {
|
|
112
|
+
const found = (items as unknown as Item[]).find(i => i.label === label)
|
|
113
|
+
if (!found) {
|
|
114
|
+
throw new Error(`no menu item ${label}`)
|
|
115
|
+
}
|
|
116
|
+
return found
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
test('another adapter gets no forms items, and its config unchanged', () => {
|
|
120
|
+
const { display } = makeView({ type: 'VcfTabixAdapter' })
|
|
121
|
+
expect(labels(display.trackMenuItems())).toEqual(['Core item'])
|
|
122
|
+
expect(display.adapterConfig).toEqual({ type: 'VcfTabixAdapter' })
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('a WalkFormsAdapter track gets the pin, re-pin and floor items', () => {
|
|
126
|
+
const { display } = makeView(formsAdapter)
|
|
127
|
+
expect(labels(display.trackMenuItems())).toEqual([
|
|
128
|
+
'Core item',
|
|
129
|
+
'Pinned to chr6:160,525,001-160,655,000',
|
|
130
|
+
'Re-pin to visible region',
|
|
131
|
+
'Hide changes under',
|
|
132
|
+
])
|
|
133
|
+
expect(
|
|
134
|
+
labels(
|
|
135
|
+
item(display.trackMenuItems(), 'Hide changes under')
|
|
136
|
+
.subMenu as unknown as MenuItem[],
|
|
137
|
+
),
|
|
138
|
+
).toEqual(['Auto', '50 bp', '1 kb', '10 kb'])
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('the pinned label counts the forms the rows hold', () => {
|
|
142
|
+
const { display } = makeView(formsAdapter)
|
|
143
|
+
display.setRows([
|
|
144
|
+
...Array.from({ length: 8 }, (_, i) => ({ name: `F${i + 1}`, forms: 1 })),
|
|
145
|
+
{ name: 'other', forms: 18 },
|
|
146
|
+
])
|
|
147
|
+
const [label] = display.formsMenuItems()
|
|
148
|
+
expect(label).toMatchObject({
|
|
149
|
+
label: 'Pinned to chr6:160,525,001-160,655,000 · 26 forms',
|
|
150
|
+
disabled: true,
|
|
151
|
+
})
|
|
152
|
+
expect(pinnedLabel(LPA, 1)).toBe(
|
|
153
|
+
'Pinned to chr6:160,525,001-160,655,000 · 1 form',
|
|
154
|
+
)
|
|
155
|
+
expect(pinnedLabel(undefined)).toBe('Not pinned')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('re-pin sets the window to the visible region, in whole bases', () => {
|
|
159
|
+
const view = makeView(formsAdapter)
|
|
160
|
+
const { display } = view
|
|
161
|
+
item(display.trackMenuItems(), 'Re-pin to visible region').onClick()
|
|
162
|
+
expect(display.formsPinned).toEqual({
|
|
163
|
+
refName: 'chr6',
|
|
164
|
+
start: 160_600_000,
|
|
165
|
+
end: 160_700_001,
|
|
166
|
+
})
|
|
167
|
+
expect(display.adapterConfig.pinned).toEqual(display.formsPinned)
|
|
168
|
+
expect(display.adapterConfig.walksLocation).toEqual(
|
|
169
|
+
formsAdapter.walksLocation,
|
|
170
|
+
)
|
|
171
|
+
expect(
|
|
172
|
+
item(display.trackMenuItems(), 'Re-pin to visible region').disabled,
|
|
173
|
+
).toBe(true)
|
|
174
|
+
|
|
175
|
+
view.pan(160_800_000, 160_900_000)
|
|
176
|
+
expect(display.adapterConfig.pinned).toEqual({
|
|
177
|
+
refName: 'chr6',
|
|
178
|
+
start: 160_600_000,
|
|
179
|
+
end: 160_700_001,
|
|
180
|
+
})
|
|
181
|
+
expect(
|
|
182
|
+
item(display.trackMenuItems(), 'Re-pin to visible region').disabled,
|
|
183
|
+
).toBe(false)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test('the floor items set floorBp and check the one in force', () => {
|
|
187
|
+
const { display } = makeView({ ...formsAdapter, floorBp: 1000 })
|
|
188
|
+
const checked = () =>
|
|
189
|
+
item(display.trackMenuItems(), 'Hide changes under')
|
|
190
|
+
.subMenu!.filter(i => i.checked)
|
|
191
|
+
.map(i => i.label)
|
|
192
|
+
expect(checked()).toEqual(['1 kb'])
|
|
193
|
+
item(display.trackMenuItems(), 'Hide changes under')
|
|
194
|
+
.subMenu!.find(i => i.label === '10 kb')!
|
|
195
|
+
.onClick()
|
|
196
|
+
expect(display.formsFloorBp).toBe(10_000)
|
|
197
|
+
expect(display.adapterConfig.floorBp).toBe(10_000)
|
|
198
|
+
expect(display.adapterConfig.pinned).toEqual(LPA)
|
|
199
|
+
expect(checked()).toEqual(['10 kb'])
|
|
200
|
+
expect(FLOORS.map(f => f.value)).toEqual(['auto', 50, 1000, 10_000])
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('the floor defaults to auto when the config leaves it unset', () => {
|
|
204
|
+
const { display } = makeView(formsAdapter)
|
|
205
|
+
expect(display.formsFloor).toBe('auto')
|
|
206
|
+
expect(display.adapterConfig).toEqual(formsAdapter)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test("a floor change or new rows take down the old window's warnings", () => {
|
|
210
|
+
const session = makeSession(formsAdapter)
|
|
211
|
+
const { display } = session.view
|
|
212
|
+
const lpa = 'Forms of chr6:160,525,001-160,655,000: 3 smaller forms'
|
|
213
|
+
session.notify(lpa)
|
|
214
|
+
session.notify('Another track: a warning of its own')
|
|
215
|
+
item(display.trackMenuItems(), 'Hide changes under')
|
|
216
|
+
.subMenu!.find(i => i.label === '10 kb')!
|
|
217
|
+
.onClick()
|
|
218
|
+
expect(session.snackbarMessages.map(m => m.message)).toEqual([
|
|
219
|
+
'Another track: a warning of its own',
|
|
220
|
+
])
|
|
221
|
+
|
|
222
|
+
session.notify(lpa)
|
|
223
|
+
display.setSources([{ name: 'F1' }])
|
|
224
|
+
expect(display.sourcesBase).toEqual([{ name: 'F1' }])
|
|
225
|
+
expect(session.snackbarMessages).toHaveLength(1)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
test("a re-pin takes down the old window's warnings", () => {
|
|
229
|
+
const session = makeSession(formsAdapter)
|
|
230
|
+
const { display } = session.view
|
|
231
|
+
display.setSources([{ name: 'F1' }])
|
|
232
|
+
session.notify('Forms of chr6:160,525,001-160,655,000: 3 smaller forms')
|
|
233
|
+
item(display.trackMenuItems(), 'Re-pin to visible region').onClick()
|
|
234
|
+
session.notify('Forms of chr6:160,600,001-160,700,001: 2 smaller forms')
|
|
235
|
+
display.setSources([{ name: 'F1' }, { name: 'F2' }])
|
|
236
|
+
expect(session.snackbarMessages).toEqual([])
|
|
237
|
+
})
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { readConfObject } from '@jbrowse/core/configuration'
|
|
2
|
+
import { DisplayType } from '@jbrowse/core/pluggableElementTypes'
|
|
3
|
+
import { getContainingView, getSession } from '@jbrowse/core/util'
|
|
4
|
+
import { types } from '@jbrowse/mobx-state-tree'
|
|
5
|
+
|
|
6
|
+
import { floorOf, pinnedLocus, pinnedOf, warningPrefix } from './pinned.ts'
|
|
7
|
+
import { hostWindow, isLinearHost } from '../GraphGenomeView/host.ts'
|
|
8
|
+
|
|
9
|
+
import type { FloorBp, Pinned } from './pinned.ts'
|
|
10
|
+
import type PluginManager from '@jbrowse/core/PluginManager'
|
|
11
|
+
import type { AnyConfigurationModel } from '@jbrowse/core/configuration'
|
|
12
|
+
import type { MenuItem } from '@jbrowse/core/ui'
|
|
13
|
+
import type { IAnyModelType, IAnyStateTreeNode } from '@jbrowse/mobx-state-tree'
|
|
14
|
+
|
|
15
|
+
export const FORMS_DISPLAYS = ['LinearMultiSampleVariantDisplay']
|
|
16
|
+
|
|
17
|
+
export const FLOORS: { label: string; value: FloorBp }[] = [
|
|
18
|
+
{ label: 'Auto', value: 'auto' },
|
|
19
|
+
{ label: '50 bp', value: 50 },
|
|
20
|
+
{ label: '1 kb', value: 1000 },
|
|
21
|
+
{ label: '10 kb', value: 10_000 },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
export function pinnedLabel(pinned: Pinned | undefined, forms?: number) {
|
|
25
|
+
return pinned
|
|
26
|
+
? `Pinned to ${pinnedLocus(pinned)}${forms === undefined ? '' : ` · ${forms} form${forms === 1 ? '' : 's'}`}`
|
|
27
|
+
: 'Not pinned'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface FormsHost {
|
|
31
|
+
parentTrack: { configuration: AnyConfigurationModel }
|
|
32
|
+
sourcesBase?: Record<string, unknown>[]
|
|
33
|
+
trackMenuItems: () => MenuItem[]
|
|
34
|
+
setSources?: (sources: unknown, adapterConfig?: unknown) => void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface Snackbar {
|
|
38
|
+
snackbarMessages?: { message: string }[]
|
|
39
|
+
removeSnackbarMessage?: (message: string) => void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function clearWarnings(
|
|
43
|
+
node: IAnyStateTreeNode,
|
|
44
|
+
windows: (Pinned | undefined)[],
|
|
45
|
+
) {
|
|
46
|
+
const session = getSession(node) as unknown as Snackbar
|
|
47
|
+
const prefixes = windows.flatMap(w => (w ? [warningPrefix(w)] : []))
|
|
48
|
+
for (const { message } of [...(session.snackbarMessages ?? [])]) {
|
|
49
|
+
if (prefixes.some(p => message.startsWith(p))) {
|
|
50
|
+
session.removeSnackbarMessage?.(message)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Gives a WalkFormsAdapter track's display a pinned window and floor of its
|
|
57
|
+
* own, which the display stores in the session and sends in the adapter config
|
|
58
|
+
* with each request. A config edit would change the track's RPC session id
|
|
59
|
+
* and move the requests to a worker without the cut; display state leaves it.
|
|
60
|
+
*/
|
|
61
|
+
export function withFormsMenu(model: IAnyModelType) {
|
|
62
|
+
return model
|
|
63
|
+
.props({
|
|
64
|
+
formsPinned: types.maybe(types.frozen<Pinned>()),
|
|
65
|
+
formsFloorBp: types.maybe(types.frozen<FloorBp>()),
|
|
66
|
+
})
|
|
67
|
+
.views(self => ({
|
|
68
|
+
get configuredAdapter(): Record<string, unknown> {
|
|
69
|
+
return readConfObject(
|
|
70
|
+
(self as unknown as FormsHost).parentTrack.configuration,
|
|
71
|
+
'adapter',
|
|
72
|
+
) as Record<string, unknown>
|
|
73
|
+
},
|
|
74
|
+
get walkForms() {
|
|
75
|
+
return this.configuredAdapter.type === 'WalkFormsAdapter'
|
|
76
|
+
},
|
|
77
|
+
}))
|
|
78
|
+
.views(self => ({
|
|
79
|
+
get adapterConfig(): Record<string, unknown> {
|
|
80
|
+
const conf = self.configuredAdapter
|
|
81
|
+
return self.walkForms
|
|
82
|
+
? {
|
|
83
|
+
...conf,
|
|
84
|
+
...(self.formsPinned ? { pinned: self.formsPinned } : {}),
|
|
85
|
+
...(self.formsFloorBp === undefined
|
|
86
|
+
? {}
|
|
87
|
+
: { floorBp: self.formsFloorBp }),
|
|
88
|
+
}
|
|
89
|
+
: conf
|
|
90
|
+
},
|
|
91
|
+
get formsWindow() {
|
|
92
|
+
return pinnedOf(this.adapterConfig.pinned)
|
|
93
|
+
},
|
|
94
|
+
get formsFloor() {
|
|
95
|
+
return floorOf(this.adapterConfig.floorBp)
|
|
96
|
+
},
|
|
97
|
+
get formsCount() {
|
|
98
|
+
return (self as unknown as FormsHost).sourcesBase?.reduce(
|
|
99
|
+
(n, row) => n + (Number(row.forms) || 1),
|
|
100
|
+
0,
|
|
101
|
+
)
|
|
102
|
+
},
|
|
103
|
+
get visibleWindow(): Pinned | undefined {
|
|
104
|
+
const view: unknown = getContainingView(self)
|
|
105
|
+
const window = isLinearHost(view) ? hostWindow(view) : undefined
|
|
106
|
+
return window && window.end > window.start
|
|
107
|
+
? {
|
|
108
|
+
refName: window.refName,
|
|
109
|
+
start: Math.max(0, Math.floor(window.start)),
|
|
110
|
+
end: Math.ceil(window.end),
|
|
111
|
+
}
|
|
112
|
+
: undefined
|
|
113
|
+
},
|
|
114
|
+
}))
|
|
115
|
+
.volatile(() => ({
|
|
116
|
+
warnedWindow: undefined as Pinned | undefined,
|
|
117
|
+
}))
|
|
118
|
+
.actions(self => {
|
|
119
|
+
const { setSources: superSetSources } = self as unknown as FormsHost
|
|
120
|
+
const clearFormsWarnings = () => {
|
|
121
|
+
clearWarnings(self, [self.warnedWindow, self.formsWindow])
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
setFormsPinned(pinned: Pinned) {
|
|
125
|
+
clearFormsWarnings()
|
|
126
|
+
self.formsPinned = pinned
|
|
127
|
+
},
|
|
128
|
+
setFormsFloorBp(floorBp: FloorBp) {
|
|
129
|
+
clearFormsWarnings()
|
|
130
|
+
self.formsFloorBp = floorBp
|
|
131
|
+
},
|
|
132
|
+
// Core posts the new forms' warnings right after this, so the old
|
|
133
|
+
// ones come down first
|
|
134
|
+
setSources(sources: unknown, adapterConfig?: unknown) {
|
|
135
|
+
if (self.walkForms) {
|
|
136
|
+
clearFormsWarnings()
|
|
137
|
+
self.warnedWindow = self.formsWindow
|
|
138
|
+
}
|
|
139
|
+
superSetSources?.(sources, adapterConfig)
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
.views(self => {
|
|
144
|
+
const { trackMenuItems: superTrackMenuItems } =
|
|
145
|
+
self as unknown as FormsHost
|
|
146
|
+
return {
|
|
147
|
+
formsMenuItems(): MenuItem[] {
|
|
148
|
+
const pinned = self.formsWindow
|
|
149
|
+
const visible = self.visibleWindow
|
|
150
|
+
return [
|
|
151
|
+
{
|
|
152
|
+
label: pinnedLabel(pinned, self.formsCount),
|
|
153
|
+
disabled: true,
|
|
154
|
+
onClick: () => {},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
label: 'Re-pin to visible region',
|
|
158
|
+
helpText:
|
|
159
|
+
'Cut the forms from the region the view shows. Panning and zooming leave the forms alone',
|
|
160
|
+
disabled:
|
|
161
|
+
!visible ||
|
|
162
|
+
(pinned !== undefined &&
|
|
163
|
+
pinnedLocus(visible) === pinnedLocus(pinned)),
|
|
164
|
+
onClick: () => {
|
|
165
|
+
if (visible) {
|
|
166
|
+
self.setFormsPinned(visible)
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
label: 'Hide changes under',
|
|
172
|
+
subMenu: FLOORS.map(({ label, value }) => ({
|
|
173
|
+
label,
|
|
174
|
+
type: 'radio' as const,
|
|
175
|
+
checked: self.formsFloor === value,
|
|
176
|
+
onClick: () => {
|
|
177
|
+
self.setFormsFloorBp(value)
|
|
178
|
+
},
|
|
179
|
+
})),
|
|
180
|
+
},
|
|
181
|
+
]
|
|
182
|
+
},
|
|
183
|
+
trackMenuItems(): MenuItem[] {
|
|
184
|
+
return self.walkForms
|
|
185
|
+
? [...superTrackMenuItems(), ...this.formsMenuItems()]
|
|
186
|
+
: superTrackMenuItems()
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export default function WalkFormsMenuF(pluginManager: PluginManager) {
|
|
193
|
+
pluginManager.addToExtensionPoint('Core-extendPluggableElement', element => {
|
|
194
|
+
if (
|
|
195
|
+
element instanceof DisplayType &&
|
|
196
|
+
FORMS_DISPLAYS.includes(element.name)
|
|
197
|
+
) {
|
|
198
|
+
element.extendStateModel(withFormsMenu)
|
|
199
|
+
}
|
|
200
|
+
return element
|
|
201
|
+
})
|
|
202
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import AdapterType from '@jbrowse/core/pluggableElementTypes/AdapterType'
|
|
2
|
+
|
|
3
|
+
import configSchema from './configSchema.ts'
|
|
4
|
+
|
|
5
|
+
import type PluginManager from '@jbrowse/core/PluginManager'
|
|
6
|
+
|
|
7
|
+
export default function WalkFormsAdapterF(pluginManager: PluginManager) {
|
|
8
|
+
pluginManager.addAdapterType(
|
|
9
|
+
() =>
|
|
10
|
+
new AdapterType({
|
|
11
|
+
name: 'WalkFormsAdapter',
|
|
12
|
+
displayName: 'Walk-file structural forms adapter',
|
|
13
|
+
configSchema,
|
|
14
|
+
adapterMetadata: {
|
|
15
|
+
category: 'Variant adapters',
|
|
16
|
+
},
|
|
17
|
+
getAdapterClass: () =>
|
|
18
|
+
import('./WalkFormsAdapter.ts').then(r => r.default),
|
|
19
|
+
}),
|
|
20
|
+
)
|
|
21
|
+
}
|