jbrowse-plugin-mafviewer 1.0.1 → 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,14 @@ 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'
12
+
13
+ function isStrs(array: unknown[]): array is string[] {
14
+ return typeof array[0] === 'string'
15
+ }
11
16
 
12
17
  /**
13
18
  * #stateModel LinearMafDisplay
@@ -20,13 +25,12 @@ export default function stateModelFactory(
20
25
  const LinearGenomePlugin = pluginManager.getPlugin(
21
26
  'LinearGenomeViewPlugin',
22
27
  ) as import('@jbrowse/plugin-linear-genome-view').default
23
- // @ts-expect-error
24
- const { linearBasicDisplayModelFactory } = LinearGenomePlugin.exports
28
+ const { BaseLinearDisplay } = LinearGenomePlugin.exports
25
29
 
26
30
  return types
27
31
  .compose(
28
32
  'LinearMafDisplay',
29
- linearBasicDisplayModelFactory(configSchema),
33
+ BaseLinearDisplay,
30
34
  types.model({
31
35
  /**
32
36
  * #property
@@ -36,25 +40,48 @@ export default function stateModelFactory(
36
40
  * #property
37
41
  */
38
42
  configuration: ConfigurationReference(configSchema),
43
+ /**
44
+ * #property
45
+ */
46
+ rowHeight: 15,
47
+ /**
48
+ * #property
49
+ */
50
+ rowProportion: 0.8,
39
51
  }),
40
52
  )
41
53
  .volatile(() => ({
42
54
  prefersOffset: true,
43
55
  }))
44
- .views(self => ({
56
+ .actions(self => ({
45
57
  /**
46
- * #getter
58
+ * #action
47
59
  */
48
- get samples() {
49
- const r = self.adapterConfig.samples as string[]
50
- return r.map(elt => ({ name: elt, color: undefined }))
60
+ setRowHeight(n: number) {
61
+ self.rowHeight = n
51
62
  },
63
+ /**
64
+ * #action
65
+ */
66
+ setRowProportion(n: number) {
67
+ self.rowProportion = n
68
+ },
69
+ }))
70
+ .views(self => ({
52
71
  /**
53
72
  * #getter
54
73
  */
55
- get rowHeight() {
56
- return 20
74
+ get samples() {
75
+ const r = self.adapterConfig.samples as
76
+ | string[]
77
+ | { id: string; label: string; color?: string }[]
78
+ if (isStrs(r)) {
79
+ return r.map(elt => ({ id: elt, label: elt, color: undefined }))
80
+ } else {
81
+ return r
82
+ }
57
83
  },
84
+
58
85
  /**
59
86
  * #getter
60
87
  */
@@ -78,7 +105,10 @@ export default function stateModelFactory(
78
105
  },
79
106
  }))
80
107
  .views(self => {
81
- const { renderProps: superRenderProps } = self
108
+ const {
109
+ trackMenuItems: superTrackMenuItems,
110
+ renderProps: superRenderProps,
111
+ } = self
82
112
  return {
83
113
  /**
84
114
  * #method
@@ -86,10 +116,29 @@ export default function stateModelFactory(
86
116
  renderProps() {
87
117
  return {
88
118
  ...superRenderProps(),
119
+ config: self.rendererConfig,
89
120
  samples: self.samples,
90
121
  rowHeight: self.rowHeight,
122
+ rowProportion: self.rowProportion,
91
123
  }
92
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
+ },
93
142
  }
94
143
  })
95
144
  .actions(self => {
@@ -100,7 +149,6 @@ export default function stateModelFactory(
100
149
  */
101
150
  async renderSvg(opts: ExportSvgDisplayOptions): Promise<any> {
102
151
  const { renderSvg } = await import('./renderSvg')
103
- // @ts-expect-error
104
152
  return renderSvg(self, opts, superRenderSvg)
105
153
  },
106
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]) => [
@@ -44,8 +33,9 @@ function makeImageData({
44
33
  }: {
45
34
  ctx: CanvasRenderingContext2D
46
35
  renderArgs: RenderArgsDeserialized & {
47
- samples: { name: string; color?: string }[]
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>
@@ -61,9 +52,11 @@ function makeImageData({
61
52
  const theme = createJBrowseTheme(configTheme)
62
53
  const colorForBase = getColorBaseMap(theme)
63
54
  const contrastForBase = getContrastBaseMap(theme)
64
- const sampleToRowMap = new Map(samples.map((s, i) => [s.name, i]))
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
- samples: { name: string; color?: string }[]
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
  )
@@ -13,7 +13,8 @@ const configSchema = ConfigurationSchema(
13
13
  * #slot
14
14
  */
15
15
  samples: {
16
- type: 'stringArray',
16
+ type: 'frozen',
17
+ description: 'string[] or {id:string,label:string,color?:string}[]',
17
18
  defaultValue: [],
18
19
  },
19
20
  /**
package/src/index.ts CHANGED
@@ -22,5 +22,5 @@ export default class MafViewerPlugin extends Plugin {
22
22
  MafAddTrackWorkflowF(pluginManager)
23
23
  }
24
24
 
25
- configure(pluginManager: PluginManager) {}
25
+ configure(_pluginManager: PluginManager) {}
26
26
  }