jbrowse-plugin-mafviewer 1.0.1
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/LICENSE +201 -0
- package/README.md +8 -0
- package/dist/jbrowse-plugin-mafviewer.umd.development.js +1310 -0
- package/dist/jbrowse-plugin-mafviewer.umd.development.js.map +1 -0
- package/dist/jbrowse-plugin-mafviewer.umd.production.min.js +2 -0
- package/dist/jbrowse-plugin-mafviewer.umd.production.min.js.map +1 -0
- package/package.json +90 -0
- package/src/BigMafAdapter/BigMafAdapter.ts +120 -0
- package/src/BigMafAdapter/configSchema.ts +33 -0
- package/src/BigMafAdapter/index.ts +15 -0
- package/src/LinearMafDisplay/components/ColorLegend.tsx +59 -0
- package/src/LinearMafDisplay/components/ReactComponent.tsx +23 -0
- package/src/LinearMafDisplay/components/RectBg.tsx +14 -0
- package/src/LinearMafDisplay/components/YScaleBars.tsx +63 -0
- package/src/LinearMafDisplay/configSchema.ts +23 -0
- package/src/LinearMafDisplay/index.ts +21 -0
- package/src/LinearMafDisplay/renderSvg.tsx +26 -0
- package/src/LinearMafDisplay/stateModel.ts +111 -0
- package/src/LinearMafRenderer/LinearMafRenderer.ts +172 -0
- package/src/LinearMafRenderer/components/ReactComponent.tsx +9 -0
- package/src/LinearMafRenderer/configSchema.ts +19 -0
- package/src/LinearMafRenderer/index.ts +16 -0
- package/src/MafAddTrackWorkflow/AddTrackWorkflow.tsx +166 -0
- package/src/MafAddTrackWorkflow/index.ts +17 -0
- package/src/MafTabixAdapter/MafTabixAdapter.ts +104 -0
- package/src/MafTabixAdapter/configSchema.ts +45 -0
- package/src/MafTabixAdapter/index.ts +15 -0
- package/src/MafTrack/configSchema.ts +20 -0
- package/src/MafTrack/index.ts +18 -0
- package/src/declare.d.ts +1 -0
- package/src/index.ts +26 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter'
|
|
2
|
+
import { getSnapshot } from 'mobx-state-tree'
|
|
3
|
+
import { Feature, Region, SimpleFeature } from '@jbrowse/core/util'
|
|
4
|
+
import { ObservableCreate } from '@jbrowse/core/util/rxjs'
|
|
5
|
+
import { firstValueFrom, toArray } from 'rxjs'
|
|
6
|
+
|
|
7
|
+
interface OrganismRecord {
|
|
8
|
+
chr: string
|
|
9
|
+
start: number
|
|
10
|
+
srcSize: number
|
|
11
|
+
strand: number
|
|
12
|
+
unknown: number
|
|
13
|
+
data: string
|
|
14
|
+
}
|
|
15
|
+
export default class BigMafAdapter extends BaseFeatureDataAdapter {
|
|
16
|
+
public setupP?: Promise<{ adapter: BaseFeatureDataAdapter }>
|
|
17
|
+
|
|
18
|
+
async setup() {
|
|
19
|
+
if (!this.getSubAdapter) {
|
|
20
|
+
throw new Error('no getSubAdapter available')
|
|
21
|
+
}
|
|
22
|
+
const adapter = await this.getSubAdapter({
|
|
23
|
+
...getSnapshot(this.config),
|
|
24
|
+
type: 'BigBedAdapter',
|
|
25
|
+
})
|
|
26
|
+
return {
|
|
27
|
+
adapter: adapter.dataAdapter as BaseFeatureDataAdapter,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async setupPre() {
|
|
31
|
+
if (!this.setupP) {
|
|
32
|
+
this.setupP = this.setup().catch(e => {
|
|
33
|
+
this.setupP = undefined
|
|
34
|
+
throw e
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
return this.setupP
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async getRefNames() {
|
|
41
|
+
const { adapter } = await this.setup()
|
|
42
|
+
return adapter.getRefNames()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async getHeader() {
|
|
46
|
+
const { adapter } = await this.setup()
|
|
47
|
+
return adapter.getHeader()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getFeatures(query: Region) {
|
|
51
|
+
return ObservableCreate<Feature>(async observer => {
|
|
52
|
+
const { adapter } = await this.setup()
|
|
53
|
+
const features = await firstValueFrom(
|
|
54
|
+
adapter.getFeatures(query).pipe(toArray()),
|
|
55
|
+
)
|
|
56
|
+
for (const feature of features) {
|
|
57
|
+
const maf = feature.get('mafBlock') as string
|
|
58
|
+
const blocks = maf.split(';')
|
|
59
|
+
let aln: string | undefined
|
|
60
|
+
const alns = [] as string[]
|
|
61
|
+
const alignments = {} as Record<string, OrganismRecord>
|
|
62
|
+
const blocks2 = [] as string[]
|
|
63
|
+
for (const block of blocks) {
|
|
64
|
+
if (block[0] === 's') {
|
|
65
|
+
if (!aln) {
|
|
66
|
+
aln = block.split(/ +/)[6]
|
|
67
|
+
alns.push(aln)
|
|
68
|
+
blocks2.push(block)
|
|
69
|
+
} else {
|
|
70
|
+
alns.push(block.split(/ +/)[6])
|
|
71
|
+
blocks2.push(block)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const alns2 = alns.map(() => '')
|
|
76
|
+
|
|
77
|
+
if (aln) {
|
|
78
|
+
for (let i = 0; i < aln?.length; i++) {
|
|
79
|
+
if (aln[i] !== '-') {
|
|
80
|
+
for (let j = 0; j < alns.length; j++) {
|
|
81
|
+
alns2[j] += alns[j][i]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < blocks2.length; i++) {
|
|
88
|
+
const elt = blocks2[i]
|
|
89
|
+
const ad = elt.split(/ +/)
|
|
90
|
+
const y = ad[1].split('.')
|
|
91
|
+
const org = y[0]
|
|
92
|
+
const chr = y[1]
|
|
93
|
+
|
|
94
|
+
alignments[org] = {
|
|
95
|
+
chr: chr,
|
|
96
|
+
start: +ad[1],
|
|
97
|
+
srcSize: +ad[2],
|
|
98
|
+
strand: ad[3] === '+' ? 1 : -1,
|
|
99
|
+
unknown: +ad[4],
|
|
100
|
+
data: alns2[i],
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
observer.next(
|
|
104
|
+
new SimpleFeature({
|
|
105
|
+
id: feature.id(),
|
|
106
|
+
data: {
|
|
107
|
+
start: feature.get('start'),
|
|
108
|
+
end: feature.get('end'),
|
|
109
|
+
refName: feature.get('refName'),
|
|
110
|
+
seq: alns2[0],
|
|
111
|
+
alignments: alignments,
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
observer.complete()
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
freeResources(): void {}
|
|
120
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ConfigurationSchema } from '@jbrowse/core/configuration'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* #config BigMafAdapter
|
|
5
|
+
* used to configure BigMaf adapter
|
|
6
|
+
*/
|
|
7
|
+
function x() {} // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
8
|
+
|
|
9
|
+
const configSchema = ConfigurationSchema(
|
|
10
|
+
'BigMafAdapter',
|
|
11
|
+
{
|
|
12
|
+
/**
|
|
13
|
+
* #slot
|
|
14
|
+
*/
|
|
15
|
+
samples: {
|
|
16
|
+
type: 'stringArray',
|
|
17
|
+
defaultValue: [],
|
|
18
|
+
},
|
|
19
|
+
/**
|
|
20
|
+
* #slot
|
|
21
|
+
*/
|
|
22
|
+
bigBedLocation: {
|
|
23
|
+
type: 'fileLocation',
|
|
24
|
+
defaultValue: {
|
|
25
|
+
uri: '/path/to/my.bb',
|
|
26
|
+
locationType: 'UriLocation',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{ explicitlyTyped: true },
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
export default configSchema
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import PluginManager from '@jbrowse/core/PluginManager'
|
|
2
|
+
import { AdapterType } from '@jbrowse/core/pluggableElementTypes'
|
|
3
|
+
import configSchema from './configSchema'
|
|
4
|
+
import BigMafAdapter from './BigMafAdapter'
|
|
5
|
+
|
|
6
|
+
export default function BigMafAdapterF(pluginManager: PluginManager) {
|
|
7
|
+
return pluginManager.addAdapterType(
|
|
8
|
+
() =>
|
|
9
|
+
new AdapterType({
|
|
10
|
+
name: 'BigMafAdapter',
|
|
11
|
+
AdapterClass: BigMafAdapter,
|
|
12
|
+
configSchema,
|
|
13
|
+
}),
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { observer } from 'mobx-react'
|
|
3
|
+
|
|
4
|
+
// locals
|
|
5
|
+
import { LinearMafDisplayModel } from '../stateModel'
|
|
6
|
+
import RectBg from './RectBg'
|
|
7
|
+
|
|
8
|
+
const ColorLegend = observer(function ({
|
|
9
|
+
model,
|
|
10
|
+
labelWidth,
|
|
11
|
+
}: {
|
|
12
|
+
model: LinearMafDisplayModel
|
|
13
|
+
labelWidth: number
|
|
14
|
+
}) {
|
|
15
|
+
const { samples, rowHeight } = model
|
|
16
|
+
const svgFontSize = Math.min(rowHeight, 12)
|
|
17
|
+
const canDisplayLabel = rowHeight > 11
|
|
18
|
+
const colorBoxWidth = 15
|
|
19
|
+
const legendWidth = labelWidth + colorBoxWidth + 5
|
|
20
|
+
const extraOffset = 0
|
|
21
|
+
|
|
22
|
+
return samples ? (
|
|
23
|
+
<>
|
|
24
|
+
{samples.map((source, idx) => {
|
|
25
|
+
const boxHeight = Math.min(20, rowHeight)
|
|
26
|
+
return (
|
|
27
|
+
<React.Fragment key={`${source.name}-${idx}`}>
|
|
28
|
+
{source.color ? (
|
|
29
|
+
<RectBg
|
|
30
|
+
y={idx * rowHeight + 1}
|
|
31
|
+
x={extraOffset}
|
|
32
|
+
width={colorBoxWidth}
|
|
33
|
+
height={boxHeight}
|
|
34
|
+
color={source.color}
|
|
35
|
+
/>
|
|
36
|
+
) : null}
|
|
37
|
+
<RectBg
|
|
38
|
+
y={idx * rowHeight + 1}
|
|
39
|
+
x={extraOffset}
|
|
40
|
+
width={legendWidth}
|
|
41
|
+
height={boxHeight}
|
|
42
|
+
/>
|
|
43
|
+
{canDisplayLabel ? (
|
|
44
|
+
<text
|
|
45
|
+
y={idx * rowHeight + 13}
|
|
46
|
+
x={extraOffset + colorBoxWidth + 2}
|
|
47
|
+
fontSize={svgFontSize}
|
|
48
|
+
>
|
|
49
|
+
{source.name}
|
|
50
|
+
</text>
|
|
51
|
+
) : null}
|
|
52
|
+
</React.Fragment>
|
|
53
|
+
)
|
|
54
|
+
})}
|
|
55
|
+
</>
|
|
56
|
+
) : null
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
export default ColorLegend
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { getEnv } from '@jbrowse/core/util'
|
|
3
|
+
import { observer } from 'mobx-react'
|
|
4
|
+
import YScaleBars from './YScaleBars'
|
|
5
|
+
|
|
6
|
+
const LinearMafDisplay = observer(function (props: any) {
|
|
7
|
+
const { model } = props
|
|
8
|
+
const { pluginManager } = getEnv(model)
|
|
9
|
+
|
|
10
|
+
const LinearGenomePlugin = pluginManager.getPlugin(
|
|
11
|
+
'LinearGenomeViewPlugin',
|
|
12
|
+
) as import('@jbrowse/plugin-linear-genome-view').default
|
|
13
|
+
const { BaseLinearDisplayComponent } = LinearGenomePlugin.exports
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div>
|
|
17
|
+
<BaseLinearDisplayComponent {...props} />
|
|
18
|
+
<YScaleBars model={model} />
|
|
19
|
+
</div>
|
|
20
|
+
)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export default LinearMafDisplay
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
const RectBg = (props: {
|
|
4
|
+
x: number
|
|
5
|
+
y: number
|
|
6
|
+
width: number
|
|
7
|
+
height: number
|
|
8
|
+
color?: string
|
|
9
|
+
}) => {
|
|
10
|
+
const { color = 'rgb(255,255,255,0.8)' } = props
|
|
11
|
+
return <rect {...props} fill={color} />
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default RectBg
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { measureText, getContainingView } from '@jbrowse/core/util'
|
|
3
|
+
import { observer } from 'mobx-react'
|
|
4
|
+
|
|
5
|
+
// locals
|
|
6
|
+
import { LinearMafDisplayModel } from '../stateModel'
|
|
7
|
+
import ColorLegend from './ColorLegend'
|
|
8
|
+
|
|
9
|
+
const Wrapper = observer(function ({
|
|
10
|
+
children,
|
|
11
|
+
model,
|
|
12
|
+
exportSVG,
|
|
13
|
+
}: {
|
|
14
|
+
model: LinearMafDisplayModel
|
|
15
|
+
children: React.ReactNode
|
|
16
|
+
exportSVG?: boolean
|
|
17
|
+
}) {
|
|
18
|
+
if (exportSVG) {
|
|
19
|
+
return <>{children}</>
|
|
20
|
+
} else {
|
|
21
|
+
const { rowHeight, samples } = model
|
|
22
|
+
return (
|
|
23
|
+
<svg
|
|
24
|
+
style={{
|
|
25
|
+
position: 'absolute',
|
|
26
|
+
top: 0,
|
|
27
|
+
left: 0,
|
|
28
|
+
pointerEvents: 'none',
|
|
29
|
+
height: samples.length * rowHeight,
|
|
30
|
+
width: getContainingView(model).width,
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
{children}
|
|
34
|
+
</svg>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
export const YScaleBars = observer(function (props: {
|
|
40
|
+
model: LinearMafDisplayModel
|
|
41
|
+
orientation?: string
|
|
42
|
+
exportSVG?: boolean
|
|
43
|
+
}) {
|
|
44
|
+
const { model } = props
|
|
45
|
+
const { rowHeight, samples } = model
|
|
46
|
+
const svgFontSize = Math.min(rowHeight, 12)
|
|
47
|
+
const canDisplayLabel = rowHeight > 11
|
|
48
|
+
const minWidth = 20
|
|
49
|
+
|
|
50
|
+
const labelWidth = Math.max(
|
|
51
|
+
...(samples
|
|
52
|
+
.map(s => measureText(s, svgFontSize))
|
|
53
|
+
.map(width => (canDisplayLabel ? width : minWidth)) || [0]),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Wrapper {...props}>
|
|
58
|
+
<ColorLegend model={model} labelWidth={labelWidth} />
|
|
59
|
+
</Wrapper>
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
export default YScaleBars
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import PluginManager from '@jbrowse/core/PluginManager'
|
|
2
|
+
import { ConfigurationSchema } from '@jbrowse/core/configuration'
|
|
3
|
+
|
|
4
|
+
export default function configSchemaF(pluginManager: PluginManager) {
|
|
5
|
+
const LinearGenomePlugin = pluginManager.getPlugin(
|
|
6
|
+
'LinearGenomeViewPlugin',
|
|
7
|
+
) as import('@jbrowse/plugin-linear-genome-view').default
|
|
8
|
+
// @ts-expect-error
|
|
9
|
+
const { linearBasicDisplayConfigSchemaFactory } = LinearGenomePlugin.exports
|
|
10
|
+
return ConfigurationSchema(
|
|
11
|
+
'LinearMafDisplay',
|
|
12
|
+
{
|
|
13
|
+
/**
|
|
14
|
+
* #slot
|
|
15
|
+
*/
|
|
16
|
+
renderer: pluginManager.pluggableConfigSchemaType('renderer'),
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
baseConfiguration: linearBasicDisplayConfigSchemaFactory(pluginManager),
|
|
20
|
+
explicitlyTyped: true,
|
|
21
|
+
},
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import PluginManager from '@jbrowse/core/PluginManager'
|
|
2
|
+
import { DisplayType } from '@jbrowse/core/pluggableElementTypes'
|
|
3
|
+
import configSchemaF from './configSchema'
|
|
4
|
+
import stateModelFactory from './stateModel'
|
|
5
|
+
import ReactComponent from './components/ReactComponent'
|
|
6
|
+
|
|
7
|
+
export default function LinearMafDisplayF(pluginManager: PluginManager) {
|
|
8
|
+
pluginManager.addDisplayType(() => {
|
|
9
|
+
const configSchema = configSchemaF(pluginManager)
|
|
10
|
+
const stateModel = stateModelFactory(configSchema, pluginManager)
|
|
11
|
+
return new DisplayType({
|
|
12
|
+
name: 'LinearMafDisplay',
|
|
13
|
+
configSchema,
|
|
14
|
+
stateModel,
|
|
15
|
+
ReactComponent,
|
|
16
|
+
viewType: 'LinearGenomeView',
|
|
17
|
+
trackType: 'MafTrack',
|
|
18
|
+
displayName: 'MAF display',
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { getContainingView } from '@jbrowse/core/util'
|
|
3
|
+
import {
|
|
4
|
+
ExportSvgDisplayOptions,
|
|
5
|
+
LinearGenomeViewModel,
|
|
6
|
+
} from '@jbrowse/plugin-linear-genome-view'
|
|
7
|
+
|
|
8
|
+
// locals
|
|
9
|
+
import { LinearMafDisplayModel } from './stateModel'
|
|
10
|
+
import YScaleBars from './components/YScaleBars'
|
|
11
|
+
|
|
12
|
+
export async function renderSvg(
|
|
13
|
+
self: LinearMafDisplayModel,
|
|
14
|
+
opts: ExportSvgDisplayOptions,
|
|
15
|
+
superRenderSvg: (opts: ExportSvgDisplayOptions) => Promise<React.ReactNode>,
|
|
16
|
+
) {
|
|
17
|
+
const { offsetPx } = getContainingView(self) as LinearGenomeViewModel
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
<g id="snpcov">{await superRenderSvg(opts)}</g>
|
|
21
|
+
<g transform={`translate(${Math.max(-offsetPx, 0)})`}>
|
|
22
|
+
<YScaleBars model={self} orientation="left" exportSVG />
|
|
23
|
+
</g>
|
|
24
|
+
</>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Instance, types } from 'mobx-state-tree'
|
|
2
|
+
import {
|
|
3
|
+
AnyConfigurationModel,
|
|
4
|
+
AnyConfigurationSchemaType,
|
|
5
|
+
ConfigurationReference,
|
|
6
|
+
getConf,
|
|
7
|
+
} from '@jbrowse/core/configuration'
|
|
8
|
+
import { getEnv } from '@jbrowse/core/util'
|
|
9
|
+
import PluginManager from '@jbrowse/core/PluginManager'
|
|
10
|
+
import { ExportSvgDisplayOptions } from '@jbrowse/plugin-linear-genome-view'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* #stateModel LinearMafDisplay
|
|
14
|
+
* extends LinearBasicDisplay
|
|
15
|
+
*/
|
|
16
|
+
export default function stateModelFactory(
|
|
17
|
+
configSchema: AnyConfigurationSchemaType,
|
|
18
|
+
pluginManager: PluginManager,
|
|
19
|
+
) {
|
|
20
|
+
const LinearGenomePlugin = pluginManager.getPlugin(
|
|
21
|
+
'LinearGenomeViewPlugin',
|
|
22
|
+
) as import('@jbrowse/plugin-linear-genome-view').default
|
|
23
|
+
// @ts-expect-error
|
|
24
|
+
const { linearBasicDisplayModelFactory } = LinearGenomePlugin.exports
|
|
25
|
+
|
|
26
|
+
return types
|
|
27
|
+
.compose(
|
|
28
|
+
'LinearMafDisplay',
|
|
29
|
+
linearBasicDisplayModelFactory(configSchema),
|
|
30
|
+
types.model({
|
|
31
|
+
/**
|
|
32
|
+
* #property
|
|
33
|
+
*/
|
|
34
|
+
type: types.literal('LinearMafDisplay'),
|
|
35
|
+
/**
|
|
36
|
+
* #property
|
|
37
|
+
*/
|
|
38
|
+
configuration: ConfigurationReference(configSchema),
|
|
39
|
+
}),
|
|
40
|
+
)
|
|
41
|
+
.volatile(() => ({
|
|
42
|
+
prefersOffset: true,
|
|
43
|
+
}))
|
|
44
|
+
.views(self => ({
|
|
45
|
+
/**
|
|
46
|
+
* #getter
|
|
47
|
+
*/
|
|
48
|
+
get samples() {
|
|
49
|
+
const r = self.adapterConfig.samples as string[]
|
|
50
|
+
return r.map(elt => ({ name: elt, color: undefined }))
|
|
51
|
+
},
|
|
52
|
+
/**
|
|
53
|
+
* #getter
|
|
54
|
+
*/
|
|
55
|
+
get rowHeight() {
|
|
56
|
+
return 20
|
|
57
|
+
},
|
|
58
|
+
/**
|
|
59
|
+
* #getter
|
|
60
|
+
*/
|
|
61
|
+
get rendererTypeName() {
|
|
62
|
+
return 'LinearMafRenderer'
|
|
63
|
+
},
|
|
64
|
+
/**
|
|
65
|
+
* #getter
|
|
66
|
+
*/
|
|
67
|
+
get rendererConfig(): AnyConfigurationModel {
|
|
68
|
+
const configBlob = getConf(self, ['renderer']) || {}
|
|
69
|
+
const config = configBlob as Omit<typeof configBlob, symbol>
|
|
70
|
+
|
|
71
|
+
return self.rendererType.configSchema.create(
|
|
72
|
+
{
|
|
73
|
+
...config,
|
|
74
|
+
type: 'LinearMafRenderer',
|
|
75
|
+
},
|
|
76
|
+
getEnv(self),
|
|
77
|
+
)
|
|
78
|
+
},
|
|
79
|
+
}))
|
|
80
|
+
.views(self => {
|
|
81
|
+
const { renderProps: superRenderProps } = self
|
|
82
|
+
return {
|
|
83
|
+
/**
|
|
84
|
+
* #method
|
|
85
|
+
*/
|
|
86
|
+
renderProps() {
|
|
87
|
+
return {
|
|
88
|
+
...superRenderProps(),
|
|
89
|
+
samples: self.samples,
|
|
90
|
+
rowHeight: self.rowHeight,
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
.actions(self => {
|
|
96
|
+
const { renderSvg: superRenderSvg } = self
|
|
97
|
+
return {
|
|
98
|
+
/**
|
|
99
|
+
* #action
|
|
100
|
+
*/
|
|
101
|
+
async renderSvg(opts: ExportSvgDisplayOptions): Promise<any> {
|
|
102
|
+
const { renderSvg } = await import('./renderSvg')
|
|
103
|
+
// @ts-expect-error
|
|
104
|
+
return renderSvg(self, opts, superRenderSvg)
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type LinearMafDisplayStateModel = ReturnType<typeof stateModelFactory>
|
|
111
|
+
export type LinearMafDisplayModel = Instance<LinearMafDisplayStateModel>
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { FeatureRendererType } from '@jbrowse/core/pluggableElementTypes'
|
|
2
|
+
import { RenderArgsDeserialized } from '@jbrowse/core/pluggableElementTypes/renderers/BoxRendererType'
|
|
3
|
+
import { createJBrowseTheme } from '@jbrowse/core/ui'
|
|
4
|
+
import {
|
|
5
|
+
Feature,
|
|
6
|
+
featureSpanPx,
|
|
7
|
+
renderToAbstractCanvas,
|
|
8
|
+
} from '@jbrowse/core/util'
|
|
9
|
+
import { Theme } from '@mui/material'
|
|
10
|
+
|
|
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
|
+
export function getContrastBaseMap(theme: Theme) {
|
|
24
|
+
return Object.fromEntries(
|
|
25
|
+
Object.entries(getColorBaseMap(theme)).map(([key, value]) => [
|
|
26
|
+
key,
|
|
27
|
+
theme.palette.getContrastText(value),
|
|
28
|
+
]),
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getColorBaseMap(theme: Theme) {
|
|
33
|
+
const { bases } = theme.palette
|
|
34
|
+
return {
|
|
35
|
+
a: bases.A.main,
|
|
36
|
+
c: bases.C.main,
|
|
37
|
+
g: bases.G.main,
|
|
38
|
+
t: bases.T.main,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function makeImageData({
|
|
42
|
+
ctx,
|
|
43
|
+
renderArgs,
|
|
44
|
+
}: {
|
|
45
|
+
ctx: CanvasRenderingContext2D
|
|
46
|
+
renderArgs: RenderArgsDeserialized & {
|
|
47
|
+
samples: { name: string; color?: string }[]
|
|
48
|
+
rowHeight: number
|
|
49
|
+
}
|
|
50
|
+
}) {
|
|
51
|
+
const {
|
|
52
|
+
regions,
|
|
53
|
+
bpPerPx,
|
|
54
|
+
rowHeight,
|
|
55
|
+
theme: configTheme,
|
|
56
|
+
samples,
|
|
57
|
+
} = renderArgs
|
|
58
|
+
const [region] = regions
|
|
59
|
+
const features = renderArgs.features as Map<string, Feature>
|
|
60
|
+
const h = rowHeight
|
|
61
|
+
const theme = createJBrowseTheme(configTheme)
|
|
62
|
+
const colorForBase = getColorBaseMap(theme)
|
|
63
|
+
const contrastForBase = getContrastBaseMap(theme)
|
|
64
|
+
const sampleToRowMap = new Map(samples.map((s, i) => [s.name, i]))
|
|
65
|
+
const scale = 1 / bpPerPx
|
|
66
|
+
const correctionFactor = getCorrectionFactor(bpPerPx)
|
|
67
|
+
|
|
68
|
+
// sample as alignments
|
|
69
|
+
ctx.font = 'bold 10px Courier New,monospace'
|
|
70
|
+
|
|
71
|
+
for (const feature of features.values()) {
|
|
72
|
+
const [leftPx] = featureSpanPx(feature, region, bpPerPx)
|
|
73
|
+
const vals = feature.get('alignments') as Record<string, { data: string }>
|
|
74
|
+
const seq = feature.get('seq').toLowerCase()
|
|
75
|
+
for (const [sample, val] of Object.entries(vals)) {
|
|
76
|
+
const origAlignment = val.data
|
|
77
|
+
const alignment = origAlignment.toLowerCase()
|
|
78
|
+
|
|
79
|
+
// gaps
|
|
80
|
+
ctx.beginPath()
|
|
81
|
+
ctx.fillStyle = 'black'
|
|
82
|
+
const offset0 = (5 / 12) * h
|
|
83
|
+
const h6 = h / 6
|
|
84
|
+
const row = sampleToRowMap.get(sample)
|
|
85
|
+
if (row === undefined) {
|
|
86
|
+
throw new Error(`unknown sample encountered: ${sample}`)
|
|
87
|
+
}
|
|
88
|
+
const t = h * row
|
|
89
|
+
for (let i = 0; i < alignment.length; i++) {
|
|
90
|
+
const l = leftPx + scale * i
|
|
91
|
+
if (alignment[i] === '-') {
|
|
92
|
+
ctx.rect(l, offset0 + t, scale + correctionFactor, h6)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
ctx.fill()
|
|
96
|
+
const offset = (1 / 4) * h
|
|
97
|
+
const h2 = h / 2
|
|
98
|
+
|
|
99
|
+
// matches
|
|
100
|
+
ctx.beginPath()
|
|
101
|
+
ctx.fillStyle = 'lightgrey'
|
|
102
|
+
for (let i = 0; i < alignment.length; i++) {
|
|
103
|
+
const c = alignment[i]
|
|
104
|
+
const l = leftPx + scale * i
|
|
105
|
+
if (seq[i] === c && c !== '-') {
|
|
106
|
+
ctx.rect(l, offset + t, scale + correctionFactor, h2)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
ctx.fill()
|
|
110
|
+
|
|
111
|
+
// mismatches
|
|
112
|
+
for (let i = 0; i < alignment.length; i++) {
|
|
113
|
+
const c = alignment[i]
|
|
114
|
+
if (seq[i] !== c && c !== '-') {
|
|
115
|
+
const l = leftPx + scale * i
|
|
116
|
+
ctx.fillStyle =
|
|
117
|
+
colorForBase[c as keyof typeof colorForBase] ?? 'purple'
|
|
118
|
+
ctx.fillRect(l, offset + t, scale + correctionFactor, h2)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// font
|
|
123
|
+
const charSize = { w: 10 }
|
|
124
|
+
if (scale >= charSize.w) {
|
|
125
|
+
for (let i = 0; i < alignment.length; i++) {
|
|
126
|
+
const l = leftPx + scale * i
|
|
127
|
+
const offset = (scale - charSize.w) / 2 + 1
|
|
128
|
+
const c = alignment[i]
|
|
129
|
+
if (seq[i] !== c && c !== '-') {
|
|
130
|
+
ctx.fillStyle = contrastForBase[c] ?? 'black'
|
|
131
|
+
ctx.fillText(origAlignment[i], l + offset, h2 + t + 3)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export default class LinearMafRenderer extends FeatureRendererType {
|
|
139
|
+
async render(
|
|
140
|
+
renderProps: RenderArgsDeserialized & {
|
|
141
|
+
samples: { name: string; color?: string }[]
|
|
142
|
+
rowHeight: number
|
|
143
|
+
},
|
|
144
|
+
) {
|
|
145
|
+
const { regions, bpPerPx, samples, rowHeight } = renderProps
|
|
146
|
+
const [region] = regions
|
|
147
|
+
const height = samples.length * rowHeight
|
|
148
|
+
const width = (region.end - region.start) / bpPerPx
|
|
149
|
+
const features = await this.getFeatures(renderProps)
|
|
150
|
+
const res = await renderToAbstractCanvas(width, height, renderProps, ctx =>
|
|
151
|
+
makeImageData({
|
|
152
|
+
ctx,
|
|
153
|
+
renderArgs: {
|
|
154
|
+
...renderProps,
|
|
155
|
+
features,
|
|
156
|
+
},
|
|
157
|
+
}),
|
|
158
|
+
)
|
|
159
|
+
const results = await super.render({
|
|
160
|
+
...renderProps,
|
|
161
|
+
...res,
|
|
162
|
+
width,
|
|
163
|
+
height,
|
|
164
|
+
})
|
|
165
|
+
return {
|
|
166
|
+
...results,
|
|
167
|
+
...res,
|
|
168
|
+
width,
|
|
169
|
+
height,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { PrerenderedCanvas } from '@jbrowse/core/ui'
|
|
2
|
+
import { observer } from 'mobx-react'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
|
|
5
|
+
const LinearMafRendering = observer(function (props: any) {
|
|
6
|
+
return <PrerenderedCanvas {...props} />
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
export default LinearMafRendering
|