genoverse 3.2.0
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/.eslintrc.js +197 -0
- package/.github/workflows/test.yml +24 -0
- package/LICENSE.TXT +24 -0
- package/README.md +11 -0
- package/css/controlPanel.css +200 -0
- package/css/fileDrop.css +22 -0
- package/css/font-awesome.css +3 -0
- package/css/fullscreen.css +19 -0
- package/css/genoverse.css +466 -0
- package/css/karyotype.css +85 -0
- package/css/resizer.css +36 -0
- package/css/tooltips.css +26 -0
- package/css/trackControls.css +111 -0
- package/expanded.html +120 -0
- package/fontawesome/css/fontawesome.min.css +5 -0
- package/fontawesome/css/regular.min.css +5 -0
- package/fontawesome/css/solid.min.css +5 -0
- package/fontawesome/webfonts/fa-brands-400.ttf +0 -0
- package/fontawesome/webfonts/fa-brands-400.woff +0 -0
- package/fontawesome/webfonts/fa-brands-400.woff2 +0 -0
- package/fontawesome/webfonts/fa-regular-400.ttf +0 -0
- package/fontawesome/webfonts/fa-regular-400.woff +0 -0
- package/fontawesome/webfonts/fa-regular-400.woff2 +0 -0
- package/fontawesome/webfonts/fa-solid-900.ttf +0 -0
- package/fontawesome/webfonts/fa-solid-900.woff +0 -0
- package/fontawesome/webfonts/fa-solid-900.woff2 +0 -0
- package/help.pdf +0 -0
- package/i/sort_handle.png +0 -0
- package/index.html +68 -0
- package/index.js +83 -0
- package/jest.config.js +4 -0
- package/js/Genoverse.js +1681 -0
- package/js/Track/Controller/Sequence.js +17 -0
- package/js/Track/Controller/Stranded.js +73 -0
- package/js/Track/Controller.js +620 -0
- package/js/Track/Model/File/BAM.js +44 -0
- package/js/Track/Model/File/BED.js +116 -0
- package/js/Track/Model/File/GFF.js +40 -0
- package/js/Track/Model/File/VCF.js +101 -0
- package/js/Track/Model/File/WIG.js +67 -0
- package/js/Track/Model/File.js +36 -0
- package/js/Track/Model/Gene/Ensembl.js +22 -0
- package/js/Track/Model/Gene.js +5 -0
- package/js/Track/Model/Sequence/Ensembl.js +4 -0
- package/js/Track/Model/Sequence/Fasta.js +60 -0
- package/js/Track/Model/Sequence.js +50 -0
- package/js/Track/Model/SequenceVariation.js +41 -0
- package/js/Track/Model/Stranded.js +28 -0
- package/js/Track/Model/Transcript/Ensembl.js +67 -0
- package/js/Track/Model/Transcript.js +5 -0
- package/js/Track/Model.js +303 -0
- package/js/Track/View/Gene/Ensembl.js +46 -0
- package/js/Track/View/Gene.js +6 -0
- package/js/Track/View/Sequence/Variation.js +115 -0
- package/js/Track/View/Sequence.js +63 -0
- package/js/Track/View/Transcript/Ensembl.js +12 -0
- package/js/Track/View/Transcript.js +28 -0
- package/js/Track/View.js +566 -0
- package/js/Track/library/Chromosome.js +145 -0
- package/js/Track/library/File/BAM.js +30 -0
- package/js/Track/library/File/BED.js +24 -0
- package/js/Track/library/File/BIGBED.js +47 -0
- package/js/Track/library/File/BIGWIG.js +52 -0
- package/js/Track/library/File/GFF.js +9 -0
- package/js/Track/library/File/VCF.js +71 -0
- package/js/Track/library/File/WIG.js +5 -0
- package/js/Track/library/File.js +10 -0
- package/js/Track/library/Gene.js +37 -0
- package/js/Track/library/Graph/Bar.js +235 -0
- package/js/Track/library/Graph/Line.js +296 -0
- package/js/Track/library/Graph.js +355 -0
- package/js/Track/library/HighlightRegion.js +292 -0
- package/js/Track/library/Legend.js +224 -0
- package/js/Track/library/Scalebar.js +227 -0
- package/js/Track/library/Scaleline.js +91 -0
- package/js/Track/library/Static.js +78 -0
- package/js/Track/library/dbSNP.js +142 -0
- package/js/Track.js +632 -0
- package/js/genomes/grch37.js +990 -0
- package/js/genomes/grch38.js +990 -0
- package/js/genoverse.min.js +2 -0
- package/js/genoverse.min.js.map +1 -0
- package/js/lib/BWReader.js +578 -0
- package/js/lib/Base.js +145 -0
- package/js/lib/VCFReader.js +286 -0
- package/js/lib/dalliance/js/bam.js +494 -0
- package/js/lib/dalliance/js/bin.js +185 -0
- package/js/lib/dalliance/js/das.js +749 -0
- package/js/lib/dalliance/js/utils.js +370 -0
- package/js/lib/dalliance-lib.js +3594 -0
- package/js/lib/dalliance-lib.min.js +68 -0
- package/js/lib/jDataView.js +2 -0
- package/js/lib/jParser.js +192 -0
- package/js/lib/jquery-ui.js +8 -0
- package/js/lib/jquery.js +2 -0
- package/js/lib/jquery.mousehold.js +53 -0
- package/js/lib/jquery.mousewheel.js +84 -0
- package/js/lib/jquery.tipsy.js +258 -0
- package/js/lib/rtree.js +1 -0
- package/js/plugins/controlPanel.js +395 -0
- package/js/plugins/fileDrop.js +62 -0
- package/js/plugins/focusRegion.js +12 -0
- package/js/plugins/fullscreen.js +77 -0
- package/js/plugins/karyotype.js +210 -0
- package/js/plugins/resizer.js +45 -0
- package/js/plugins/tooltips.js +94 -0
- package/js/plugins/trackControls.js +143 -0
- package/package.json +43 -0
- package/test/View/__snapshots__/render-bar-graph.test.js.snap +111 -0
- package/test/View/__snapshots__/render-blocks.test.js.snap +105 -0
- package/test/View/__snapshots__/render-chromosome.test.js.snap +5 -0
- package/test/View/__snapshots__/render-highlights.test.js.snap +73 -0
- package/test/View/__snapshots__/render-insert-variants.test.js.snap +9 -0
- package/test/View/__snapshots__/render-labels.test.js.snap +241 -0
- package/test/View/__snapshots__/render-legends.test.js.snap +13 -0
- package/test/View/__snapshots__/render-line-graph.test.js.snap +349 -0
- package/test/View/__snapshots__/render-scalebar.test.js.snap +49 -0
- package/test/View/__snapshots__/render-scaleline.test.js.snap +31 -0
- package/test/View/__snapshots__/render-sequence.test.js.snap +23 -0
- package/test/View/__snapshots__/render-stranded.test.js.snap +5 -0
- package/test/View/__snapshots__/render-transcripts.test.js.snap +193 -0
- package/test/View/render-bar-graph.test.js +87 -0
- package/test/View/render-blocks.test.js +171 -0
- package/test/View/render-chromosome.test.js +40 -0
- package/test/View/render-highlights.test.js +67 -0
- package/test/View/render-insert-variants.test.js +11 -0
- package/test/View/render-labels.test.js +266 -0
- package/test/View/render-legends.test.js +31 -0
- package/test/View/render-line-graph.test.js +169 -0
- package/test/View/render-scalebar.test.js +36 -0
- package/test/View/render-scaleline.test.js +28 -0
- package/test/View/render-sequence.test.js +49 -0
- package/test/View/render-stranded.test.js +10 -0
- package/test/View/render-transcripts.test.js +165 -0
- package/test/create-and-destroy.test.js +63 -0
- package/test/track-ordering.test.js +514 -0
- package/test/track_config/__snapshots__/config-settings.test.js.snap +23 -0
- package/test/track_config/config-settings.test.js +321 -0
- package/test/track_config/zoom-level-settings.test.js +98 -0
- package/test/utils.js +80 -0
- package/utils/createGenome.js +52 -0
- package/utils/devServer.js +36 -0
- package/utils/expandedTemplate.html +46 -0
- package/utils/git-hooks/post-commit +9 -0
- package/utils/git-hooks/pre-commit +7 -0
- package/utils/git-hooks/setup +6 -0
- package/utils/makeExpanded.js +19 -0
- package/webpack.config.js +39 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
Genoverse.Track.Controller.Graph.Line = {
|
|
2
|
+
click: function () {
|
|
3
|
+
if (this.prop('showPopups')) {
|
|
4
|
+
this.prop('menus').hide(); // Hide first, because closeMenus causes fadeOut to happen, which doens't look great in this scenario
|
|
5
|
+
this.browser.closeMenus(this);
|
|
6
|
+
return Genoverse.Track.Controller.prototype.click.apply(this, arguments);
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
getClickedFeatures: function (x) {
|
|
11
|
+
var bounds = { x: x, y: 0, w: 1, h: 9e99 };
|
|
12
|
+
var tolerance = this.scale > 1 ? 0 : 1 / this.scale;
|
|
13
|
+
var xMid = bounds.x / this.scale;
|
|
14
|
+
var xRange = tolerance ? [ Math.floor(xMid - tolerance), Math.ceil(xMid + tolerance) ] : [ Math.floor(xMid), Math.floor(xMid) ];
|
|
15
|
+
var features = {};
|
|
16
|
+
|
|
17
|
+
this.featurePositions.search(bounds).forEach(function (f) {
|
|
18
|
+
if (!features[f.dataset]) {
|
|
19
|
+
features[f.dataset] = f;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return [
|
|
24
|
+
this.model.sortFeatures(Object.keys(features).map(function (k) {
|
|
25
|
+
return $.extend(true, {}, features[k], { clickedCoords: features[k].coords.filter(function (c) { return c[0] >= xRange[0] && c[0] <= xRange[1]; }) });
|
|
26
|
+
}))
|
|
27
|
+
];
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
populateMenu: function (features) {
|
|
31
|
+
if (!features.length || !features[0].clickedCoords.length) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
var start = features[0].clickedCoords[0][0];
|
|
36
|
+
var end = features[0].clickedCoords[features[0].clickedCoords.length - 1][0];
|
|
37
|
+
var avg = start !== end;
|
|
38
|
+
var menu = { title: features[0].chr + ':' + (start === end ? start : start + '-' + end) };
|
|
39
|
+
var m, values, i;
|
|
40
|
+
|
|
41
|
+
function getValues(coords) {
|
|
42
|
+
var vals = coords.map(function (c) { return c[1]; }).sort(function (a, b) { return a - b; });
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
avg : vals.reduce(function (n, v) { return n + v; }, 0) / vals.length,
|
|
46
|
+
min : vals[0],
|
|
47
|
+
max : vals[vals.length - 1]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (avg) {
|
|
52
|
+
if (features.length === 1) {
|
|
53
|
+
values = getValues(features[0].clickedCoords);
|
|
54
|
+
|
|
55
|
+
menu['Average value'] = values.avg;
|
|
56
|
+
menu['Min value'] = values.min;
|
|
57
|
+
menu['Max value'] = values.max;
|
|
58
|
+
} else {
|
|
59
|
+
menu = [ menu ];
|
|
60
|
+
|
|
61
|
+
for (i = 0; i < features.length; i++) {
|
|
62
|
+
values = getValues(features[i].clickedCoords);
|
|
63
|
+
m = { title: features[i].dataset };
|
|
64
|
+
m.Average = values.avg;
|
|
65
|
+
m.Min = values.min;
|
|
66
|
+
m.Max = values.max;
|
|
67
|
+
|
|
68
|
+
menu.push(m);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} else if (features.length === 1) {
|
|
72
|
+
menu.Value = features[0].clickedCoords[0][1];
|
|
73
|
+
} else {
|
|
74
|
+
for (i = 0; i < features.length; i++) {
|
|
75
|
+
menu[features[i].dataset] = features[i].clickedCoords[0][1];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return menu;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
Genoverse.Track.Model.Graph.Line = Genoverse.Track.Model.Graph.extend({
|
|
84
|
+
parseData: function (data, chr, start, end) {
|
|
85
|
+
var features = [];
|
|
86
|
+
var feature, x;
|
|
87
|
+
|
|
88
|
+
function getX(f) {
|
|
89
|
+
return typeof f.x !== 'undefined' ? f.x : f.start + (f.start === f.end ? 0 : (f.end - f.start + 1) / 2);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
data.sort(function (a, b) { return (a.start - b.start) || (a.x - b.x); });
|
|
93
|
+
|
|
94
|
+
for (var i = 0; i < data.length; i++) {
|
|
95
|
+
if (typeof data[i].y !== 'undefined' && !data[i].coords) {
|
|
96
|
+
x = getX(data[i]);
|
|
97
|
+
|
|
98
|
+
if (feature && feature.coords[feature.coords.length - 1][0] === x - 1) {
|
|
99
|
+
feature.coords.push([ x, data[i].y ]);
|
|
100
|
+
feature.end = x;
|
|
101
|
+
} else {
|
|
102
|
+
if (feature) {
|
|
103
|
+
features.push(feature);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
feature = $.extend({ coords: [[ x, data[i].y ]], start: x, end: x }, data[i]);
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
if (feature) {
|
|
110
|
+
features.push(feature);
|
|
111
|
+
feature = undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
features.push(data[i]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (feature) {
|
|
119
|
+
features.push(feature);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return this.base(features, chr, start, end);
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
insertFeature: function (feature) {
|
|
126
|
+
var datasets = this.prop('datasets');
|
|
127
|
+
|
|
128
|
+
if (feature.coords) {
|
|
129
|
+
feature.coords = feature.coords.map(function (c, i) { return c.length > 1 ? c : [ feature.start + i, c ]; }).filter(function (c) { return c[0] >= feature.start && c[0] <= feature.end; });
|
|
130
|
+
} else if (feature.y) {
|
|
131
|
+
feature.coords = [[ feature.start + (feature.start === feature.end ? 0 : (feature.end - feature.start + 1) / 2), feature.y ]];
|
|
132
|
+
} else {
|
|
133
|
+
feature.coords = [];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (datasets.length) {
|
|
137
|
+
feature.legend = feature.dataset;
|
|
138
|
+
feature.color = (datasets.filter(function (s) { return s.name === feature.dataset; })[0] || { color: this.color }).color;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
feature.id = feature.id || [ feature.chr, feature.start, feature.end, feature.dataset || '' ].join(':');
|
|
142
|
+
|
|
143
|
+
return this.base.apply(this, arguments);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
Genoverse.Track.View.Graph.Line = Genoverse.Track.View.Graph.extend({
|
|
148
|
+
featureHeight: 1,
|
|
149
|
+
|
|
150
|
+
positionFeatures: function (features, params) {
|
|
151
|
+
var scale = params.scale;
|
|
152
|
+
var yScale = this.track.getYScale();
|
|
153
|
+
var margin = this.prop('marginTop');
|
|
154
|
+
var zeroY = margin - this.prop('range')[0] * yScale;
|
|
155
|
+
var add = (scale > 1 ? scale / 2 : 0) - params.scaledStart;
|
|
156
|
+
|
|
157
|
+
function setCoords(c) {
|
|
158
|
+
return [ c[0] * scale + add, c[1] * yScale + zeroY ];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (var i = 0; i < features.length; i++) {
|
|
162
|
+
features[i].coordPositions = features[i].coords.map(setCoords);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
params.featureHeight = this.prop('height');
|
|
166
|
+
|
|
167
|
+
return this.base(features, params);
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
draw: function (features, featureContext, labelContext, scale) {
|
|
171
|
+
if (!features.length) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
var datasets = this.featureDataSets(features);
|
|
176
|
+
var height = this.prop('height');
|
|
177
|
+
var marginTop = this.prop('marginTop');
|
|
178
|
+
var marginBottom = this.prop('margin');
|
|
179
|
+
var baseline = Math.min(Math.max(marginTop, marginTop - this.prop('range')[0] * this.track.getYScale()), height - marginTop);
|
|
180
|
+
var binSize = scale < 1 ? Math.floor(1 / scale) : 0;
|
|
181
|
+
var set, conf, feature, coords, binnedFeatures, lastBinSize, j, k, binStart, bin, l, prevFeature, prevCoords;
|
|
182
|
+
|
|
183
|
+
var defaults = {
|
|
184
|
+
color : this.color,
|
|
185
|
+
fill : this.prop('fill'),
|
|
186
|
+
lineWidth : this.prop('lineWidth'),
|
|
187
|
+
globalAlpha : this.prop('globalAlpha')
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
for (var i = 0; i < datasets.list.length; i++) {
|
|
191
|
+
set = datasets.list[i].name;
|
|
192
|
+
conf = $.extend({}, defaults, datasets.list[i]);
|
|
193
|
+
|
|
194
|
+
for (j = 0; j < (datasets.features[set] || []).length; j++) {
|
|
195
|
+
feature = datasets.features[set][j];
|
|
196
|
+
coords = feature.coordPositions;
|
|
197
|
+
|
|
198
|
+
if (coords.length) {
|
|
199
|
+
if (binSize) {
|
|
200
|
+
binnedFeatures = [];
|
|
201
|
+
k = 0;
|
|
202
|
+
|
|
203
|
+
while (k < coords.length) {
|
|
204
|
+
binStart = feature.coords[k][0];
|
|
205
|
+
bin = [];
|
|
206
|
+
|
|
207
|
+
while (coords[k] && feature.coords[k][0] - binStart < binSize) {
|
|
208
|
+
bin.push(coords[k++]);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
l = bin.length;
|
|
212
|
+
bin = bin.reduce(function (arr, b) { arr[0] += b[0]; arr[1] += b[1]; return arr; }, [ 0, 0 ]);
|
|
213
|
+
bin[0] = Math.round(bin[0] / l);
|
|
214
|
+
|
|
215
|
+
if (binnedFeatures.length && bin[0] === binnedFeatures[binnedFeatures.length - 1][0]) {
|
|
216
|
+
binnedFeatures[binnedFeatures.length - 1][1] = (binnedFeatures[binnedFeatures.length - 1][1] * lastBinSize + bin[1]) / (lastBinSize + l);
|
|
217
|
+
} else {
|
|
218
|
+
binnedFeatures.push([ bin[0], bin[1] / l ]);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
lastBinSize = l;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
coords = binnedFeatures;
|
|
225
|
+
feature.binnedCoords = coords;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
featureContext.fillStyle = featureContext.strokeStyle = conf.color;
|
|
229
|
+
featureContext.lineWidth = conf.lineWidth;
|
|
230
|
+
|
|
231
|
+
if (conf.fill) {
|
|
232
|
+
featureContext.globalAlpha = conf.globalAlpha;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
featureContext.beginPath();
|
|
236
|
+
|
|
237
|
+
if (conf.fill) {
|
|
238
|
+
featureContext.moveTo(coords[0][0], baseline);
|
|
239
|
+
featureContext.lineTo.apply(featureContext, coords[0]);
|
|
240
|
+
} else {
|
|
241
|
+
featureContext.moveTo.apply(featureContext, coords[0]);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for (k = 1; k < coords.length; k++) {
|
|
245
|
+
featureContext.lineTo.apply(featureContext, coords[k]);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
featureContext.stroke();
|
|
249
|
+
|
|
250
|
+
if (conf.fill) {
|
|
251
|
+
featureContext.lineTo(coords[coords.length - 1][0], baseline);
|
|
252
|
+
featureContext.closePath();
|
|
253
|
+
featureContext.fill();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
prevFeature = j ? datasets.features[set][j - i] : undefined;
|
|
257
|
+
|
|
258
|
+
if (prevFeature && prevFeature.end === feature.start - 1) {
|
|
259
|
+
featureContext.beginPath();
|
|
260
|
+
|
|
261
|
+
prevCoords = (binSize ? prevFeature.binnedCoords : prevFeature.coordPositions).slice(-1);
|
|
262
|
+
|
|
263
|
+
featureContext.moveTo.apply(featureContext, prevCoords[0]);
|
|
264
|
+
featureContext.lineTo.apply(featureContext, coords[0]);
|
|
265
|
+
featureContext.stroke();
|
|
266
|
+
|
|
267
|
+
if (conf.fill) {
|
|
268
|
+
featureContext.lineTo(coords[0][0], baseline);
|
|
269
|
+
featureContext.lineTo(prevCoords[0][0], baseline);
|
|
270
|
+
|
|
271
|
+
featureContext.closePath();
|
|
272
|
+
featureContext.fill();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (conf.fill) {
|
|
277
|
+
featureContext.globalAlpha = 1;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Don't allow features to be drawn in the margins
|
|
284
|
+
featureContext.clearRect(0, 0, this.width, marginTop - 1);
|
|
285
|
+
featureContext.clearRect(0, height - marginBottom, this.width, marginBottom);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
Genoverse.Track.Graph.Line = Genoverse.Track.Graph.extend({
|
|
290
|
+
type : 'Line',
|
|
291
|
+
showPopups : true, // If true, clicking on the track will show popups. If false, popups will not appear.
|
|
292
|
+
fill : false,
|
|
293
|
+
lineWidth : 1,
|
|
294
|
+
model : Genoverse.Track.Model.Graph.Line,
|
|
295
|
+
view : Genoverse.Track.View.Graph.Line
|
|
296
|
+
});
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
// These are abstract classes, implemented by Graph.Bar and Graph.Line. They will not work properly on their own.
|
|
2
|
+
|
|
3
|
+
Genoverse.Track.Controller.Graph = Genoverse.Track.Controller.extend({
|
|
4
|
+
setYRange: function (min, max) {
|
|
5
|
+
if (this.browser.dragging) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (this.prop('showZeroY')) {
|
|
10
|
+
this.prop('range', [ Math.min(min, 0), Math.max(max, 0) ]);
|
|
11
|
+
} else {
|
|
12
|
+
this.prop('range', [ min, max ]);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
this.track.reset();
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
yMinMaxFromFeatures: function (features) {
|
|
19
|
+
var min = Infinity;
|
|
20
|
+
var max = -Infinity;
|
|
21
|
+
var i, j;
|
|
22
|
+
|
|
23
|
+
if (this.prop('type') === 'Line') {
|
|
24
|
+
for (i = 0; i < features.length; i++) {
|
|
25
|
+
for (j = 0; j < features[i].coords.length; j++) {
|
|
26
|
+
if (!isNaN(features[i].coords[j][1])) {
|
|
27
|
+
min = Math.min(min, features[i].coords[j][1]);
|
|
28
|
+
max = Math.max(max, features[i].coords[j][1]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
for (i = 0; i < features.length; i++) {
|
|
34
|
+
if (!isNaN(features[i].height)) {
|
|
35
|
+
min = Math.min(min, features[i].height);
|
|
36
|
+
max = Math.max(max, features[i].height);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
min = min === Infinity ? 0 : min;
|
|
42
|
+
max = max === -Infinity ? 0 : max;
|
|
43
|
+
|
|
44
|
+
return { min: min, max: max };
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
afterSetName: function () {
|
|
48
|
+
this.minLabelHeight = Math.max(this.minLabelHeight, this.prop('fontHeight') * 2 + this.prop('margin') + this.prop('marginTop')); // Minimum height that can contain axis labels for range[0] and range[1]
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
visibleFeatureHeight: function () {
|
|
52
|
+
if (this.prop('rescaleable') === 'auto') {
|
|
53
|
+
var yScale = this.track.getYScale();
|
|
54
|
+
var y = this.yMinMaxFromFeatures(this.model.findFeatures(this.browser.chr, this.browser.start, this.browser.end));
|
|
55
|
+
|
|
56
|
+
return Math.ceil(Math.max(yScale * (y.max - y.min), this.prop('hideEmpty') ? 0 : this.minLabelHeight));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return this.prop('height');
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
resize: function () {
|
|
63
|
+
var prevHeight = this.prop('height');
|
|
64
|
+
var rtn = this.base.apply(this, arguments);
|
|
65
|
+
var height = this.prop('height');
|
|
66
|
+
|
|
67
|
+
if (prevHeight !== height) {
|
|
68
|
+
if (this.prop('rescaleable') === true) {
|
|
69
|
+
var prevRange = this.prop('range');
|
|
70
|
+
var maxDP = Math.max.apply(null, prevRange.map(function (r) { return (r.toString().split('.')[1] || '').length; }));
|
|
71
|
+
var prevRangeSize = prevRange[1] - prevRange[0];
|
|
72
|
+
var rangeChange = Math.ceil((prevRangeSize * (height / prevHeight) - prevRangeSize) / 2);
|
|
73
|
+
|
|
74
|
+
this.setYRange(
|
|
75
|
+
parseFloat((prevRange[0] - rangeChange).toFixed(maxDP), 10),
|
|
76
|
+
parseFloat((prevRange[1] + rangeChange).toFixed(maxDP), 10)
|
|
77
|
+
);
|
|
78
|
+
} else {
|
|
79
|
+
this.track.reset();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
(this.prop('expander') || $()).hide();
|
|
84
|
+
(this.prop('resizer') || $()).removeClass('gv-resizer-expander');
|
|
85
|
+
|
|
86
|
+
return rtn;
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
autoResize: function () {
|
|
90
|
+
if (this.prop('rescaleable') === 'auto') {
|
|
91
|
+
var visibleFeatures = this.model.findFeatures(this.browser.chr, this.browser.start, this.browser.end);
|
|
92
|
+
|
|
93
|
+
if (visibleFeatures.length) {
|
|
94
|
+
var range = this.prop('range');
|
|
95
|
+
var y = this.yMinMaxFromFeatures(visibleFeatures);
|
|
96
|
+
|
|
97
|
+
if (y.min || y.max) {
|
|
98
|
+
var maxDP = Math.max.apply(null, range.map(function (r) { return (r.toString().split('.')[1] || '').length; }));
|
|
99
|
+
var round = Math.pow(10, maxDP);
|
|
100
|
+
var minY = parseFloat((Math.floor(y.min * round) / round).toFixed(maxDP), 10);
|
|
101
|
+
var maxY = parseFloat((Math.ceil(y.max * round) / round).toFixed(maxDP), 10);
|
|
102
|
+
|
|
103
|
+
if (this.prop('showZeroY')) {
|
|
104
|
+
minY = Math.min(minY, 0);
|
|
105
|
+
maxY = Math.max(maxY, 0);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (minY === maxY) {
|
|
109
|
+
maxY++;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (minY !== range[0] || maxY !== range[1]) {
|
|
113
|
+
return this.setYRange(minY, maxY);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
return this.base.apply(this, arguments);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
makeFirstImage: function () {
|
|
123
|
+
var controller = this;
|
|
124
|
+
|
|
125
|
+
return this.base.apply(this, arguments).done(function () {
|
|
126
|
+
controller.prop('yAxisPlaceholder').hide();
|
|
127
|
+
controller.prop('offsetContainer')
|
|
128
|
+
.prepend(controller.prop('guidelinesCanvas'))
|
|
129
|
+
.before(controller.prop('yAxisCanvas').removeClass('gv-loading'));
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
typeWrapper : function (func, args) { return (Genoverse.Track.Controller.Graph[this.prop('type')][func] || Genoverse.Track.Controller.prototype[func]).apply(this, args); },
|
|
134
|
+
click : function () { return this.typeWrapper('click', arguments); },
|
|
135
|
+
getClickedFeatures : function () { return this.typeWrapper('getClickedFeatures', arguments); },
|
|
136
|
+
populateMenu : function () { return this.typeWrapper('populateMenu', arguments); }
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
Genoverse.Track.Model.Graph = Genoverse.Track.Model.extend({
|
|
140
|
+
dataBuffer : { start: 1, end: 1 },
|
|
141
|
+
setLabelBuffer : $.noop,
|
|
142
|
+
sortFeatures : function (features) { return features.sort(function (a, b) { return a.start - b.start; }); }
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
Genoverse.Track.View.Graph = Genoverse.Track.View.extend({
|
|
146
|
+
featureMargin: {},
|
|
147
|
+
|
|
148
|
+
featureDataSets: function (features) {
|
|
149
|
+
var datasets = this.prop('datasets').concat({ name: '_default' });
|
|
150
|
+
var setNames = {};
|
|
151
|
+
var sets = {};
|
|
152
|
+
var i, set;
|
|
153
|
+
|
|
154
|
+
for (i = 0; i < datasets.length; i++) {
|
|
155
|
+
setNames[datasets[i].name] = true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (i = 0; i < features.length; i++) {
|
|
159
|
+
set = setNames[features[i].dataset] ? features[i].dataset : '_default';
|
|
160
|
+
|
|
161
|
+
sets[set] = sets[set] || [];
|
|
162
|
+
sets[set].push(features[i]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { list: datasets, features: sets };
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
Genoverse.Track.Graph = Genoverse.Track.extend({
|
|
170
|
+
controller : Genoverse.Track.Controller.Graph,
|
|
171
|
+
margin : 10, // Same as fontHeight - needed to allow axis labels for range[0] and range[1] to be drawn without being cut off by the edge of the image
|
|
172
|
+
invert : true,
|
|
173
|
+
yAxisLabels : undefined, // An array of numerical labels for the y-axis. Should not be configured manually if the track is resizable.
|
|
174
|
+
yRange : undefined, // An array of [ minY, maxY ] for the graph
|
|
175
|
+
showZeroY : true, // If true, 0 will always be included in auto-generated yRanges. If yRange is defined in configuration, this setting will be ignored.
|
|
176
|
+
globalAlpha : 1,
|
|
177
|
+
axesSettings : { axisColor: 'black', axisLabelColor: 'black', scaleLineColor: '#E5E5E5' },
|
|
178
|
+
datasets : [],
|
|
179
|
+
legend : true,
|
|
180
|
+
labels : false,
|
|
181
|
+
|
|
182
|
+
/*
|
|
183
|
+
* resizable and rescaleableY combine to define what happens when the track "resizes", as follows:
|
|
184
|
+
* resizable | rescaleableY | Effect
|
|
185
|
+
* --------- | ------------ | ------
|
|
186
|
+
* true | true | Users can change the track height, and doing so changes the y-axis range (y-axis range will change proportionally to track height change)
|
|
187
|
+
* true | 'auto' | Users can change the track height, and doing so does not change the y-axis range. However, the y-axis range will automatically change so that no peaks are cut off.
|
|
188
|
+
* true | false | Users can change the track height, and doing so does not change the y-axis range (peak heights will change proportionally to track height change)
|
|
189
|
+
* false | true | Like true/true
|
|
190
|
+
* false | 'auto' | Track height cannot be changed, but the y-axis range will automatically change so that no peaks are cut off
|
|
191
|
+
* false | false | Neither track height nor y-axis range can be changed, either by users or automatically
|
|
192
|
+
* 'auto' | true | Like false/'auto'
|
|
193
|
+
* 'auto' | 'auto' | Like false/'auto'
|
|
194
|
+
* 'auto' | false | Like false/'auto' (it is not possible to change a track's height such that no peaks are cut off without being able to change the y-axis range)
|
|
195
|
+
*/
|
|
196
|
+
resizable : true,
|
|
197
|
+
rescaleableY : 'auto',
|
|
198
|
+
|
|
199
|
+
setDefaults: function () {
|
|
200
|
+
this.range = this.yRange || [ 0, this.height ];
|
|
201
|
+
this.rescaleable = this.rescaleableY;
|
|
202
|
+
|
|
203
|
+
if ($.isPlainObject(this.margin)) {
|
|
204
|
+
if (this.invert) {
|
|
205
|
+
this.marginTop = this.margin.bottom;
|
|
206
|
+
this.margin = this.margin.top;
|
|
207
|
+
} else {
|
|
208
|
+
this.marginTop = this.margin.top;
|
|
209
|
+
this.margin = this.margin.bottom;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this.marginTop = typeof this.marginTop === 'number' ? this.marginTop : this.margin;
|
|
214
|
+
|
|
215
|
+
if (this.resizable === false) {
|
|
216
|
+
this.resizable = this.rescaleable;
|
|
217
|
+
} else if (this.resizable === 'auto') {
|
|
218
|
+
this.rescaleable = 'auto';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
this.base.apply(this, arguments);
|
|
222
|
+
|
|
223
|
+
if (this.legend && !this.datasets.length) {
|
|
224
|
+
this.legend = false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
this.height += this.marginTop;
|
|
228
|
+
this.initialHeight += this.marginTop;
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
setHeight: function (height) {
|
|
232
|
+
return this.base(height, true); // always force show
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
setMVC: function () {
|
|
236
|
+
var hadController = this.controller instanceof Genoverse.Track.Controller;
|
|
237
|
+
var rtn = this.base.apply(this, arguments);
|
|
238
|
+
|
|
239
|
+
if (!hadController) {
|
|
240
|
+
var scrollContainer = this.prop('scrollContainer');
|
|
241
|
+
|
|
242
|
+
this.yAxisPlaceholder = $('<div class="gv-image-container gv-loading">');
|
|
243
|
+
this.yAxisCanvas = $('<canvas class="gv-image-container gv-barchart-axis">').attr('width', this.width);
|
|
244
|
+
this.guidelinesCanvas = $('<canvas class="gv-image-container gv-barchart-guide">').attr('width', this.width);
|
|
245
|
+
|
|
246
|
+
if (this.disabled) {
|
|
247
|
+
this.yAxisCanvas.add(this.guidelinesCanvas).attr('height', 0);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
this.offsetContainer = $('<div class="gv-scroll-container-offset">')
|
|
251
|
+
.width(this.width)
|
|
252
|
+
.insertAfter(scrollContainer)
|
|
253
|
+
.append(scrollContainer)
|
|
254
|
+
.before(this.yAxisPlaceholder);
|
|
255
|
+
|
|
256
|
+
this.drawAxes();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return rtn;
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
afterSetMVC: function () {
|
|
263
|
+
// Never show the control to switch between auto-height and manual resizing, since its behaviour is not the same here as for standard tracks, due to interactions between resizable and rescaleableY.
|
|
264
|
+
(this.prop('heightToggler') || $()).addClass('gv-hidden');
|
|
265
|
+
(this.prop('resizer') || $()).off('click');
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
reset: function () {
|
|
269
|
+
this.drawAxes();
|
|
270
|
+
return this.base.apply(this, arguments);
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
enable: function () {
|
|
274
|
+
var wasDisabled = this.disabled;
|
|
275
|
+
var rtn = this.base.apply(this, arguments);
|
|
276
|
+
|
|
277
|
+
if (wasDisabled) {
|
|
278
|
+
this.drawAxes();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return rtn;
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
getYScale: function () {
|
|
285
|
+
var range = this.prop('range');
|
|
286
|
+
var yScale = (this.prop('height') - this.prop('margin') - this.prop('marginTop')) / (range[1] - range[0]);
|
|
287
|
+
|
|
288
|
+
return yScale;
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
drawAxes: function () {
|
|
292
|
+
if (this.prop('disabled')) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
var width = this.width;
|
|
297
|
+
var height = this.prop('height');
|
|
298
|
+
var invert = this.prop('invert');
|
|
299
|
+
var margin = this.prop('margin');
|
|
300
|
+
var marginTop = this.prop('marginTop');
|
|
301
|
+
var fontHeight = this.prop('fontHeight');
|
|
302
|
+
var range = this.prop('range');
|
|
303
|
+
var axesSettings = this.prop('axesSettings');
|
|
304
|
+
var yAxisLabels = this.prop('yAxisLabels');
|
|
305
|
+
var yScale = this.getYScale();
|
|
306
|
+
var axisContext = this.prop('yAxisCanvas').attr('height', height)[0].getContext('2d');
|
|
307
|
+
var linesContext = this.prop('guidelinesCanvas').attr('height', height)[0].getContext('2d');
|
|
308
|
+
var y, n, i, interval, maxDP;
|
|
309
|
+
|
|
310
|
+
if (!yAxisLabels) {
|
|
311
|
+
n = Math.floor((height - margin - marginTop) / (fontHeight * 2)); // number of labels that can be shown
|
|
312
|
+
interval = (range[1] - range[0]) / n; // label incrementor
|
|
313
|
+
yAxisLabels = [];
|
|
314
|
+
|
|
315
|
+
if (interval !== Math.round(interval)) { // floats
|
|
316
|
+
// Strenuously ensure that interval does not contain a floating point error.
|
|
317
|
+
// Assumes that values in range do not contain floating point errors.
|
|
318
|
+
maxDP = Math.max.apply(null, range.map(function (r) { return (r.toString().split('.')[1] || '').length; })) + 1;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
for (i = 0; i <= n; i++) {
|
|
322
|
+
yAxisLabels.push((range[0] + interval * i)[maxDP ? 'toFixed' : 'toString'](maxDP));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
var axisWidth = Math.max.apply(null, yAxisLabels.map(function (label) { return axisContext.measureText(label).width; })) + 10;
|
|
327
|
+
|
|
328
|
+
this.prop('offsetContainer').css('marginLeft', axisWidth).width(width - axisWidth);
|
|
329
|
+
this.prop('scrollContainer').css('marginLeft', -axisWidth);
|
|
330
|
+
|
|
331
|
+
this.prop('yAxisPlaceholder').width(axisWidth).show();
|
|
332
|
+
|
|
333
|
+
axisContext.fillStyle = axesSettings.axisColor;
|
|
334
|
+
axisContext.fillRect(axisWidth - 1, invert ? margin : marginTop, 1, height - margin - marginTop); // Vertical line
|
|
335
|
+
|
|
336
|
+
linesContext.fillStyle = axesSettings.scaleLineColor;
|
|
337
|
+
axisContext.fillStyle = axesSettings.axisLabelColor;
|
|
338
|
+
axisContext.textBaseline = 'middle';
|
|
339
|
+
axisContext.textAlign = 'right';
|
|
340
|
+
|
|
341
|
+
for (i = 0; i < yAxisLabels.length; i++) {
|
|
342
|
+
y = marginTop + (parseFloat(yAxisLabels[i], 10) - range[0]) * yScale;
|
|
343
|
+
y = invert ? height - y : y;
|
|
344
|
+
|
|
345
|
+
linesContext.fillRect(0, y, width, 1); // Horizontal line, indicating the y-position of a numerical value
|
|
346
|
+
axisContext.fillRect(axisWidth - 4, y, 4, 1); // Horizontal line, indicating the y-position of a numerical value
|
|
347
|
+
axisContext.fillText(yAxisLabels[i], axisWidth - 6, y); // The numerical value for the horizontal line
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Draw a horizontal line at y = 0
|
|
351
|
+
y = (-range[0] * yScale) + marginTop;
|
|
352
|
+
linesContext.fillStyle = axesSettings.axisColor;
|
|
353
|
+
linesContext.fillRect(0, invert ? height - y : y, width, 1);
|
|
354
|
+
}
|
|
355
|
+
});
|