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.
@@ -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,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 { renderProps: superRenderProps } = self
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 correctionFactor = getCorrectionFactor(bpPerPx)
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
- 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)
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.fill()
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
- 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
+ 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] !== 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)
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
- 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)
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 alns2 = data.map(() => '')
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, 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
- }
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('.')[0]
73
- const chr = ad[0].split('.')[1]
75
+ const [org, chr] = ad[0].split('.')
74
76
 
75
77
  alignments[org] = {
76
- chr: 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: alns2[j],
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: alignments,
95
- seq: alns2[0],
96
+ alignments,
97
+ seq: alns[0],
96
98
  },
97
99
  }),
98
100
  )