jbrowse-plugin-mafviewer 1.0.2 → 1.0.3
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/jbrowse-plugin-mafviewer.umd.development.js +200 -97
- package/dist/jbrowse-plugin-mafviewer.umd.development.js.map +1 -1
- package/dist/jbrowse-plugin-mafviewer.umd.production.min.js +1 -1
- package/dist/jbrowse-plugin-mafviewer.umd.production.min.js.map +1 -1
- package/package.json +1 -1
- package/src/LinearMafDisplay/components/ColorLegend.tsx +25 -28
- package/src/LinearMafDisplay/components/SetRowHeight.tsx +76 -0
- package/src/LinearMafDisplay/components/YScaleBars.tsx +1 -1
- package/src/LinearMafDisplay/configSchema.ts +2 -3
- package/src/LinearMafDisplay/renderSvg.tsx +13 -4
- package/src/LinearMafDisplay/stateModel.ts +50 -12
- package/src/LinearMafRenderer/LinearMafRenderer.ts +98 -43
- package/src/MafTabixAdapter/MafTabixAdapter.ts +19 -17
|
@@ -5,9 +5,10 @@ import {
|
|
|
5
5
|
ConfigurationReference,
|
|
6
6
|
getConf,
|
|
7
7
|
} from '@jbrowse/core/configuration'
|
|
8
|
-
import { getEnv } from '@jbrowse/core/util'
|
|
8
|
+
import { getEnv, getSession } from '@jbrowse/core/util'
|
|
9
9
|
import PluginManager from '@jbrowse/core/PluginManager'
|
|
10
10
|
import { ExportSvgDisplayOptions } from '@jbrowse/plugin-linear-genome-view'
|
|
11
|
+
import SetRowHeightDialog from './components/SetRowHeight'
|
|
11
12
|
|
|
12
13
|
function isStrs(array: unknown[]): array is string[] {
|
|
13
14
|
return typeof array[0] === 'string'
|
|
@@ -24,13 +25,12 @@ export default function stateModelFactory(
|
|
|
24
25
|
const LinearGenomePlugin = pluginManager.getPlugin(
|
|
25
26
|
'LinearGenomeViewPlugin',
|
|
26
27
|
) as import('@jbrowse/plugin-linear-genome-view').default
|
|
27
|
-
|
|
28
|
-
const { linearBasicDisplayModelFactory } = LinearGenomePlugin.exports
|
|
28
|
+
const { BaseLinearDisplay } = LinearGenomePlugin.exports
|
|
29
29
|
|
|
30
30
|
return types
|
|
31
31
|
.compose(
|
|
32
32
|
'LinearMafDisplay',
|
|
33
|
-
|
|
33
|
+
BaseLinearDisplay,
|
|
34
34
|
types.model({
|
|
35
35
|
/**
|
|
36
36
|
* #property
|
|
@@ -40,11 +40,33 @@ export default function stateModelFactory(
|
|
|
40
40
|
* #property
|
|
41
41
|
*/
|
|
42
42
|
configuration: ConfigurationReference(configSchema),
|
|
43
|
+
/**
|
|
44
|
+
* #property
|
|
45
|
+
*/
|
|
46
|
+
rowHeight: 15,
|
|
47
|
+
/**
|
|
48
|
+
* #property
|
|
49
|
+
*/
|
|
50
|
+
rowProportion: 0.8,
|
|
43
51
|
}),
|
|
44
52
|
)
|
|
45
53
|
.volatile(() => ({
|
|
46
54
|
prefersOffset: true,
|
|
47
55
|
}))
|
|
56
|
+
.actions(self => ({
|
|
57
|
+
/**
|
|
58
|
+
* #action
|
|
59
|
+
*/
|
|
60
|
+
setRowHeight(n: number) {
|
|
61
|
+
self.rowHeight = n
|
|
62
|
+
},
|
|
63
|
+
/**
|
|
64
|
+
* #action
|
|
65
|
+
*/
|
|
66
|
+
setRowProportion(n: number) {
|
|
67
|
+
self.rowProportion = n
|
|
68
|
+
},
|
|
69
|
+
}))
|
|
48
70
|
.views(self => ({
|
|
49
71
|
/**
|
|
50
72
|
* #getter
|
|
@@ -59,12 +81,7 @@ export default function stateModelFactory(
|
|
|
59
81
|
return r
|
|
60
82
|
}
|
|
61
83
|
},
|
|
62
|
-
|
|
63
|
-
* #getter
|
|
64
|
-
*/
|
|
65
|
-
get rowHeight() {
|
|
66
|
-
return 20
|
|
67
|
-
},
|
|
84
|
+
|
|
68
85
|
/**
|
|
69
86
|
* #getter
|
|
70
87
|
*/
|
|
@@ -88,7 +105,10 @@ export default function stateModelFactory(
|
|
|
88
105
|
},
|
|
89
106
|
}))
|
|
90
107
|
.views(self => {
|
|
91
|
-
const {
|
|
108
|
+
const {
|
|
109
|
+
trackMenuItems: superTrackMenuItems,
|
|
110
|
+
renderProps: superRenderProps,
|
|
111
|
+
} = self
|
|
92
112
|
return {
|
|
93
113
|
/**
|
|
94
114
|
* #method
|
|
@@ -96,10 +116,29 @@ export default function stateModelFactory(
|
|
|
96
116
|
renderProps() {
|
|
97
117
|
return {
|
|
98
118
|
...superRenderProps(),
|
|
119
|
+
config: self.rendererConfig,
|
|
99
120
|
samples: self.samples,
|
|
100
121
|
rowHeight: self.rowHeight,
|
|
122
|
+
rowProportion: self.rowProportion,
|
|
101
123
|
}
|
|
102
124
|
},
|
|
125
|
+
/**
|
|
126
|
+
* #method
|
|
127
|
+
*/
|
|
128
|
+
trackMenuItems() {
|
|
129
|
+
return [
|
|
130
|
+
...superTrackMenuItems(),
|
|
131
|
+
{
|
|
132
|
+
label: 'Set row height',
|
|
133
|
+
onClick: () => {
|
|
134
|
+
getSession(self).queueDialog(handleClose => [
|
|
135
|
+
SetRowHeightDialog,
|
|
136
|
+
{ model: self, handleClose },
|
|
137
|
+
])
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
]
|
|
141
|
+
},
|
|
103
142
|
}
|
|
104
143
|
})
|
|
105
144
|
.actions(self => {
|
|
@@ -110,7 +149,6 @@ export default function stateModelFactory(
|
|
|
110
149
|
*/
|
|
111
150
|
async renderSvg(opts: ExportSvgDisplayOptions): Promise<any> {
|
|
112
151
|
const { renderSvg } = await import('./renderSvg')
|
|
113
|
-
// @ts-expect-error
|
|
114
152
|
return renderSvg(self, opts, superRenderSvg)
|
|
115
153
|
},
|
|
116
154
|
}
|
|
@@ -3,23 +3,12 @@ import { RenderArgsDeserialized } from '@jbrowse/core/pluggableElementTypes/rend
|
|
|
3
3
|
import { createJBrowseTheme } from '@jbrowse/core/ui'
|
|
4
4
|
import {
|
|
5
5
|
Feature,
|
|
6
|
+
Region,
|
|
6
7
|
featureSpanPx,
|
|
7
8
|
renderToAbstractCanvas,
|
|
8
9
|
} from '@jbrowse/core/util'
|
|
9
10
|
import { Theme } from '@mui/material'
|
|
10
11
|
|
|
11
|
-
function getCorrectionFactor(scale: number) {
|
|
12
|
-
if (scale >= 1) {
|
|
13
|
-
return 0.6
|
|
14
|
-
} else if (scale >= 0.2) {
|
|
15
|
-
return 0.05
|
|
16
|
-
} else if (scale >= 0.02) {
|
|
17
|
-
return 0.03
|
|
18
|
-
} else {
|
|
19
|
-
return 0.02
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
12
|
export function getContrastBaseMap(theme: Theme) {
|
|
24
13
|
return Object.fromEntries(
|
|
25
14
|
Object.entries(getColorBaseMap(theme)).map(([key, value]) => [
|
|
@@ -46,6 +35,7 @@ function makeImageData({
|
|
|
46
35
|
renderArgs: RenderArgsDeserialized & {
|
|
47
36
|
samples: { id: string; color?: string }[]
|
|
48
37
|
rowHeight: number
|
|
38
|
+
rowProportion: number
|
|
49
39
|
}
|
|
50
40
|
}) {
|
|
51
41
|
const {
|
|
@@ -54,6 +44,7 @@ function makeImageData({
|
|
|
54
44
|
rowHeight,
|
|
55
45
|
theme: configTheme,
|
|
56
46
|
samples,
|
|
47
|
+
rowProportion,
|
|
57
48
|
} = renderArgs
|
|
58
49
|
const [region] = regions
|
|
59
50
|
const features = renderArgs.features as Map<string, Feature>
|
|
@@ -63,7 +54,9 @@ function makeImageData({
|
|
|
63
54
|
const contrastForBase = getContrastBaseMap(theme)
|
|
64
55
|
const sampleToRowMap = new Map(samples.map((s, i) => [s.id, i]))
|
|
65
56
|
const scale = 1 / bpPerPx
|
|
66
|
-
const
|
|
57
|
+
const f = 0.4
|
|
58
|
+
const h2 = h * rowProportion
|
|
59
|
+
const offset = h2 / 2
|
|
67
60
|
|
|
68
61
|
// sample as alignments
|
|
69
62
|
ctx.font = 'bold 10px Courier New,monospace'
|
|
@@ -76,70 +69,132 @@ function makeImageData({
|
|
|
76
69
|
const origAlignment = val.data
|
|
77
70
|
const alignment = origAlignment.toLowerCase()
|
|
78
71
|
|
|
79
|
-
// gaps
|
|
80
|
-
ctx.beginPath()
|
|
81
|
-
ctx.fillStyle = 'black'
|
|
82
|
-
const offset0 = (5 / 12) * h
|
|
83
|
-
const h6 = h / 6
|
|
84
72
|
const row = sampleToRowMap.get(sample)
|
|
85
73
|
if (row === undefined) {
|
|
86
74
|
throw new Error(`unknown sample encountered: ${sample}`)
|
|
87
75
|
}
|
|
76
|
+
|
|
88
77
|
const t = h * row
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
78
|
+
|
|
79
|
+
// gaps
|
|
80
|
+
ctx.beginPath()
|
|
81
|
+
ctx.fillStyle = 'black'
|
|
82
|
+
for (let i = 0, o = 0; i < alignment.length; i++) {
|
|
83
|
+
if (seq[i] !== '-') {
|
|
84
|
+
if (alignment[i] === '-') {
|
|
85
|
+
const l = leftPx + scale * o
|
|
86
|
+
ctx.moveTo(l, t + h2)
|
|
87
|
+
ctx.lineTo(l + scale + f, t + h2)
|
|
88
|
+
}
|
|
89
|
+
o++
|
|
93
90
|
}
|
|
94
91
|
}
|
|
95
|
-
ctx.
|
|
96
|
-
const offset = (1 / 4) * h
|
|
97
|
-
const h2 = h / 2
|
|
92
|
+
ctx.stroke()
|
|
98
93
|
|
|
99
94
|
// matches
|
|
100
95
|
ctx.beginPath()
|
|
101
96
|
ctx.fillStyle = 'lightgrey'
|
|
102
|
-
for (let i = 0; i < alignment.length; i++) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
97
|
+
for (let i = 0, o = 0; i < alignment.length; i++) {
|
|
98
|
+
if (seq[i] !== '-') {
|
|
99
|
+
const c = alignment[i]
|
|
100
|
+
const l = leftPx + scale * o
|
|
101
|
+
if (seq[i] === c && c !== '-') {
|
|
102
|
+
ctx.rect(l, offset + t, scale + f, h2)
|
|
103
|
+
}
|
|
104
|
+
o++
|
|
107
105
|
}
|
|
108
106
|
}
|
|
109
107
|
ctx.fill()
|
|
110
108
|
|
|
111
109
|
// mismatches
|
|
112
|
-
for (let i = 0; i < alignment.length; i++) {
|
|
110
|
+
for (let i = 0, o = 0; i < alignment.length; i++) {
|
|
113
111
|
const c = alignment[i]
|
|
114
|
-
if (seq[i] !==
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
112
|
+
if (seq[i] !== '-') {
|
|
113
|
+
if (seq[i] !== c && c !== '-') {
|
|
114
|
+
const l = leftPx + scale * o
|
|
115
|
+
ctx.fillStyle =
|
|
116
|
+
colorForBase[c as keyof typeof colorForBase] ?? 'purple'
|
|
117
|
+
ctx.fillRect(l, offset + t, scale + f, h2)
|
|
118
|
+
}
|
|
119
|
+
o++
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
// font
|
|
123
124
|
const charSize = { w: 10 }
|
|
124
125
|
if (scale >= charSize.w) {
|
|
125
|
-
for (let i = 0; i < alignment.length; i++) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
126
|
+
for (let i = 0, o = 0; i < alignment.length; i++) {
|
|
127
|
+
if (seq[i] !== '-') {
|
|
128
|
+
const l = leftPx + scale * o
|
|
129
|
+
const offset = (scale - charSize.w) / 2 + 1
|
|
130
|
+
const c = alignment[i]
|
|
131
|
+
if (seq[i] !== c && c !== '-') {
|
|
132
|
+
ctx.fillStyle = contrastForBase[c] ?? 'black'
|
|
133
|
+
ctx.fillText(origAlignment[i], l + offset, h2 + t + 3)
|
|
134
|
+
}
|
|
135
|
+
o++
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// second pass for insertions, has slightly improved look since the
|
|
143
|
+
// insertions are always 'on top' of the other features
|
|
144
|
+
for (const feature of features.values()) {
|
|
145
|
+
const [leftPx] = featureSpanPx(feature, region, bpPerPx)
|
|
146
|
+
const vals = feature.get('alignments') as Record<string, { data: string }>
|
|
147
|
+
const seq = feature.get('seq').toLowerCase()
|
|
148
|
+
|
|
149
|
+
for (const [sample, val] of Object.entries(vals)) {
|
|
150
|
+
const origAlignment = val.data
|
|
151
|
+
const alignment = origAlignment.toLowerCase()
|
|
152
|
+
const row = sampleToRowMap.get(sample)
|
|
153
|
+
if (row === undefined) {
|
|
154
|
+
throw new Error(`unknown sample encountered: ${sample}`)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const t = h * row
|
|
158
|
+
|
|
159
|
+
ctx.beginPath()
|
|
160
|
+
ctx.fillStyle = 'purple'
|
|
161
|
+
for (let i = 0, o = 0; i < alignment.length; i++) {
|
|
162
|
+
let ins = ''
|
|
163
|
+
while (seq[i] === '-') {
|
|
164
|
+
if (alignment[i] !== '-') {
|
|
165
|
+
ins += alignment[i]
|
|
132
166
|
}
|
|
167
|
+
i++
|
|
133
168
|
}
|
|
169
|
+
if (ins.length) {
|
|
170
|
+
const l = leftPx + scale * o - 2
|
|
171
|
+
ctx.rect(l, offset + t, 2, h2)
|
|
172
|
+
ctx.rect(l - 2, offset + t, 6, 1)
|
|
173
|
+
ctx.rect(l - 2, offset + t + h2, 6, 1)
|
|
174
|
+
}
|
|
175
|
+
o++
|
|
134
176
|
}
|
|
177
|
+
ctx.fill()
|
|
135
178
|
}
|
|
136
179
|
}
|
|
137
180
|
}
|
|
138
181
|
export default class LinearMafRenderer extends FeatureRendererType {
|
|
182
|
+
getExpandedRegion(region: Region) {
|
|
183
|
+
const { start, end } = region
|
|
184
|
+
const bpExpansion = 1
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
// xref https://github.com/mobxjs/mobx-state-tree/issues/1524 for Omit
|
|
188
|
+
...(region as Omit<typeof region, symbol>),
|
|
189
|
+
start: Math.floor(Math.max(start - bpExpansion, 0)),
|
|
190
|
+
end: Math.ceil(end + bpExpansion),
|
|
191
|
+
}
|
|
192
|
+
}
|
|
139
193
|
async render(
|
|
140
194
|
renderProps: RenderArgsDeserialized & {
|
|
141
195
|
samples: { id: string; color?: string }[]
|
|
142
196
|
rowHeight: number
|
|
197
|
+
rowProportion: number
|
|
143
198
|
},
|
|
144
199
|
) {
|
|
145
200
|
const { regions, bpPerPx, samples, rowHeight } = renderProps
|
|
@@ -43,6 +43,11 @@ export default class BigMafAdapter extends BaseFeatureDataAdapter {
|
|
|
43
43
|
return adapter.getRefNames()
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
async getHeader() {
|
|
47
|
+
const { adapter } = await this.setup()
|
|
48
|
+
return adapter.getHeader()
|
|
49
|
+
}
|
|
50
|
+
|
|
46
51
|
getFeatures(query: Region) {
|
|
47
52
|
return ObservableCreate<Feature>(async observer => {
|
|
48
53
|
const { adapter } = await this.setup()
|
|
@@ -52,33 +57,30 @@ export default class BigMafAdapter extends BaseFeatureDataAdapter {
|
|
|
52
57
|
for (const feature of features) {
|
|
53
58
|
const data = (feature.get('field5') as string).split(',')
|
|
54
59
|
const alignments = {} as Record<string, OrganismRecord>
|
|
55
|
-
const main = data[0]
|
|
56
|
-
const aln = main.split(':')[5]
|
|
57
60
|
const alns = data.map(elt => elt.split(':')[5])
|
|
58
|
-
const
|
|
61
|
+
// const aln = alns[0]
|
|
62
|
+
// const alns2 = data.map(() => '')
|
|
59
63
|
// remove extraneous data in other alignments
|
|
60
64
|
// reason being: cannot represent missing data in main species that are in others)
|
|
61
|
-
for (let i = 0
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
65
|
+
// for (let i = 0; i < aln.length; i++) {
|
|
66
|
+
// if (aln[i] !== '-') {
|
|
67
|
+
// for (let j = 0; j < data.length; j++) {
|
|
68
|
+
// alns2[j] += alns[j][i]
|
|
69
|
+
// }
|
|
70
|
+
// }
|
|
71
|
+
// }
|
|
68
72
|
for (let j = 0; j < data.length; j++) {
|
|
69
73
|
const elt = data[j]
|
|
70
|
-
|
|
71
74
|
const ad = elt.split(':')
|
|
72
|
-
const org = ad[0].split('.')
|
|
73
|
-
const chr = ad[0].split('.')[1]
|
|
75
|
+
const [org, chr] = ad[0].split('.')
|
|
74
76
|
|
|
75
77
|
alignments[org] = {
|
|
76
|
-
chr
|
|
78
|
+
chr,
|
|
77
79
|
start: +ad[1],
|
|
78
80
|
srcSize: +ad[2],
|
|
79
81
|
strand: ad[3] === '-' ? -1 : 1,
|
|
80
82
|
unknown: +ad[4],
|
|
81
|
-
data:
|
|
83
|
+
data: alns[j],
|
|
82
84
|
}
|
|
83
85
|
}
|
|
84
86
|
|
|
@@ -91,8 +93,8 @@ export default class BigMafAdapter extends BaseFeatureDataAdapter {
|
|
|
91
93
|
refName: feature.get('refName'),
|
|
92
94
|
name: feature.get('name'),
|
|
93
95
|
score: feature.get('score'),
|
|
94
|
-
alignments
|
|
95
|
-
seq:
|
|
96
|
+
alignments,
|
|
97
|
+
seq: alns[0],
|
|
96
98
|
},
|
|
97
99
|
}),
|
|
98
100
|
)
|