Visualization infrastructure: (a) Backbone-ify cache and data managers; (b) push parameter value sampling code into Tool objects. Pack scripts.

This commit is contained in:
Jeremy Goecks
2012-06-07 16:47:06 -04:00
parent 9101867e13
commit f8f06c3706
10 changed files with 641 additions and 319 deletions
+36 -1
View File
@@ -63,13 +63,48 @@ var Tool = BaseModel.extend({
* A tool input.
*/
var ToolInput = Backbone.RelationalModel.extend({
defaults: {
name: null,
type: null
},
initialize: function() {
this.attributes.html = unescape(this.attributes.html);
}
},
/**
* Returns samples from a tool input.
*/
get_samples: function(num_samples) {
var
type = this.get('type'),
samples = []
if (type === 'number') {
samples = d3.scale.linear().domain([this.get('min'), this.get('max')]).ticks(num_samples);
}
else if (type === 'select') {
samples = _.map(this.get('options'), function(option) {
return option[0];
});
}
return new ParamSamples({
param: this,
samples: samples
});
}
});
/**
* A tool and parameter samples.
*/
var ParamSamples = Backbone.Model.extend({
defaults: {
param: null,
samples: null
}
})
/**
* Wrap collection of tools for fast access/manipulation.
*/
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
var ToolParameterTree=Backbone.Model.extend({defaults:{tool:null,samples:4},initialize:function(d){var c=this.get("tool"),b=this.get("samples"),a=c.get("inputs").filter(function(h){return(["number","select"].indexOf(h.get("type"))!==-1)}),f=_.map(a,function(h){return h.get("name")});sampling=_.map(a,function(h){var i=h.get("type");if(i==="number"){return d3.scale.linear().domain([h.get("min"),h.get("max")]).ticks(b)}else{if(i==="select"){return _.map(h.get("options"),function(j){return j[0]})}}});var g=function(j,h,i){if(h.length-1===i){return _.map(h[i],function(k){return{name:k}})}return _.map(h[i],function(k){return{name:j[i]+":"+k,children:g(j,h,i+1)}})};var e={name:"Parameter Tree for "+c.get("name"),children:g(f,sampling,0)};this.set("valid_inputs",a);this.set("tree_data",e)}});var TileView=Backbone.View.extend({});var ToolParameterTreeView=Backbone.View.extend({className:"paramamonster",initialize:function(a){this.model=a.model},render:function(){var e=960,b=2000;var a=d3.layout.cluster().size([b,e-160]);var d=d3.svg.diagonal().projection(function(i){return[i.y,i.x]});var h=d3.select(this.$el[0]).append("svg").attr("width",e).attr("height",b).append("g").attr("transform","translate(80, 0)");var c=a.nodes(this.model.get("tree_data"));var g=h.selectAll("path.link").data(a.links(c)).enter().append("path").attr("class","link").attr("d",d);var f=h.selectAll("g.node").data(c).enter().append("g").attr("class","node").attr("transform",function(i){return"translate("+i.y+","+i.x+")"});f.append("circle").attr("r",4.5);f.append("text").attr("dx",function(i){return i.children?-8:8}).attr("dy",3).attr("text-anchor",function(i){return i.children?"end":"start"}).text(function(i){return i.name})}});
var ToolParameterTree=Backbone.Model.extend({defaults:{tool:null,params:null,num_samples:4},initialize:function(d){var c=this.get("tool"),b=this.get("num_samples"),f=c.get("inputs").map(function(h){return h.get_samples(b)}),a=_.filter(f,function(h){return h.get("samples").length!==0});var g=function(j,h){var m=j[h],l=m.get("param"),k=l.get("name"),i=m.get("samples");if(j.length-1===h){return _.map(i,function(n){return{name:k+"="+n,param:l}})}return _.map(i,function(n){return{name:k+"="+n,param:l,children:g(a,h+1)}})};var e={name:"Parameter Tree for "+c.get("name"),children:g(a,0)};this.set("params",_.map(f,function(h){return h.get("param")}));this.set("tree_data",e)}});var Tile=Backbone.Model.extend({defaults:{track:null,index:null,region:null,resolution:null,data:null,stale:null,html_elt:null},initialize:function(a){}});var Track=Backbone.Model.extend({defaults:{dataset:null}});var FeatureTrack=Track.extend({defaults:{track:null},draw_tile:function(n,r,p,s,d,j,c){var q=this,b=r.canvas,y=d.get("start"),a=d.get("end"),D=25,e=this.left_offset;if(p==="summary_tree"||p==="Histogram"){if(n.dataset_type!=="summary_tree"){var k=this.get_summary_tree_data(n.data,y,a,200);if(n.max){k.max=n.max}n=k}var A=new painters.SummaryTreePainter(n,y,a,this.prefs);A.draw(r,b.width,b.height,j);return new SummaryTreeTile(q,tile_index,s,b,n.data,n.max)}var h=[],o=this.slotters[j].slots;all_slotted=true;if(n.data){var l=this.filters_manager.filters;for(var t=0,v=n.data.length;t<v;t++){var g=n.data[t];var u=false;var m;for(var x=0,C=l.length;x<C;x++){m=l[x];m.update_attrs(g);if(!m.keep(g)){u=true;break}}if(!u){h.push(g);if(!(g[0] in o)){all_slotted=false}}}}var B=(this.filters_manager.alpha_filter?new FilterScaler(this.filters_manager.alpha_filter):null);var z=(this.filters_manager.height_filter?new FilterScaler(this.filters_manager.height_filter):null);var A=new (this.painter)(h,y,a,this.prefs,p,B,z,c);var w=null;r.fillStyle=this.prefs.block_color;r.font=r.canvas.manager.default_font;r.textAlign="right";if(n.data){w=A.draw(r,b.width,b.height,j,o);w.translation=-e}return new FeatureTrackTile(q,tile_index,s,b,n.data,j,p,n.message,all_slotted,w)}});var TileView=Backbone.View.extend({});var ToolParameterTreeView=Backbone.View.extend({className:"tool-parameter-tree",initialize:function(a){this.model=a.model},render:function(){var e=960,b=2000;var a=d3.layout.cluster().size([b,e-160]);var d=d3.svg.diagonal().projection(function(i){return[i.y,i.x]});var h=d3.select(this.$el[0]).append("svg").attr("width",e).attr("height",b).append("g").attr("transform","translate(80, 0)");var c=a.nodes(this.model.get("tree_data"));var g=h.selectAll("path.link").data(a.links(c)).enter().append("path").attr("class","link").attr("d",d);var f=h.selectAll("g.node").data(c).enter().append("g").attr("class","node").attr("transform",function(i){return"translate("+i.y+","+i.x+")"});f.on("click",function(k,j){console.log(k,j)});f.append("circle").attr("r",4.5);f.append("text").attr("dx",function(i){return i.children?-8:8}).attr("dy",3).attr("text-anchor",function(i){return i.children?"end":"start"}).text(function(i){return i.name})}});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+183 -46
View File
@@ -3,10 +3,15 @@
* genomic visualization.
*/
/**
* --- Models ---
*/
var ToolParameterTree = Backbone.Model.extend({
defaults: {
tool: null,
samples: 4
params: null,
num_samples: 4
},
@@ -17,64 +22,189 @@ var ToolParameterTree = Backbone.Model.extend({
// Valid inputs for tree are number, select parameters.
var tool = this.get('tool'),
samples = this.get('samples'),
inputs = tool.get('inputs').filter(function(input) {
return ( ['number', 'select'].indexOf(input.get('type')) !== -1 );
num_samples = this.get('num_samples'),
params_samples = tool.get('inputs').map(function(input) {
return input.get_samples(num_samples);
}),
inputs_names = _.map(inputs, function(i) { return i.get('name')});
// Sample from all valid inputs.
sampling = _.map(inputs, function(input) {
var type = input.get('type');
if (type === 'number') {
return d3.scale.linear().domain([input.get('min'), input.get('max')]).ticks(samples);
}
else if (type === 'select') {
return _.map(input.get('options'), function(option) {
return option[0];
});
}
filtered_params_samples = _.filter(params_samples, function(param_sample) {
return param_sample.get('samples').length !== 0;
});
/**
* Returns tree data.
* Returns tree data. Params_sampling is an array of
*
*/
var create_tree_data = function(param_names, param_settings, index) {
// Terminate when last parameter setting is reached.
if (param_settings.length - 1 === index) {
return _.map(param_settings[index], function(setting) {
var create_tree_data = function(params_samples, index) {
var
param_samples = params_samples[index],
param = param_samples.get('param'),
param_name = param.get('name'),
settings = param_samples.get('samples');
// Create leaves when last parameter setting is reached.
if (params_samples.length - 1 === index) {
return _.map(settings, function(setting) {
return {
name: setting
name: param_name + '=' + setting,
param: param
}
});
}
// Recurse to handle other parameters.
return _.map(param_settings[index], function(setting) {
return _.map(settings, function(setting) {
return {
name: param_names[index] + ':' + setting,
children: create_tree_data(param_names, param_settings, index + 1)
name: param_name + '=' + setting,
param: param,
children: create_tree_data(filtered_params_samples, index + 1)
}
});
};
var tree_data = {
name: 'Parameter Tree for ' + tool.get('name'),
children: create_tree_data(inputs_names, sampling, 0)
children: create_tree_data(filtered_params_samples, 0)
};
// Set valid inputs, tree data for later use.
this.set('valid_inputs', inputs);
this.set('params', _.map(params_samples, function(s) { return s.get('param') }));
this.set('tree_data', tree_data);
}
});
/**
* Tile of rendered genomic data.
*/
var Tile = Backbone.Model.extend({
defaults: {
track: null,
index: null,
region: null,
resolution: null,
data: null,
stale: null,
html_elt: null
},
initialize: function(options) {
}
});
/**
* A track in a genome browser.
*/
var Track = Backbone.Model.extend({
defaults: {
dataset: null
}
});
var FeatureTrack = Track.extend({
defaults: {
track: null
},
/**
* Draw FeatureTrack tile.
* @param result result from server
* @param cxt canvas context to draw on
* @param mode mode to draw in
* @param resolution view resolution
* @param region region to draw
* @param w_scale pixels per base
* @param ref_seq reference sequence data
*/
draw_tile: function(result, ctx, mode, resolution, region, w_scale, ref_seq) {
var track = this,
canvas = ctx.canvas,
tile_low = region.get('start'),
tile_high = region.get('end'),
min_height = 25,
left_offset = this.left_offset;
// Drawing the summary tree (feature coverage histogram)
if (mode === "summary_tree" || mode === "Histogram") {
// Get summary tree data if necessary and set max if there is one.
if (result.dataset_type !== "summary_tree") {
var st_data = this.get_summary_tree_data(result.data, tile_low, tile_high, 200);
if (result.max) {
st_data.max = result.max;
}
result = st_data;
}
// Paint summary tree into canvas
var painter = new painters.SummaryTreePainter(result, tile_low, tile_high, this.prefs);
painter.draw(ctx, canvas.width, canvas.height, w_scale);
return new SummaryTreeTile(track, tile_index, resolution, canvas, result.data, result.max);
}
// Handle row-by-row tracks
// Preprocessing: filter features and determine whether all unfiltered features have been slotted.
var
filtered = [],
slots = this.slotters[w_scale].slots;
all_slotted = true;
if ( result.data ) {
var filters = this.filters_manager.filters;
for (var i = 0, len = result.data.length; i < len; i++) {
var feature = result.data[i];
var hide_feature = false;
var filter;
for (var f = 0, flen = filters.length; f < flen; f++) {
filter = filters[f];
filter.update_attrs(feature);
if (!filter.keep(feature)) {
hide_feature = true;
break;
}
}
if (!hide_feature) {
// Feature visible.
filtered.push(feature);
// Set flag if not slotted.
if ( !(feature[0] in slots) ) {
all_slotted = false;
}
}
}
}
// Create painter.
var filter_alpha_scaler = (this.filters_manager.alpha_filter ? new FilterScaler(this.filters_manager.alpha_filter) : null);
var filter_height_scaler = (this.filters_manager.height_filter ? new FilterScaler(this.filters_manager.height_filter) : null);
// HACK: ref_seq will only be defined for ReadTracks, and only the ReadPainter accepts that argument
var painter = new (this.painter)(filtered, tile_low, tile_high, this.prefs, mode, filter_alpha_scaler, filter_height_scaler, ref_seq);
var feature_mapper = null;
// console.log(( tile_low - this.view.low ) * w_scale, tile_index, w_scale);
ctx.fillStyle = this.prefs.block_color;
ctx.font = ctx.canvas.manager.default_font;
ctx.textAlign = "right";
if (result.data) {
// Draw features.
feature_mapper = painter.draw(ctx, canvas.width, canvas.height, w_scale, slots);
feature_mapper.translation = -left_offset;
}
return new FeatureTrackTile(track, tile_index, resolution, canvas, result.data, w_scale, mode, result.message, all_slotted, feature_mapper);
}
});
/**
* --- Views ---
*/
var TileView = Backbone.View.extend({
});
var ToolParameterTreeView = Backbone.View.extend({
className: 'paramamonster',
className: 'tool-parameter-tree',
initialize: function(options) {
this.model = options.model;
@@ -84,12 +214,14 @@ var ToolParameterTreeView = Backbone.View.extend({
var width = 960,
height = 2000;
// Layout tree.
var cluster = d3.layout.cluster()
.size([height, width - 160]);
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
// Set up vis element.
var vis = d3.select(this.$el[0])
.append("svg")
.attr("width", width)
@@ -97,27 +229,32 @@ var ToolParameterTreeView = Backbone.View.extend({
.append("g")
.attr("transform", "translate(80, 0)");
var nodes = cluster.nodes(this.model.get('tree_data'));
// Set up nodes, links.
var nodes = cluster.nodes(this.model.get('tree_data'));
var link = vis.selectAll("path.link")
.data(cluster.links(nodes))
.enter().append("path")
.attr("class", "link")
.attr("d", diagonal);
var link = vis.selectAll("path.link")
.data(cluster.links(nodes))
.enter().append("path")
.attr("class", "link")
.attr("d", diagonal);
var node = vis.selectAll("g.node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
var node = vis.selectAll("g.node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
node.on("click", function(d, i) {
console.log(d, i);
});
node.append("circle")
.attr("r", 4.5);
node.append("circle")
.attr("r", 4.5);
node.append("text")
.attr("dx", function(d) { return d.children ? -8 : 8; })
.attr("dy", 3)
.attr("text-anchor", function(d) { return d.children ? "end" : "start"; })
.text(function(d) { return d.name; });
node.append("text")
.attr("dx", function(d) { return d.children ? -8 : 8; })
.attr("dy", 3)
.attr("text-anchor", function(d) { return d.children ? "end" : "start"; })
.text(function(d) { return d.name; });
}
});
+41 -260
View File
@@ -436,249 +436,6 @@ function round(num, places) {
return Math.round(num * val) / val;
}
/**
* Generic cache that handles key/value pairs.
*/
var Cache = function( num_elements ) {
this.num_elements = num_elements;
this.clear();
};
extend(Cache.prototype, {
get: function(key) {
var index = this.key_ary.indexOf(key);
if (index !== -1) {
if (this.obj_cache[key].stale) {
// Object is stale, so remove key and object.
this.key_ary.splice(index, 1);
delete this.obj_cache[key];
}
else {
this.move_key_to_end(key, index);
}
}
return this.obj_cache[key];
},
set: function(key, value) {
if (!this.obj_cache[key]) {
if (this.key_ary.length >= this.num_elements) {
// Remove first element
var deleted_key = this.key_ary.shift();
delete this.obj_cache[deleted_key];
}
this.key_ary.push(key);
}
this.obj_cache[key] = value;
return value;
},
// Move key to end of cache. Keys are removed from the front, so moving a key to the end
// delays the key's removal.
move_key_to_end: function(key, index) {
this.key_ary.splice(index, 1);
this.key_ary.push(key);
},
clear: function() {
this.obj_cache = {};
this.key_ary = [];
},
// Returns the number of elements in the cache.
size: function() {
return this.key_ary.length;
}
});
/**
* Data manager for a track.
*/
var DataManager = function(num_elements, track) {
Cache.call(this, num_elements);
this.track = track;
};
extend(DataManager.prototype, Cache.prototype, {
/**
* Load data from server; returns AJAX object so that use of Deferred is possible.
*/
load_data: function(low, high, mode, resolution, extra_params) {
// Setup data request params.
var
chrom = this.track.view.chrom,
params = {"chrom": chrom, "low": low, "high": high, "mode": mode,
"resolution": resolution, "dataset_id" : this.track.dataset_id,
"hda_ldda": this.track.hda_ldda};
$.extend(params, extra_params);
// Add track filters to params.
if (this.track.filters_manager) {
var filter_names = [];
var filters = this.track.filters_manager.filters;
for (var i = 0; i < filters.length; i++) {
filter_names.push(filters[i].name);
}
params.filter_cols = JSON.stringify(filter_names);
}
// Do request.
var manager = this;
return $.getJSON(this.track.data_url, params, function (result) {
manager.set_data(low, high, result);
});
},
/**
* Get track data.
*/
get_data: function(low, high, mode, resolution, extra_params) {
// Debugging:
//console.log("get_data", low, high, mode);
/*
console.log("cache contents:")
for (var i = 0; i < this.key_ary.length; i++) {
console.log("\t", this.key_ary[i], this.obj_cache[this.key_ary[i]]);
}
*/
// Look for entry and return if it's a deferred or if data available is compatible with mode.
var entry = this.get(low, high);
if ( entry &&
( is_deferred(entry) || this.track.data_and_mode_compatible(entry, mode) ) ) {
return entry;
}
//
// Look in cache for data that can be used. Data can be reused if it
// has the requested data and is not summary tree and has details.
// TODO: this logic could be improved if the visualization knew whether
// the data was "index" or "data."
//
var key, split_key, entry_low, entry_high, mode, entry;
for (var i = 0; i < this.key_ary.length; i++) {
key = this.key_ary[i];
split_key = this.split_key(key);
entry_low = split_key[0];
entry_high = split_key[1];
if (low >= entry_low && high <= entry_high) {
// This entry has data in the requested range. Return if data
// is compatible and can be subsetted.
var entry = this.obj_cache[key];
if ( is_deferred(entry) ||
( this.track.data_and_mode_compatible(entry, mode) && this.track.can_subset(entry) ) ) {
this.move_key_to_end(key, i);
return entry;
}
}
}
// Load data from server. The deferred is immediately saved until the
// data is ready, it then replaces itself with the actual data.
entry = this.load_data(low, high, mode, resolution, extra_params);
this.set_data(low, high, entry);
return entry;
},
/** "Deep" data request; used as a parameter for DataManager.get_more_data() */
DEEP_DATA_REQ: "deep",
/** "Broad" data request; used as a parameter for DataManager.get_more_data() */
BROAD_DATA_REQ: "breadth",
/**
* Gets more data for a region using either a depth-first or a breadth-first approach.
*/
get_more_data: function(low, high, mode, resolution, extra_params, req_type) {
//
// Get current data from cache and mark as stale.
//
var cur_data = this.get(low, high);
if ( !(cur_data && this.track.data_and_mode_compatible(cur_data, mode)) ) {
console.log("ERROR: no current data for: ", this.track, low, high, mode, resolution, extra_params);
return;
}
cur_data.stale = true;
//
// Set parameters based on request type.
//
var query_low = low;
if (req_type === this.DEEP_DATA_REQ) {
// Use same interval but set start_val to skip data that's already in cur_data.
$.extend(extra_params, {start_val: cur_data.data.length + 1});
}
else if (req_type === this.BROAD_DATA_REQ) {
// To get past an area of extreme feature depth, set query low to be after either
// (a) the maximum high or HACK/FIXME (b) the end of the last feature returned.
query_low = (cur_data.max_high ? cur_data.max_high : cur_data.data[cur_data.data.length - 1][2]) + 1;
}
//
// Get additional data, append to current data, and set new data. Use a custom deferred object
// to signal when new data is available.
//
var
data_manager = this,
new_data_request = this.load_data(query_low, high, mode, resolution, extra_params)
new_data_available = $.Deferred();
// load_data sets cache to new_data_request, but use custom deferred object so that signal and data
// is all data, not just new data.
this.set_data(low, high, new_data_available);
$.when(new_data_request).then(function(result) {
// Update data and message.
if (result.data) {
result.data = cur_data.data.concat(result.data);
if (result.max_low) {
result.max_low = cur_data.max_low;
}
if (result.message) {
// HACK: replace number in message with current data length. Works but is ugly.
result.message = result.message.replace(/[0-9]+/, result.data.length);
}
}
data_manager.set_data(low, high, result);
new_data_available.resolve(result);
});
return new_data_available;
},
/**
* Get data from the cache.
*/
get: function(low, high) {
return Cache.prototype.get.call( this, this.gen_key(low, high) );
},
/**
* Sets data in the cache.
*/
set_data: function(low, high, result) {
return this.set(this.gen_key(low, high), result);
},
/**
* Generate key for cache.
*/
// TODO: use chrom in key so that (a) data is not thrown away when changing chroms and (b)
// manager does not need to be cleared when changing chroms.
// TODO: use resolution in key b/c summary tree data is dependent on resolution -- is this
// necessary, i.e. will resolution change but not low/high/mode?
gen_key: function(low, high) {
var key = low + "_" + high;
return key;
},
/**
* Split key from cache into array with format [low, high]
*/
split_key: function(key) {
return key.split("_");
}
});
var ReferenceTrackDataManager = function(num_elements, track, subset) {
DataManager.call(this, num_elements, track, subset);
};
extend(ReferenceTrackDataManager.prototype, DataManager.prototype, Cache.prototype, {
get: DataManager.prototype.get,
load_data: function(low, high, mode, resolution, extra_params) {
if (resolution > 1) {
// Now that data is pre-fetched before draw, we don't load reference tracks
// unless it's at the bottom level.
return { data: null };
}
return DataManager.prototype.load_data.call(this, low, high, mode, resolution, extra_params);
}
});
/**
* Drawables hierarchy:
*
@@ -3082,7 +2839,11 @@ var FeatureTrackTile = function(track, index, resolution, canvas, data, w_scale,
.css({'height': ERROR_PADDING-1, 'width': canvas.width}).prependTo(this.html_elt);
// Handle message; only message currently is that only the first N elements are displayed.
var
var tile_region = new GenomeRegion({
chrom: track.view.chrom,
start: this.low,
end: this.high
}),
num_features = data.length,
more_down_icon = $("<a href='javascript:void(0);'/>").addClass("icon more-down")
.attr("title", "For speed, only the first " + num_features + " features in this region were obtained from server. Click to get more data including depth")
@@ -3095,7 +2856,7 @@ var FeatureTrackTile = function(track, index, resolution, canvas, data, w_scale,
more_down_icon.click(function() {
// Mark tile as stale, request more data, and redraw track.
tile.stale = true;
track.data_manager.get_more_data(tile.low, tile.high, track.mode, tile.resolution, {}, track.data_manager.DEEP_DATA_REQ);
track.data_manager.get_more_data(tile_region, track.mode, tile.resolution, {}, track.data_manager.DEEP_DATA_REQ);
$(".tipsy").hide();
track.request_draw(true);
}).dblclick(function(e) {
@@ -3106,7 +2867,7 @@ var FeatureTrackTile = function(track, index, resolution, canvas, data, w_scale,
more_across_icon.click(function() {
// Mark tile as stale, request more data, and redraw track.
tile.stale = true;
track.data_manager.get_more_data(tile.low, tile.high, track.mode, tile.resolution, {}, track.data_manager.BROAD_DATA_REQ);
track.data_manager.get_more_data(tile_region, track.mode, tile.resolution, {}, track.data_manager.BROAD_DATA_REQ);
$(".tipsy").hide();
track.request_draw(true);
}).dblclick(function(e) {
@@ -3245,7 +3006,20 @@ var Track = function(view, container, obj_dict) {
this.data_url_extra_params = {}
this.data_query_wait = ('data_query_wait' in obj_dict ? obj_dict.data_query_wait : DEFAULT_DATA_QUERY_WAIT);
this.dataset_check_url = converted_datasets_state_url;
this.data_manager = ('data_manager' in obj_dict ? obj_dict.data_manager : new DataManager(DATA_CACHE_SIZE, this));
// A little ugly creating data manager right now due to transition to Backbone-based objects.
var dataset = new Dataset({
id: obj_dict.dataset_id,
hda_ldda: obj_dict.hda_ldda
});
this.data_manager = ('data_manager' in obj_dict ?
obj_dict.data_manager :
new GenomeDataManager({
dataset: dataset,
data_url: default_data_url,
data_mode_compatible: this.data_and_mode_compatible,
can_subset: this.can_subset,
}));
// Height attributes: min height, max height, and visible height.
this.min_height_px = 16;
@@ -3560,6 +3334,9 @@ var TiledTrack = function(view, container, obj_dict) {
// Attribute init.
this.filters_manager = new FiltersManager(this, ('filters' in obj_dict ? obj_dict.filters : null));
// HACK: set filters manager for data manager.
// FIXME: prolly need function to set filters and update data_manager reference.
this.data_manager.set('filters_manager', this.filters_manager);
this.filters_available = false;
this.tool = ('tool' in obj_dict && obj_dict.tool ? new Tool(this, obj_dict.tool, obj_dict.tool_state) : null);
this.tile_cache = new Cache(TILE_CACHE_SIZE);
@@ -3810,15 +3587,18 @@ extend(TiledTrack.prototype, Drawable.prototype, Track.prototype, {
var track = this,
key = this._gen_tile_cache_key(width, w_scale, tile_index),
tile_bounds = this._get_tile_bounds(tile_index, resolution),
tile_low = tile_bounds[0],
tile_high = tile_bounds[1];
region = new GenomeRegion({
chrom: this.view.chrom,
start: tile_bounds[0],
end: tile_bounds[1]
});
// Init kwargs if necessary to avoid having to check if kwargs defined.
if (!kwargs) { kwargs = {}; }
// Check tile cache, if found show existing tile in correct position
var tile = (force ? undefined : track.tile_cache.get(key));
if (tile) {
var tile = (force ? undefined : track.tile_cache.get_elt(key));
if (tile) {
track.show_tile(tile, parent_element, w_scale);
return tile;
}
@@ -3827,7 +3607,7 @@ extend(TiledTrack.prototype, Drawable.prototype, Track.prototype, {
var can_draw_now = true
// Get the track data, maybe a deferred
var tile_data = track.data_manager.get_data( tile_low, tile_high, track.mode, resolution, track.data_url_extra_params );
var tile_data = track.data_manager.get_data( region, track.mode, resolution, track.data_url_extra_params );
if ( is_deferred( tile_data ) ) {
can_draw_now = false;
}
@@ -3835,7 +3615,7 @@ extend(TiledTrack.prototype, Drawable.prototype, Track.prototype, {
// Get seq data if needed, maybe a deferred
var seq_data;
if ( view.reference_track && w_scale > view.canvas_manager.char_width_px ) {
seq_data = view.reference_track.data_manager.get_data(tile_low, tile_high, track.mode, resolution, view.reference_track.data_url_extra_params)
seq_data = view.reference_track.data_manager.get_data(region, track.mode, resolution, view.reference_track.data_url_extra_params)
if ( is_deferred( seq_data ) ) {
can_draw_now = false;
}
@@ -3857,9 +3637,8 @@ extend(TiledTrack.prototype, Drawable.prototype, Track.prototype, {
// Draw canvas.
var
canvas = track.view.canvas_manager.new_canvas(),
tile_bounds = track._get_tile_bounds(tile_index, resolution),
tile_low = tile_bounds[0],
tile_high = tile_bounds[1],
tile_low = region.get('start'),
tile_high = region.get('end'),
width = Math.ceil( (tile_high - tile_low) * w_scale ) + track.left_offset,
height = track.get_canvas_height(tile_data, mode, w_scale, width);
@@ -3871,7 +3650,7 @@ extend(TiledTrack.prototype, Drawable.prototype, Track.prototype, {
// Don't cache, show if no tile.
if (tile !== undefined) {
track.tile_cache.set(key, tile);
track.tile_cache.set_elt(key, tile);
track.show_tile(tile, parent_element, w_scale);
}
return tile;
@@ -4158,7 +3937,7 @@ extend(CompositeTrack.prototype, TiledTrack.prototype, {
if (!kwargs) { kwargs = {}; }
// Check tile cache, if found show existing tile in correct position
var tile = (force ? undefined : track.tile_cache.get(key));
var tile = (force ? undefined : track.tile_cache.get_elt(key));
if (tile) {
track.show_tile(tile, parent_element, w_scale);
return tile;
@@ -4246,7 +4025,7 @@ extend(CompositeTrack.prototype, TiledTrack.prototype, {
}
// Don't cache, show if no tile.
this.tile_cache.set(key, tile);
this.tile_cache.set_elt(key, tile);
this.show_tile(tile, parent_element, w_scale);
return tile;
}
@@ -4353,7 +4132,9 @@ var ReferenceTrack = function (view) {
this.content_div.css("border", "none");
this.data_url = reference_url;
this.data_url_extra_params = {dbkey: view.dbkey};
this.data_manager = new ReferenceTrackDataManager(DATA_CACHE_SIZE, this, false);
this.data_manager = new ReferenceTrackDataManager({
data_url: reference_url
});
this.hide_contents();
};
extend(ReferenceTrack.prototype, Drawable.prototype, TiledTrack.prototype, {
+371 -5
View File
@@ -6,9 +6,271 @@
* and models can be used without views.
*/
// --------- Models ---------
/**
* -- Models --
* Generic cache that handles key/value pairs.
*/
var Cache = Backbone.Model.extend({
defaults: {
num_elements: 20,
obj_cache: {},
key_ary: []
},
get_elt: function(key) {
var obj_cache = this.attributes.obj_cache,
key_ary = this.attributes.key_ary,
index = key_ary.indexOf(key);
if (index !== -1) {
if (obj_cache[key].stale) {
// Object is stale, so remove key and object.
key_ary.splice(index, 1);
delete obj_cache[key];
}
else {
this.move_key_to_end(key, index);
}
}
return obj_cache[key];
},
set_elt: function(key, value) {
var obj_cache = this.attributes.obj_cache,
key_ary = this.attributes.key_ary,
num_elements = this.attributes.num_elements;
if (!obj_cache[key]) {
if (key_ary.length >= num_elements) {
// Remove first element
var deleted_key = key_ary.shift();
delete obj_cache[deleted_key];
}
key_ary.push(key);
}
obj_cache[key] = value;
return value;
},
// Move key to end of cache. Keys are removed from the front, so moving a key to the end
// delays the key's removal.
move_key_to_end: function(key, index) {
this.attributes.key_ary.splice(index, 1);
this.attributes.key_ary.push(key);
},
clear: function() {
this.attributes.obj_cache = {};
this.attributes.key_ary = [];
},
// Returns the number of elements in the cache.
size: function() {
return this.attributes.key_ary.length;
}
});
/**
* Data manager for genomic data. Data is connected to and queryable by genomic regions.
*/
var GenomeDataManager = Cache.extend({
defaults: _.extend({}, Cache.prototype.defaults, {
dataset: null,
filters_manager: null,
data_url: null,
data_mode_compatible: function(entry, mode) { return true; },
can_subset: function(entry) { return false; }
}),
/**
* Load data from server; returns AJAX object so that use of Deferred is possible.
*/
load_data: function(region, mode, resolution, extra_params) {
// Setup data request params.
var params = {
"chrom": region.get('chrom'),
"low": region.get('start'),
"high": region.get('end'),
"mode": mode,
"resolution": resolution
};
dataset = this.get('dataset');
// ReferenceDataManager does not have dataset.
if (dataset) {
params['dataset_id'] = dataset.id;
params['hda_ldda'] = dataset.get('hda_ldda');
}
$.extend(params, extra_params);
// Add track filters to params.
var filters_manager = this.get('filters_manager');
if (filters_manager) {
var filter_names = [];
var filters = filters_manager.filters;
for (var i = 0; i < filters.length; i++) {
filter_names.push(filters[i].name);
}
params.filter_cols = JSON.stringify(filter_names);
}
// Do request.
var manager = this;
return $.getJSON(this.get('data_url'), params, function (result) {
manager.set_data(region, result);
});
},
/**
* Get data from dataset.
*/
get_data: function(region, mode, resolution, extra_params) {
// Debugging:
//console.log("get_data", low, high, mode);
/*
console.log("cache contents:")
for (var i = 0; i < this.key_ary.length; i++) {
console.log("\t", this.key_ary[i], this.obj_cache[this.key_ary[i]]);
}
*/
// Look for entry and return if it's a deferred or if data available is compatible with mode.
var entry = this.get_elt(region);
if ( entry &&
( is_deferred(entry) || this.get('data_mode_compatible')(entry, mode) ) ) {
return entry;
}
//
// Look in cache for data that can be used. Data can be reused if it
// has the requested data and is not summary tree and has details.
// TODO: this logic could be improved if the visualization knew whether
// the data was "index" or "data."
//
var
key_ary = this.get('key_ary'),
obj_cache = this.get('obj_cache'),
key, region, entry_region, mode, entry;
for (var i = 0; i < key_ary.length; i++) {
entry_region = new GenomeRegion(key_ary[i]);
if (entry_region.contains(region)) {
// This entry has data in the requested range. Return if data
// is compatible and can be subsetted.
var entry = obj_cache[key];
if ( is_deferred(entry) ||
( this.get('data_mode_compatible')(entry, mode) && this.get('can_subset')(entry) ) ) {
this.move_key_to_end(key, i);
return entry;
}
}
}
// Load data from server. The deferred is immediately saved until the
// data is ready, it then replaces itself with the actual data.
entry = this.load_data(region, mode, resolution, extra_params);
this.set_data(region, entry);
return entry;
},
/**
* Alias for set_elt for readbility.
*/
set_data: function(region, entry) {
this.set_elt(region, entry);
},
/**
/** "Deep" data request; used as a parameter for DataManager.get_more_data() */
DEEP_DATA_REQ: "deep",
/** "Broad" data request; used as a parameter for DataManager.get_more_data() */
BROAD_DATA_REQ: "breadth",
/**
* Gets more data for a region using either a depth-first or a breadth-first approach.
*/
get_more_data: function(region, mode, resolution, extra_params, req_type) {
//
// Get current data from cache and mark as stale.
//
var cur_data = this.get_elt(region);
if ( !(cur_data && this.get('data_mode_compatible')(cur_data, mode)) ) {
console.log("ERROR: no current data for: ", dataset, region.toString(), mode, resolution, extra_params);
return;
}
cur_data.stale = true;
//
// Set parameters based on request type.
//
var query_low = region.get('start');
if (req_type === this.DEEP_DATA_REQ) {
// Use same interval but set start_val to skip data that's already in cur_data.
$.extend(extra_params, {start_val: cur_data.data.length + 1});
}
else if (req_type === this.BROAD_DATA_REQ) {
// To get past an area of extreme feature depth, set query low to be after either
// (a) the maximum high or HACK/FIXME (b) the end of the last feature returned.
query_low = (cur_data.max_high ? cur_data.max_high : cur_data.data[cur_data.data.length - 1][2]) + 1;
}
var query_region = region.copy().set('start', query_low);
//
// Get additional data, append to current data, and set new data. Use a custom deferred object
// to signal when new data is available.
//
var
data_manager = this,
new_data_request = this.load_data(query_region, mode, resolution, extra_params)
new_data_available = $.Deferred();
// load_data sets cache to new_data_request, but use custom deferred object so that signal and data
// is all data, not just new data.
this.set_data(region, new_data_available);
$.when(new_data_request).then(function(result) {
// Update data and message.
if (result.data) {
result.data = cur_data.data.concat(result.data);
if (result.max_low) {
result.max_low = cur_data.max_low;
}
if (result.message) {
// HACK: replace number in message with current data length. Works but is ugly.
result.message = result.message.replace(/[0-9]+/, result.data.length);
}
}
data_manager.set_data(region, result);
new_data_available.resolve(result);
});
return new_data_available;
},
/**
* Get data from the cache.
*/
get_elt: function(region) {
return Cache.prototype.get_elt.call(this, region.toString());
},
/**
* Sets data in the cache.
*/
set_elt: function(region, result) {
return Cache.prototype.set_elt.call(this, region.toString(), result);
}
});
var ReferenceTrackDataManager = GenomeDataManager.extend({
load_data: function(low, high, mode, resolution, extra_params) {
if (resolution > 1) {
// Now that data is pre-fetched before draw, we don't load reference tracks
// unless it's at the bottom level.
return { data: null };
}
return GenomeDataManager.prototype.load_data.call(this, low, high, mode, resolution, extra_params);
}
});
/**
* A genome build.
@@ -26,13 +288,117 @@ var Genome = Backbone.Model.extend({
});
/**
* A genome browser bookmark.
* A genomic region.
*/
var BrowserBookmark = Backbone.Model.extend({
var GenomeRegion = Backbone.Model.extend({
defaults: {
chrom: null,
start: 0,
end: 0,
DIF_CHROMS: 1000,
BEFORE: 1001,
CONTAINS: 1002,
OVERLAP_START: 1003,
OVERLAP_END: 1004,
CONTAINED_BY: 1005,
AFTER: 1006
},
/**
* as_str attribute using the format chrom:start-end can be
* used to set object's attributes.
*/
initialize: function(options) {
if (!this.get('chrom') && !this.get('start') &&
!this.get('end') && 'as_str' in options) {
var pieces = options.as_str.split(':'),
chrom = pieces[0],
pieces = pieces.split('-'),
start = pieces[0],
end = pieces[1];
this.set('chrom', chrom);
this.set('start', start);
this.set('end', end);
}
},
copy: function() {
return new GenomeRegion({
chrom: this.get('chrom'),
start: this.get('start'),
end: this.get('end')
});
},
/** Returns region in canonical form chrom:start-end */
toString: function() {
return this.get('chrom') + ":" + this.get('start') + "-" + this.get('end');
},
/**
* Compute the type of overlap between this region and another region. The overlap is computed relative to the given/second region;
* hence, OVERLAP_START indicates that the first region overlaps the start (but not the end) of the second region.
*/
compute_overlap: function(a_region) {
var first_chrom = this.get('chrom'), second_chrom = a_region.get('chrom'),
first_start = this.get('start'), second_start = a_region.get('start'),
first_end = this.get('end'), second_end = a_region.get('end'),
overlap;
// Look at chroms.
if (first_chrom && second_chrom && first_chrom !== second_chrom) {
return this.get('DIF_CHROMS');
}
// Look at regions.
if (first_start < second_start) {
if (first_end < second_start) {
overlap = this.get('BEFORE');
}
else if (first_end <= second_end) {
overlap = this.get('OVERLAP_START');
}
else { // first_end > second_end
overlap = this.get('CONTAINS');
}
}
else { // first_start >= second_start
if (first_start > second_end) {
overlap = this.get('AFTER');
}
else if (first_end <= second_end) {
overlap = this.get('CONTAINED_BY');
}
else {
overlap = this.get('OVERLAP_END');
}
}
return overlap;
},
/**
* Returns true if this region contains a given region.
*/
contains: function(a_region) {
return this.compute_overlap(a_region) === this.get('CONTAINS');
},
/**
* Returns true if regions overlap.
*/
overlaps: function(a_region) {
return _.intersection( [this.compute_overlap(a_region)],
[this.get('DIF_CHROMS'), this.get('BEFORE'), this.get('AFTER')] ).length === 0;
}
});
/**
* A genome browser bookmark.
*/
var BrowserBookmark = Backbone.Model.extend({
defaults: {
region: null,
note: ""
}
});
@@ -100,7 +466,7 @@ var Dataset = Backbone.Model.extend({
id: "",
type: "",
name: "",
hda_ldda: ""
hda_ldda: ""
}
});
@@ -232,7 +598,7 @@ var CircsterView = Backbone.View.extend({
var chrom_arc = chrom_info[0],
chrom_data = chrom_info[1];
return arcs_layout.chrom_data_layout(chrom_arc, chrom_data, radius_start, radius_start + dataset_arc_height, dataset_max);
});
});
// -- Render viz. --
+1 -1
View File
@@ -42,7 +42,7 @@ ${parent.javascripts()}
<script type='text/javascript' src="${h.url_for('/static/scripts/excanvas.js')}"></script>
<![endif]-->
${h.js( "galaxy.base", "galaxy.panels", "json2", "jquery", "jstorage", "jquery.event.drag", "jquery.event.hover","jquery.mousewheel", "jquery.autocomplete", "viz/trackster", "viz/trackster_ui", "jquery.ui.sortable.slider", "farbtastic", "viz/visualization" )}
${h.js( "galaxy.base", "galaxy.panels", "json2", "jquery", "jstorage", "jquery.event.drag", "jquery.event.hover","jquery.mousewheel", "jquery.autocomplete", "viz/visualization", "viz/trackster", "viz/trackster_ui", "jquery.ui.sortable.slider", "farbtastic" )}
<script type="text/javascript">
//
+5 -2
View File
@@ -46,7 +46,7 @@
tool_param_tree_view = new ToolParameterTreeView({ model: tool_param_tree });
tool_param_tree_view.render();
$('#vis').append(tool_param_tree_view.$el);
$('#param-tree').append(tool_param_tree_view.$el);
});
</script>
</%def>
@@ -58,5 +58,8 @@
</div>
<div style="clear: both"></div>
</div>
<div id="vis" class="unified-panel-body"></div>
<div class="unified-panel-body">
<div id="param-tree"></div>
<div id="tile-view"></div>
</div>
</%def>