jbrowse-plugin-mafviewer 1.0.3 → 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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ # v1.0.4
2
+
3
+ - Fix row height calculations
package/README.md CHANGED
@@ -6,3 +6,186 @@ This is a port of the JBrowse 1 plugin https://github.com/cmdcolin/mafviewer to
6
6
  JBrowse 2
7
7
 
8
8
  ![](img/1.png)
9
+
10
+ ## Demo
11
+
12
+ https://jbrowse.org/code/jb2/main/?config=%2Fdemos%2Fmafviewer%2Fhg38%2Fdistconfig.json&session=share-O3sxhB3iS2&password=8Ysiv
13
+
14
+ ## GUI usage (e.g. in JBrowse Desktop)
15
+
16
+ This short screenshot workflow shows how you can load your own custom MAF files
17
+ via the GUI
18
+
19
+ First install the plugin via the plugin store
20
+
21
+ ![](img/3.png)
22
+
23
+ Then use the custom "Add track workflow"
24
+
25
+ ![](img/2.png)
26
+
27
+ ## Manual config entry
28
+
29
+ ### Add plugin to your jbrowse 2 config.json
30
+
31
+ ```json
32
+ {
33
+ "plugins": [
34
+ {
35
+ "name": "MafViewer",
36
+ "url": "https://unpkg.com/jbrowse-plugin-mafviewer/dist/jbrowse-plugin-mafviewer.umd.production.min.js"
37
+ }
38
+ ]
39
+ }
40
+ ```
41
+
42
+ ### Example MafTabixAdapter config
43
+
44
+ The MafTabix track is created according to
45
+
46
+ ```json
47
+ {
48
+ "type": "MafTrack",
49
+ "trackId": "chrI.bed",
50
+ "name": "chrI.bed",
51
+ "adapter": {
52
+ "type": "MafTabixAdapter",
53
+ "samples": ["ce10", "cb4", "caeSp111", "caeRem4", "caeJap4", "caePb3"],
54
+ "bedGzLocation": {
55
+ "uri": "chrI.bed.gz"
56
+ },
57
+ "index": {
58
+ "location": {
59
+ "uri": "chrI.bed.gz.tbi"
60
+ }
61
+ }
62
+ },
63
+ "assemblyNames": ["c_elegans"]
64
+ }
65
+ ```
66
+
67
+ ### Example BigMafAdapter config
68
+
69
+ ```json
70
+ {
71
+ "type": "MafTrack",
72
+ "trackId": "bigMaf",
73
+ "name": "bigMaf (chr22_KI270731v1_random)",
74
+ "adapter": {
75
+ "type": "BigMafAdapter",
76
+ "samples": [
77
+ "hg38",
78
+ "panTro4",
79
+ "rheMac3",
80
+ "mm10",
81
+ "rn5",
82
+ "canFam3",
83
+ "monDom5"
84
+ ],
85
+ "bigBedLocation": {
86
+ "uri": "bigMaf.bb"
87
+ }
88
+ },
89
+ "assemblyNames": ["hg38"]
90
+ }
91
+ ```
92
+
93
+ ### Example with customized sample names and colors
94
+
95
+ ```json
96
+ {
97
+ "trackId": "MAF",
98
+ "name": "example",
99
+ "type": "MafTrack",
100
+ "assemblyNames": ["hg38"],
101
+ "adapter": {
102
+ "type": "MafTabixAdapter",
103
+ "bedGzLocation": {
104
+ "uri": "data.txt.gz"
105
+ },
106
+ "index": {
107
+ "location": {
108
+ "uri": "data.txt.gz.tbi"
109
+ }
110
+ },
111
+ "samples": [
112
+ {
113
+ "id": "hg38",
114
+ "label": "Human",
115
+ "color": "rgba(255,255,255,0.7)"
116
+ },
117
+ {
118
+ "id": "panTro4",
119
+ "label": "Chimp",
120
+ "color": "rgba(255,0,0,0.7)"
121
+ },
122
+ {
123
+ "id": "gorGor3",
124
+ "label": "Gorilla",
125
+ "color": "rgba(0,0,255,0.7)"
126
+ },
127
+ {
128
+ "id": "ponAbe2",
129
+ "label": "Orangutan",
130
+ "color": "rgba(255,255,255,0.7)"
131
+ }
132
+ ]
133
+ }
134
+ }
135
+ ```
136
+
137
+ The samples array is either `string[]|{id:string,label:string,color?:string}[]`
138
+
139
+ ## Prepare data
140
+
141
+ This is the same as the jbrowse 1 mafviewer plugin (currently the similar to
142
+ the). This plugin supports two formats
143
+
144
+ 1. BigMaf format, which can be created following UCSC guidelines
145
+
146
+ 2. MAF tabix based format, based on a custom BED created via conversion tools in
147
+ this repo.
148
+
149
+ The choice between the two is your convenience. BigMaf is a "standard" UCSC
150
+ format, basically just a specialized BigBed, so it requires JBrowse 1.14.0 or
151
+ newer for it's BigBed support. The custom BED format only requires JBrowse
152
+ 1.12.3 or newer, so therefore some slightly older JBrowse versions can support
153
+ it.
154
+
155
+ _Note: Both formats start with a MAF as input, and note that your MAF file
156
+ should contain the species name and chromosome name e.g. hg38.chr1 in the
157
+ sequence identifiers._
158
+
159
+ ### Preparing BigMaf
160
+
161
+ Follow instructions from https://genome.ucsc.edu/FAQ/FAQformat.html#format9.3
162
+ and set the storeType of your track as MAFViewer/Store/SeqFeature/BigMaf
163
+
164
+ ### Preparing the tabix BED format
165
+
166
+ Start by converting the MAF into a pseudo-BED format using the maf2bed tool
167
+
168
+ ```bash
169
+ # from https://github.com/cmdcolin/maf2bed
170
+ cargo install maf2bed
171
+ cat file.maf | maf2bed hg38 | bgzip > out.bed
172
+ tabix -p bed out.bed.gz
173
+ ```
174
+
175
+ The second argument to maf2bed is the genome version e.g. hg38 used for the main
176
+ species in the MAF (if your MAF comes from a pipeline like Ensembl or UCSC, the
177
+ identifiers in the MAF file will say something like hg38.chr1, therefore, the
178
+ argument to maf2bed should just be hg38 to remove hg38 part of the identifier.
179
+ if your MAF file does not include the species name as part of the identifier,
180
+ you should add the species into them the those scaffold/chromosome e.g. create
181
+ hg38.chr1 if it was just chr1 before)
182
+
183
+ If all is well, your BED file should have 6 columns, with
184
+ `chr, start, end, id, score, alignment_data`, where `alignment_data` is
185
+ separated between each species by `;` and each field in the alignment is
186
+ separated by `:`.
187
+
188
+ ### Footnote
189
+
190
+ If you can't use the `cargo install maf2bed` binary, there is a `bin/maf2bed.pl`
191
+ perl version of it in this repo
@@ -4,7 +4,7 @@
4
4
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.JBrowsePluginMafViewer = {}, global.JBrowseExports["@jbrowse/core/Plugin"], global.JBrowseExports["@jbrowse/core/pluggableElementTypes"], global.JBrowseExports["@jbrowse/core/configuration"], global.JBrowseExports["@jbrowse/core/data_adapters/BaseAdapter"], global.JBrowseExports["mobx-state-tree"], global.JBrowseExports["@jbrowse/core/util"], global.JBrowseExports["@jbrowse/core/util/rxjs"], global.JBrowseExports.react, global.JBrowseExports["mobx-react"], global.JBrowseExports["@mui/material"], global.JBrowseExports["@jbrowse/core/ui"], global.JBrowseExports["tss-react/mui"]));
5
5
  })(this, (function (exports, Plugin, pluggableElementTypes, configuration, BaseAdapter, mobxStateTree, util, rxjs, React, mobxReact, material, ui, mui) { 'use strict';
6
6
 
7
- var version = "1.0.3";
7
+ var version = "1.0.4";
8
8
 
9
9
  const configSchema$2 = configuration.ConfigurationSchema('BigMafAdapter', {
10
10
  /**
@@ -649,28 +649,19 @@
649
649
  const alignments = {};
650
650
  const blocks2 = [];
651
651
  for (const block of blocks) {
652
- if (block[0] === 's') {
653
- if (!aln) {
654
- aln = block.split(/ +/)[6];
655
- alns.push(aln);
652
+ if (block.startsWith('s')) {
653
+ if (aln) {
654
+ alns.push(block.split(/ +/)[6]);
656
655
  blocks2.push(block);
657
656
  }
658
657
  else {
659
- alns.push(block.split(/ +/)[6]);
658
+ aln = block.split(/ +/)[6];
659
+ alns.push(aln);
660
660
  blocks2.push(block);
661
661
  }
662
662
  }
663
663
  }
664
- const alns2 = alns.map(() => '');
665
- if (aln) {
666
- for (let i = 0; i < aln?.length; i++) {
667
- if (aln[i] !== '-') {
668
- for (let j = 0; j < alns.length; j++) {
669
- alns2[j] += alns[j][i];
670
- }
671
- }
672
- }
673
- }
664
+ // eslint-disable-next-line unicorn/no-for-loop
674
665
  for (let i = 0; i < blocks2.length; i++) {
675
666
  const elt = blocks2[i];
676
667
  const ad = elt.split(/ +/);
@@ -683,7 +674,7 @@
683
674
  srcSize: +ad[2],
684
675
  strand: ad[3] === '+' ? 1 : -1,
685
676
  unknown: +ad[4],
686
- data: alns2[i],
677
+ data: alns[i],
687
678
  };
688
679
  }
689
680
  observer.next(new util.SimpleFeature({
@@ -692,7 +683,7 @@
692
683
  start: feature.get('start'),
693
684
  end: feature.get('end'),
694
685
  refName: feature.get('refName'),
695
- seq: alns2[0],
686
+ seq: alns[0],
696
687
  alignments: alignments,
697
688
  },
698
689
  }));
@@ -802,6 +793,10 @@
802
793
  * #property
803
794
  */
804
795
  rowProportion: 0.8,
796
+ /**
797
+ * #property
798
+ */
799
+ showAllLetters: false,
805
800
  }))
806
801
  .volatile(() => ({
807
802
  prefersOffset: true,
@@ -819,6 +814,12 @@
819
814
  setRowProportion(n) {
820
815
  self.rowProportion = n;
821
816
  },
817
+ /**
818
+ * #action
819
+ */
820
+ setShowAllLetters(f) {
821
+ self.showAllLetters = f;
822
+ },
822
823
  }))
823
824
  .views(self => ({
824
825
  /**
@@ -826,12 +827,9 @@
826
827
  */
827
828
  get samples() {
828
829
  const r = self.adapterConfig.samples;
829
- if (isStrs(r)) {
830
- return r.map(elt => ({ id: elt, label: elt, color: undefined }));
831
- }
832
- else {
833
- return r;
834
- }
830
+ return isStrs(r)
831
+ ? r.map(elt => ({ id: elt, label: elt, color: undefined }))
832
+ : r;
835
833
  },
836
834
  /**
837
835
  * #getter
@@ -858,12 +856,14 @@
858
856
  * #method
859
857
  */
860
858
  renderProps() {
859
+ const { showAllLetters, rendererConfig, samples, rowHeight, rowProportion, } = self;
861
860
  return {
862
861
  ...superRenderProps(),
863
- config: self.rendererConfig,
864
- samples: self.samples,
865
- rowHeight: self.rowHeight,
866
- rowProportion: self.rowProportion,
862
+ config: rendererConfig,
863
+ samples,
864
+ rowHeight,
865
+ rowProportion,
866
+ showAllLetters,
867
867
  };
868
868
  },
869
869
  /**
@@ -881,11 +881,20 @@
881
881
  ]);
882
882
  },
883
883
  },
884
+ {
885
+ label: 'Show all letters',
886
+ type: 'checkbox',
887
+ checked: self.showAllLetters,
888
+ onClick: () => {
889
+ self.setShowAllLetters(!self.showAllLetters);
890
+ },
891
+ },
884
892
  ];
885
893
  },
886
894
  };
887
895
  })
888
896
  .actions(self => {
897
+ // eslint-disable-next-line @typescript-eslint/unbound-method
889
898
  const { renderSvg: superRenderSvg } = self;
890
899
  return {
891
900
  /**
@@ -904,15 +913,14 @@
904
913
  return React.createElement("rect", { ...props, fill: color });
905
914
  };
906
915
 
907
- const ColorLegend = mobxReact.observer(function ({ model, labelWidth, }) {
916
+ const ColorLegend = mobxReact.observer(function ({ model, labelWidth, svgFontSize, }) {
908
917
  const { samples, rowHeight } = model;
909
- const svgFontSize = Math.min(rowHeight, 10);
910
918
  const canDisplayLabel = rowHeight >= 10;
911
919
  const boxHeight = Math.min(20, rowHeight);
912
920
  return samples ? (React.createElement(React.Fragment, null,
913
- samples.map((sample, idx) => (React.createElement(RectBg, { key: `${sample.id}-${idx}`, y: idx * rowHeight + 1, x: 0, width: labelWidth + 5, height: boxHeight, color: sample.color }))),
921
+ samples.map((sample, idx) => (React.createElement(RectBg, { key: `${sample.id}-${idx}`, y: idx * rowHeight, x: 0, width: labelWidth + 5, height: boxHeight, color: sample.color }))),
914
922
  canDisplayLabel
915
- ? samples.map((sample, idx) => (React.createElement("text", { key: `${sample.id}-${idx}`, y: idx * rowHeight + 14, x: 2, fontSize: svgFontSize }, sample.label)))
923
+ ? samples.map((sample, idx) => (React.createElement("text", { key: `${sample.id}-${idx}`, y: idx * rowHeight + rowHeight / 2, dominantBaseline: "middle", x: 2, fontSize: svgFontSize }, sample.label)))
916
924
  : null)) : null;
917
925
  });
918
926
 
@@ -935,14 +943,14 @@
935
943
  const YScaleBars = mobxReact.observer(function (props) {
936
944
  const { model } = props;
937
945
  const { rowHeight, samples } = model;
938
- const svgFontSize = Math.min(rowHeight, 12);
946
+ const svgFontSize = Math.min(Math.max(rowHeight, 10), 14);
939
947
  const canDisplayLabel = rowHeight >= 10;
940
948
  const minWidth = 20;
941
949
  const labelWidth = Math.max(...(samples
942
950
  .map(s => util.measureText(s.label, svgFontSize))
943
951
  .map(width => (canDisplayLabel ? width : minWidth)) || [0]));
944
952
  return (React.createElement(Wrapper, { ...props },
945
- React.createElement(ColorLegend, { model: model, labelWidth: labelWidth })));
953
+ React.createElement(ColorLegend, { model: model, labelWidth: labelWidth, svgFontSize: svgFontSize })));
946
954
  });
947
955
 
948
956
  const LinearMafDisplay = mobxReact.observer(function (props) {
@@ -994,18 +1002,19 @@
994
1002
  };
995
1003
  }
996
1004
  function makeImageData({ ctx, renderArgs, }) {
997
- const { regions, bpPerPx, rowHeight, theme: configTheme, samples, rowProportion, } = renderArgs;
1005
+ const { regions, bpPerPx, rowHeight, showAllLetters, theme: configTheme, samples, rowProportion, } = renderArgs;
998
1006
  const [region] = regions;
999
1007
  const features = renderArgs.features;
1000
- const h = rowHeight;
1008
+ const h = rowHeight * rowProportion;
1001
1009
  const theme = ui.createJBrowseTheme(configTheme);
1002
1010
  const colorForBase = getColorBaseMap(theme);
1003
1011
  const contrastForBase = getContrastBaseMap(theme);
1004
1012
  const sampleToRowMap = new Map(samples.map((s, i) => [s.id, i]));
1005
1013
  const scale = 1 / bpPerPx;
1006
1014
  const f = 0.4;
1007
- const h2 = h * rowProportion;
1008
- const offset = h2 / 2;
1015
+ const h2 = rowHeight / 2;
1016
+ const hp2 = h / 2;
1017
+ const offset = (rowHeight - h) / 2;
1009
1018
  // sample as alignments
1010
1019
  ctx.font = 'bold 10px Courier New,monospace';
1011
1020
  for (const feature of features.values()) {
@@ -1019,7 +1028,7 @@
1019
1028
  if (row === undefined) {
1020
1029
  throw new Error(`unknown sample encountered: ${sample}`);
1021
1030
  }
1022
- const t = h * row;
1031
+ const t = rowHeight * row;
1023
1032
  // gaps
1024
1033
  ctx.beginPath();
1025
1034
  ctx.fillStyle = 'black';
@@ -1034,29 +1043,31 @@
1034
1043
  }
1035
1044
  }
1036
1045
  ctx.stroke();
1037
- // matches
1038
- ctx.beginPath();
1039
- ctx.fillStyle = 'lightgrey';
1040
- for (let i = 0, o = 0; i < alignment.length; i++) {
1041
- if (seq[i] !== '-') {
1042
- const c = alignment[i];
1043
- const l = leftPx + scale * o;
1044
- if (seq[i] === c && c !== '-') {
1045
- ctx.rect(l, offset + t, scale + f, h2);
1046
+ if (!showAllLetters) {
1047
+ // matches
1048
+ ctx.beginPath();
1049
+ ctx.fillStyle = 'lightgrey';
1050
+ for (let i = 0, o = 0; i < alignment.length; i++) {
1051
+ if (seq[i] !== '-') {
1052
+ const c = alignment[i];
1053
+ const l = leftPx + scale * o;
1054
+ if (seq[i] === c && c !== '-') {
1055
+ ctx.rect(l, offset + t, scale + f, h);
1056
+ }
1057
+ o++;
1046
1058
  }
1047
- o++;
1048
1059
  }
1060
+ ctx.fill();
1049
1061
  }
1050
- ctx.fill();
1051
1062
  // mismatches
1052
1063
  for (let i = 0, o = 0; i < alignment.length; i++) {
1053
1064
  const c = alignment[i];
1054
1065
  if (seq[i] !== '-') {
1055
- if (seq[i] !== c && c !== '-') {
1066
+ if ((showAllLetters || seq[i] !== c) && c !== '-') {
1056
1067
  const l = leftPx + scale * o;
1057
1068
  ctx.fillStyle =
1058
- colorForBase[c] ?? 'purple';
1059
- ctx.fillRect(l, offset + t, scale + f, h2);
1069
+ colorForBase[c] ?? 'black';
1070
+ ctx.fillRect(l, offset + t, scale + f, h);
1060
1071
  }
1061
1072
  o++;
1062
1073
  }
@@ -1069,9 +1080,9 @@
1069
1080
  const l = leftPx + scale * o;
1070
1081
  const offset = (scale - charSize.w) / 2 + 1;
1071
1082
  const c = alignment[i];
1072
- if (seq[i] !== c && c !== '-') {
1073
- ctx.fillStyle = contrastForBase[c] ?? 'black';
1074
- ctx.fillText(origAlignment[i], l + offset, h2 + t + 3);
1083
+ if ((showAllLetters || seq[i] !== c) && c !== '-') {
1084
+ ctx.fillStyle = contrastForBase[c] ?? 'white';
1085
+ ctx.fillText(origAlignment[i], l + offset, hp2 + t + 3);
1075
1086
  }
1076
1087
  o++;
1077
1088
  }
@@ -1092,7 +1103,7 @@
1092
1103
  if (row === undefined) {
1093
1104
  throw new Error(`unknown sample encountered: ${sample}`);
1094
1105
  }
1095
- const t = h * row;
1106
+ const t = rowHeight * row;
1096
1107
  ctx.beginPath();
1097
1108
  ctx.fillStyle = 'purple';
1098
1109
  for (let i = 0, o = 0; i < alignment.length; i++) {
@@ -1103,11 +1114,11 @@
1103
1114
  }
1104
1115
  i++;
1105
1116
  }
1106
- if (ins.length) {
1107
- const l = leftPx + scale * o - 2;
1108
- ctx.rect(l, offset + t, 2, h2);
1109
- ctx.rect(l - 2, offset + t, 6, 1);
1110
- ctx.rect(l - 2, offset + t + h2, 6, 1);
1117
+ if (ins.length > 0) {
1118
+ const l = leftPx + scale * o - 1;
1119
+ ctx.rect(l, offset + t + 1, 1, h - 1);
1120
+ ctx.rect(l - 2, offset + t, 5, 1);
1121
+ ctx.rect(l - 2, offset + t + h - 1, 5, 1);
1111
1122
  }
1112
1123
  o++;
1113
1124
  }
@@ -1129,7 +1140,7 @@
1129
1140
  async render(renderProps) {
1130
1141
  const { regions, bpPerPx, samples, rowHeight } = renderProps;
1131
1142
  const [region] = regions;
1132
- const height = samples.length * rowHeight;
1143
+ const height = samples.length * rowHeight + 100;
1133
1144
  const width = (region.end - region.start) / bpPerPx;
1134
1145
  const features = await this.getFeatures(renderProps);
1135
1146
  const res = await util.renderToAbstractCanvas(width, height, renderProps, ctx => makeImageData({
@@ -1240,19 +1251,7 @@
1240
1251
  const data = feature.get('field5').split(',');
1241
1252
  const alignments = {};
1242
1253
  const alns = data.map(elt => elt.split(':')[5]);
1243
- // const aln = alns[0]
1244
- // const alns2 = data.map(() => '')
1245
- // remove extraneous data in other alignments
1246
- // reason being: cannot represent missing data in main species that are in others)
1247
- // for (let i = 0; i < aln.length; i++) {
1248
- // if (aln[i] !== '-') {
1249
- // for (let j = 0; j < data.length; j++) {
1250
- // alns2[j] += alns[j][i]
1251
- // }
1252
- // }
1253
- // }
1254
- for (let j = 0; j < data.length; j++) {
1255
- const elt = data[j];
1254
+ for (const [j, elt] of data.entries()) {
1256
1255
  const ad = elt.split(':');
1257
1256
  const [org, chr] = ad[0].split('.');
1258
1257
  alignments[org] = {
@@ -1313,6 +1312,7 @@
1313
1312
  const [error, setError] = React.useState();
1314
1313
  const [trackName, setTrackName] = React.useState('MAF track');
1315
1314
  const [choice, setChoice] = React.useState('BigMafAdapter');
1315
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1316
1316
  const rootModel = mobxStateTree.getRoot(model);
1317
1317
  return (React.createElement(material.Paper, { className: classes.paper },
1318
1318
  React.createElement(material.Paper, null,
@@ -1396,9 +1396,9 @@
1396
1396
  }
1397
1397
 
1398
1398
  async function renderSvg(self, opts, superRenderSvg) {
1399
- const { height } = self;
1399
+ const { height, id } = self;
1400
1400
  const { offsetPx, width } = util.getContainingView(self);
1401
- const clipid = `mafclip-${self.id}`;
1401
+ const clipid = `mafclip-${id}`;
1402
1402
  return (React.createElement(React.Fragment, null,
1403
1403
  React.createElement("defs", null,
1404
1404
  React.createElement("clipPath", { id: clipid },