jbrowse-plugin-mafviewer 1.0.2 → 1.0.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.
@@ -43,8 +43,8 @@ export const YScaleBars = observer(function (props: {
43
43
  }) {
44
44
  const { model } = props
45
45
  const { rowHeight, samples } = model
46
- const svgFontSize = Math.min(rowHeight, 12)
47
- const canDisplayLabel = rowHeight > 11
46
+ const svgFontSize = Math.min(Math.max(rowHeight, 10), 14)
47
+ const canDisplayLabel = rowHeight >= 10
48
48
  const minWidth = 20
49
49
 
50
50
  const labelWidth = Math.max(
@@ -55,7 +55,11 @@ export const YScaleBars = observer(function (props: {
55
55
 
56
56
  return (
57
57
  <Wrapper {...props}>
58
- <ColorLegend model={model} labelWidth={labelWidth} />
58
+ <ColorLegend
59
+ model={model}
60
+ labelWidth={labelWidth}
61
+ svgFontSize={svgFontSize}
62
+ />
59
63
  </Wrapper>
60
64
  )
61
65
  })
@@ -5,8 +5,7 @@ export default function configSchemaF(pluginManager: PluginManager) {
5
5
  const LinearGenomePlugin = pluginManager.getPlugin(
6
6
  'LinearGenomeViewPlugin',
7
7
  ) as import('@jbrowse/plugin-linear-genome-view').default
8
- // @ts-expect-error
9
- const { linearBasicDisplayConfigSchemaFactory } = LinearGenomePlugin.exports
8
+ const { baseLinearDisplayConfigSchema } = LinearGenomePlugin.exports
10
9
  return ConfigurationSchema(
11
10
  'LinearMafDisplay',
12
11
  {
@@ -16,7 +15,7 @@ export default function configSchemaF(pluginManager: PluginManager) {
16
15
  renderer: pluginManager.pluggableConfigSchemaType('renderer'),
17
16
  },
18
17
  {
19
- baseConfiguration: linearBasicDisplayConfigSchemaFactory(pluginManager),
18
+ baseConfiguration: baseLinearDisplayConfigSchema,
20
19
  explicitlyTyped: true,
21
20
  },
22
21
  )
@@ -14,12 +14,21 @@ export async function renderSvg(
14
14
  opts: ExportSvgDisplayOptions,
15
15
  superRenderSvg: (opts: ExportSvgDisplayOptions) => Promise<React.ReactNode>,
16
16
  ) {
17
- const { offsetPx } = getContainingView(self) as LinearGenomeViewModel
17
+ const { height, id } = self
18
+ const { offsetPx, width } = getContainingView(self) as LinearGenomeViewModel
19
+ const clipid = `mafclip-${id}`
18
20
  return (
19
21
  <>
20
- <g id="snpcov">{await superRenderSvg(opts)}</g>
21
- <g transform={`translate(${Math.max(-offsetPx, 0)})`}>
22
- <YScaleBars model={self} orientation="left" exportSVG />
22
+ <defs>
23
+ <clipPath id={clipid}>
24
+ <rect x={0} y={0} width={width} height={height} />
25
+ </clipPath>
26
+ </defs>
27
+ <g clipPath={`url(#${clipid})`}>
28
+ <g id="snpcov">{await superRenderSvg(opts)}</g>
29
+ <g transform={`translate(${Math.max(-offsetPx, 0)})`}>
30
+ <YScaleBars model={self} orientation="left" exportSVG />
31
+ </g>
23
32
  </g>
24
33
  </>
25
34
  )
@@ -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
- // @ts-expect-error
28
- const { linearBasicDisplayModelFactory } = LinearGenomePlugin.exports
28
+ const { BaseLinearDisplay } = LinearGenomePlugin.exports
29
29
 
30
30
  return types
31
31
  .compose(
32
32
  'LinearMafDisplay',
33
- linearBasicDisplayModelFactory(configSchema),
33
+ BaseLinearDisplay,
34
34
  types.model({
35
35
  /**
36
36
  * #property
@@ -40,11 +40,43 @@ 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,
51
+ /**
52
+ * #property
53
+ */
54
+ showAllLetters: false,
43
55
  }),
44
56
  )
45
57
  .volatile(() => ({
46
58
  prefersOffset: true,
47
59
  }))
60
+ .actions(self => ({
61
+ /**
62
+ * #action
63
+ */
64
+ setRowHeight(n: number) {
65
+ self.rowHeight = n
66
+ },
67
+ /**
68
+ * #action
69
+ */
70
+ setRowProportion(n: number) {
71
+ self.rowProportion = n
72
+ },
73
+ /**
74
+ * #action
75
+ */
76
+ setShowAllLetters(f: boolean) {
77
+ self.showAllLetters = f
78
+ },
79
+ }))
48
80
  .views(self => ({
49
81
  /**
50
82
  * #getter
@@ -53,18 +85,11 @@ export default function stateModelFactory(
53
85
  const r = self.adapterConfig.samples as
54
86
  | string[]
55
87
  | { id: string; label: string; color?: string }[]
56
- if (isStrs(r)) {
57
- return r.map(elt => ({ id: elt, label: elt, color: undefined }))
58
- } else {
59
- return r
60
- }
61
- },
62
- /**
63
- * #getter
64
- */
65
- get rowHeight() {
66
- return 20
88
+ return isStrs(r)
89
+ ? r.map(elt => ({ id: elt, label: elt, color: undefined }))
90
+ : r
67
91
  },
92
+
68
93
  /**
69
94
  * #getter
70
95
  */
@@ -88,29 +113,67 @@ export default function stateModelFactory(
88
113
  },
89
114
  }))
90
115
  .views(self => {
91
- const { renderProps: superRenderProps } = self
116
+ const {
117
+ trackMenuItems: superTrackMenuItems,
118
+ renderProps: superRenderProps,
119
+ } = self
92
120
  return {
93
121
  /**
94
122
  * #method
95
123
  */
96
124
  renderProps() {
125
+ const {
126
+ showAllLetters,
127
+ rendererConfig,
128
+ samples,
129
+ rowHeight,
130
+ rowProportion,
131
+ } = self
97
132
  return {
98
133
  ...superRenderProps(),
99
- samples: self.samples,
100
- rowHeight: self.rowHeight,
134
+ config: rendererConfig,
135
+ samples,
136
+ rowHeight,
137
+ rowProportion,
138
+ showAllLetters,
101
139
  }
102
140
  },
141
+ /**
142
+ * #method
143
+ */
144
+ trackMenuItems() {
145
+ return [
146
+ ...superTrackMenuItems(),
147
+ {
148
+ label: 'Set row height',
149
+ onClick: () => {
150
+ getSession(self).queueDialog(handleClose => [
151
+ SetRowHeightDialog,
152
+ { model: self, handleClose },
153
+ ])
154
+ },
155
+ },
156
+ {
157
+ label: 'Show all letters',
158
+ type: 'checkbox',
159
+ checked: self.showAllLetters,
160
+ onClick: () => {
161
+ self.setShowAllLetters(!self.showAllLetters)
162
+ },
163
+ },
164
+ ]
165
+ },
103
166
  }
104
167
  })
105
168
  .actions(self => {
169
+ // eslint-disable-next-line @typescript-eslint/unbound-method
106
170
  const { renderSvg: superRenderSvg } = self
107
171
  return {
108
172
  /**
109
173
  * #action
110
174
  */
111
- async renderSvg(opts: ExportSvgDisplayOptions): Promise<any> {
175
+ async renderSvg(opts: ExportSvgDisplayOptions) {
112
176
  const { renderSvg } = await import('./renderSvg')
113
- // @ts-expect-error
114
177
  return renderSvg(self, opts, superRenderSvg)
115
178
  },
116
179
  }
@@ -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,24 +35,31 @@ function makeImageData({
46
35
  renderArgs: RenderArgsDeserialized & {
47
36
  samples: { id: string; color?: string }[]
48
37
  rowHeight: number
38
+ rowProportion: number
39
+ showAllLetters: boolean
49
40
  }
50
41
  }) {
51
42
  const {
52
43
  regions,
53
44
  bpPerPx,
54
45
  rowHeight,
46
+ showAllLetters,
55
47
  theme: configTheme,
56
48
  samples,
49
+ rowProportion,
57
50
  } = renderArgs
58
51
  const [region] = regions
59
52
  const features = renderArgs.features as Map<string, Feature>
60
- const h = rowHeight
53
+ const h = rowHeight * rowProportion
61
54
  const theme = createJBrowseTheme(configTheme)
62
55
  const colorForBase = getColorBaseMap(theme)
63
56
  const contrastForBase = getContrastBaseMap(theme)
64
57
  const sampleToRowMap = new Map(samples.map((s, i) => [s.id, i]))
65
58
  const scale = 1 / bpPerPx
66
- const correctionFactor = getCorrectionFactor(bpPerPx)
59
+ const f = 0.4
60
+ const h2 = rowHeight / 2
61
+ const hp2 = h / 2
62
+ const offset = (rowHeight - h) / 2
67
63
 
68
64
  // sample as alignments
69
65
  ctx.font = 'bold 10px Courier New,monospace'
@@ -76,75 +72,139 @@ function makeImageData({
76
72
  const origAlignment = val.data
77
73
  const alignment = origAlignment.toLowerCase()
78
74
 
79
- // gaps
80
- ctx.beginPath()
81
- ctx.fillStyle = 'black'
82
- const offset0 = (5 / 12) * h
83
- const h6 = h / 6
84
75
  const row = sampleToRowMap.get(sample)
85
76
  if (row === undefined) {
86
77
  throw new Error(`unknown sample encountered: ${sample}`)
87
78
  }
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)
79
+
80
+ const t = rowHeight * row
81
+
82
+ // gaps
83
+ ctx.beginPath()
84
+ ctx.fillStyle = 'black'
85
+ for (let i = 0, o = 0; i < alignment.length; i++) {
86
+ if (seq[i] !== '-') {
87
+ if (alignment[i] === '-') {
88
+ const l = leftPx + scale * o
89
+ ctx.moveTo(l, t + h2)
90
+ ctx.lineTo(l + scale + f, t + h2)
91
+ }
92
+ o++
93
93
  }
94
94
  }
95
- ctx.fill()
96
- const offset = (1 / 4) * h
97
- const h2 = h / 2
95
+ ctx.stroke()
98
96
 
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)
97
+ if (!showAllLetters) {
98
+ // matches
99
+ ctx.beginPath()
100
+ ctx.fillStyle = 'lightgrey'
101
+ for (let i = 0, o = 0; i < alignment.length; i++) {
102
+ if (seq[i] !== '-') {
103
+ const c = alignment[i]
104
+ const l = leftPx + scale * o
105
+ if (seq[i] === c && c !== '-') {
106
+ ctx.rect(l, offset + t, scale + f, h)
107
+ }
108
+ o++
109
+ }
107
110
  }
111
+ ctx.fill()
108
112
  }
109
- ctx.fill()
110
113
 
111
114
  // mismatches
112
- for (let i = 0; i < alignment.length; i++) {
115
+ for (let i = 0, o = 0; i < alignment.length; i++) {
113
116
  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)
117
+ if (seq[i] !== '-') {
118
+ if ((showAllLetters || seq[i] !== c) && c !== '-') {
119
+ const l = leftPx + scale * o
120
+ ctx.fillStyle =
121
+ colorForBase[c as keyof typeof colorForBase] ?? 'black'
122
+ ctx.fillRect(l, offset + t, scale + f, h)
123
+ }
124
+ o++
119
125
  }
120
126
  }
121
127
 
122
128
  // font
123
129
  const charSize = { w: 10 }
124
130
  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)
131
+ for (let i = 0, o = 0; i < alignment.length; i++) {
132
+ if (seq[i] !== '-') {
133
+ const l = leftPx + scale * o
134
+ const offset = (scale - charSize.w) / 2 + 1
135
+ const c = alignment[i]
136
+ if ((showAllLetters || seq[i] !== c) && c !== '-') {
137
+ ctx.fillStyle = contrastForBase[c] ?? 'white'
138
+ ctx.fillText(origAlignment[i], l + offset, hp2 + t + 3)
139
+ }
140
+ o++
132
141
  }
133
142
  }
134
143
  }
135
144
  }
136
145
  }
146
+
147
+ // second pass for insertions, has slightly improved look since the
148
+ // insertions are always 'on top' of the other features
149
+ for (const feature of features.values()) {
150
+ const [leftPx] = featureSpanPx(feature, region, bpPerPx)
151
+ const vals = feature.get('alignments') as Record<string, { data: string }>
152
+ const seq = feature.get('seq').toLowerCase()
153
+
154
+ for (const [sample, val] of Object.entries(vals)) {
155
+ const origAlignment = val.data
156
+ const alignment = origAlignment.toLowerCase()
157
+ const row = sampleToRowMap.get(sample)
158
+ if (row === undefined) {
159
+ throw new Error(`unknown sample encountered: ${sample}`)
160
+ }
161
+
162
+ const t = rowHeight * row
163
+
164
+ ctx.beginPath()
165
+ ctx.fillStyle = 'purple'
166
+ for (let i = 0, o = 0; i < alignment.length; i++) {
167
+ let ins = ''
168
+ while (seq[i] === '-') {
169
+ if (alignment[i] !== '-') {
170
+ ins += alignment[i]
171
+ }
172
+ i++
173
+ }
174
+ if (ins.length > 0) {
175
+ const l = leftPx + scale * o - 1
176
+ ctx.rect(l, offset + t + 1, 1, h - 1)
177
+ ctx.rect(l - 2, offset + t, 5, 1)
178
+ ctx.rect(l - 2, offset + t + h - 1, 5, 1)
179
+ }
180
+ o++
181
+ }
182
+ ctx.fill()
183
+ }
184
+ }
137
185
  }
138
186
  export default class LinearMafRenderer extends FeatureRendererType {
187
+ getExpandedRegion(region: Region) {
188
+ const { start, end } = region
189
+ const bpExpansion = 1
190
+
191
+ return {
192
+ // xref https://github.com/mobxjs/mobx-state-tree/issues/1524 for Omit
193
+ ...(region as Omit<typeof region, symbol>),
194
+ start: Math.floor(Math.max(start - bpExpansion, 0)),
195
+ end: Math.ceil(end + bpExpansion),
196
+ }
197
+ }
139
198
  async render(
140
199
  renderProps: RenderArgsDeserialized & {
141
200
  samples: { id: string; color?: string }[]
142
201
  rowHeight: number
202
+ rowProportion: number
143
203
  },
144
204
  ) {
145
205
  const { regions, bpPerPx, samples, rowHeight } = renderProps
146
206
  const [region] = regions
147
- const height = samples.length * rowHeight
207
+ const height = samples.length * rowHeight + 100
148
208
  const width = (region.end - region.start) / bpPerPx
149
209
  const features = await this.getFeatures(renderProps)
150
210
  const res = await renderToAbstractCanvas(width, height, renderProps, ctx =>
@@ -2,7 +2,10 @@ import { PrerenderedCanvas } from '@jbrowse/core/ui'
2
2
  import { observer } from 'mobx-react'
3
3
  import React from 'react'
4
4
 
5
- const LinearMafRendering = observer(function (props: any) {
5
+ const LinearMafRendering = observer(function (props: {
6
+ width: number
7
+ height: number
8
+ }) {
6
9
  return <PrerenderedCanvas {...props} />
7
10
  })
8
11
 
@@ -43,6 +43,7 @@ export default function MultiMAFWidget({ model }: { model: AddTrackModel }) {
43
43
  const [error, setError] = useState<unknown>()
44
44
  const [trackName, setTrackName] = useState('MAF track')
45
45
  const [choice, setChoice] = useState('BigMafAdapter')
46
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
47
  const rootModel = getRoot<any>(model)
47
48
  return (
48
49
  <Paper className={classes.paper}>
@@ -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,19 @@ 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 alns2 = data.map(() => '')
59
- // remove extraneous data in other alignments
60
- // reason being: cannot represent missing data in main species that are in others)
61
- for (let i = 0, o = 0; i < aln.length; i++, o++) {
62
- if (aln[i] !== '-') {
63
- for (let j = 0; j < data.length; j++) {
64
- alns2[j] += alns[j][i]
65
- }
66
- }
67
- }
68
- for (let j = 0; j < data.length; j++) {
69
- const elt = data[j]
70
61
 
62
+ for (const [j, elt] of data.entries()) {
71
63
  const ad = elt.split(':')
72
- const org = ad[0].split('.')[0]
73
- const chr = ad[0].split('.')[1]
64
+ const [org, chr] = ad[0].split('.')
74
65
 
75
66
  alignments[org] = {
76
- chr: chr,
67
+ chr,
77
68
  start: +ad[1],
78
69
  srcSize: +ad[2],
79
70
  strand: ad[3] === '-' ? -1 : 1,
80
71
  unknown: +ad[4],
81
- data: alns2[j],
72
+ data: alns[j],
82
73
  }
83
74
  }
84
75
 
@@ -91,8 +82,8 @@ export default class BigMafAdapter extends BaseFeatureDataAdapter {
91
82
  refName: feature.get('refName'),
92
83
  name: feature.get('name'),
93
84
  score: feature.get('score'),
94
- alignments: alignments,
95
- seq: alns2[0],
85
+ alignments,
86
+ seq: alns[0],
96
87
  },
97
88
  }),
98
89
  )