Partial implementation of Circos visualization using D3.

This commit is contained in:
Jeremy Goecks
2012-05-25 15:13:25 -04:00
parent 1d6a796f94
commit 97650ce691
7 changed files with 9637 additions and 5 deletions
+23
View File
@@ -826,6 +826,29 @@ class TracksController( BaseUIController, UsesVisualization, UsesHistoryDatasetA
return self.add_track_async( trans, output_dataset.id )
@web.expose
@web.require_login( "use Galaxy visualizations", use_panels=True )
def circos( self, trans, hda_ldda, dataset_id ):
# Get dataset.
dataset = self._get_dataset( trans, hda_ldda, dataset_id )
# Get genome info.
dbkey = dataset.dbkey
chroms_info = self.genomes.chroms( trans, dbkey=dbkey )
genome = { 'dbkey': dbkey, 'chroms_info': chroms_info }
# Get summary tree data for dataset.
data_sources = self._get_datasources( trans, dataset )
tracks_dataset_type = data_sources['index']['name']
converted_dataset = dataset.get_converted_dataset( trans, tracks_dataset_type )
indexer = get_data_provider( tracks_dataset_type )( converted_dataset, dataset )
dataset_summary = []
for chrom_info in chroms_info[ 'chrom_info' ]:
summary = indexer.get_summary( chrom_info[ 'chrom' ], 0, chrom_info[ 'len' ], level=4 )
dataset_summary.append( summary )
return trans.fill_template_mako( "visualization/circos.mako", dataset=dataset, dataset_summary=dataset_summary, genome=genome )
# -----------------
# Helper methods.
# -----------------
+9384
View File
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -1,7 +1,9 @@
/**
* Model classes for Galaxy tools and tool panel. Models have no references to views,
* instead using events to indicate state changes; this is advantageous because
* multiple views can use the same object and models can be used without views.
* Model, view, and controller objects for Galaxy tools and tool panel.
*
* Models have no references to views, instead using events to indicate state
* changes; this is advantageous because multiple views can use the same object
* and models can be used without views.
*/
/**
@@ -481,7 +483,6 @@ var ToolFormView = Backbone.View.extend({
}
});
/**
* Integrated tool menu + tool execution.
*/
+184
View File
@@ -1,3 +1,187 @@
/**
* Model, view, and controller objects for Galaxy tools and tool panel.
*
* Models have no references to views, instead using events to indicate state
* changes; this is advantageous because multiple views can use the same object
* and models can be used without views.
*/
/**
* -- Models --
*/
/**
* A genome build.
*/
var Genome = Backbone.Model.extend({
defaults: {
name: null,
key: null,
chroms_info: null
},
get_chroms_info: function() {
return this.attributes.chroms_info.chrom_info;
}
});
/**
* A histogram dataset.
*/
var HistogramDataset = Backbone.Model.extend({
initialize: function(data) {
// Set max across dataset.
this.attributes.data = data;
this.attributes.max = _.max(data, function(d) {
if (!d || typeof d === "string") { return 0; }
return d[1];
})[1];
}
});
/**
* Layout for a histogram dataset in a circos visualization.
*/
var CircosHistogramDatasetLayout = Backbone.Model.extend({
// TODO: should accept genome and dataset and use these to generate layout data.
/**
* Returns arc layouts for genome's chromosomes/contigs. Arcs are arranged in a circle
* separated by gaps.
*/
chroms_layout: function() {
// Setup chroms layout using pie.
var chroms_info = this.attributes.genome.get_chroms_info(),
pie_layout = d3.layout.pie().value(function(d) { return d.len; }).sort(null),
init_arcs = pie_layout(chroms_info),
gap_per_chrom = this.attributes.total_gap / chroms_info.length,
chrom_arcs = _.map(init_arcs, function(arc, index) {
// For short chroms, endAngle === startAngle.
var new_endAngle = arc.endAngle - gap_per_chrom;
arc.endAngle = (new_endAngle > arc.startAngle ? new_endAngle : arc.startAngle);
return arc;
});
// TODO: remove arcs for chroms that are too small and recompute?
return chrom_arcs;
},
/**
* Returns layouts for drawing a chromosome's data. For now, only works with summary tree data.
*/
chrom_data_layout: function(chrom_arc, chrom_data, inner_radius, outer_radius, max) {
// If no chrom data, return null.
if (!chrom_data || typeof chrom_data === "string") {
return null;
}
var data = chrom_data[0],
delta = chrom_data[3],
scale = d3.scale.linear()
.domain( [0, max] )
.range( [inner_radius, outer_radius] ),
arc_layout = d3.layout.pie().value(function(d) {
return delta;
})
.startAngle(chrom_arc.startAngle)
.endAngle(chrom_arc.endAngle),
arcs = arc_layout(data);
// Use scale to assign outer radius.
_.each(data, function(datum, index) {
arcs[index].outerRadius = scale(datum[1]);
});
return arcs;
}
});
/**
* -- Views --
*/
var CircosView = Backbone.View.extend({
className: 'circos',
initialize: function(options) {
this.width = options.width;
this.height = options.height;
this.total_gap = options.total_gap;
this.genome = options.genome;
this.dataset = options.dataset;
this.radius_start = options.radius_start;
this.dataset_arc_height = options.dataset_arc_height;
},
render: function() {
// -- Layout viz. --
var radius_start = this.radius_start,
dataset_arc_height = this.dataset_arc_height,
// Layout chromosome arcs.
arcs_layout = new CircosHistogramDatasetLayout({
genome: this.genome,
total_gap: this.total_gap
}),
chrom_arcs = arcs_layout.chroms_layout(),
// Merge chroms layout with data.
layout_and_data = _.zip(chrom_arcs, this.dataset.attributes.data),
dataset_max = this.dataset.attributes.max,
// Do dataset layout for each chromosome's data using pie layout.
chroms_data_layout = _.map(layout_and_data, function(chrom_info) {
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. --
var svg = d3.select(this.$el[0])
.append("svg")
.attr("width", this.width)
.attr("height", this.height)
.append("g")
.attr("transform", "translate(" + this.width / 2 + "," + this.height / 2 + ")");
// Draw background arcs for each chromosome.
var base_arc = svg.append("g").attr("id", "inner-arc"),
arc_gen = d3.svg.arc()
.innerRadius(radius_start)
.outerRadius(radius_start + dataset_arc_height),
// Draw arcs.
chroms_elts = base_arc.selectAll("#inner-arc>path")
.data(chrom_arcs).enter().append("path")
.attr("d", arc_gen)
.style("stroke", "#ccc")
.style("fill", "#ccc")
.append("title").text(function(d) { return d.data.chrom; });
// For each chromosome, draw dataset.
_.each(chroms_data_layout, function(chrom_layout) {
if (!chrom_layout) { return; }
var group = svg.append("g"),
arc_gen = d3.svg.arc().innerRadius(radius_start),
dataset_elts = group.selectAll("path")
.data(chrom_layout).enter().append("path")
.attr("d", arc_gen)
.style("stroke", "red")
.style("fill", "red");
});
}
});
/**
* -- Routers --
*/
/**
* Router for track browser.
*/
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
var TrackBrowserRouter=Backbone.Router.extend({initialize:function(b){this.view=b.view;this.route(/([\w]+)$/,"change_location");this.route(/([\w]+\:[\d,]+-[\d,]+)$/,"change_location");var a=this;a.view.on("navigate",function(c){a.navigate(c)})},change_location:function(a){this.view.go_to(a)}});
var Genome=Backbone.Model.extend({defaults:{name:null,key:null,chroms_info:null},get_chroms_info:function(){return this.attributes.chroms_info.chrom_info}});var HistogramDataset=Backbone.Model.extend({initialize:function(a){this.attributes.data=a;this.attributes.max=_.max(a,function(b){if(!b||typeof b==="string"){return 0}return b[1]})[1]}});var CircosHistogramDatasetLayout=Backbone.Model.extend({chroms_layout:function(){var b=this.attributes.genome.get_chroms_info(),d=d3.layout.pie().value(function(f){return f.len}).sort(null),e=d(b),a=this.attributes.total_gap/b.length,c=_.map(e,function(h,g){var f=h.endAngle-a;h.endAngle=(f>h.startAngle?f:h.startAngle);return h});return c},chrom_data_layout:function(j,b,g,f,h){if(!b||typeof b==="string"){return null}var d=b[0],i=b[3],c=d3.scale.linear().domain([0,h]).range([g,f]),e=d3.layout.pie().value(function(k){return i}).startAngle(j.startAngle).endAngle(j.endAngle),a=e(d);_.each(d,function(k,l){a[l].outerRadius=c(k[1])});return a}});var CircosView=Backbone.View.extend({className:"circos",initialize:function(a){this.width=a.width;this.height=a.height;this.total_gap=a.total_gap;this.genome=a.genome;this.dataset=a.dataset;this.radius_start=a.radius_start;this.dataset_arc_height=a.dataset_arc_height},render:function(){var d=this.radius_start,e=this.dataset_arc_height,j=new CircosHistogramDatasetLayout({genome:this.genome,total_gap:this.total_gap}),i=j.chroms_layout(),g=_.zip(i,this.dataset.attributes.data),h=this.dataset.attributes.max,b=_.map(g,function(m){var n=m[0],l=m[1];return j.chrom_data_layout(n,l,d,d+e,h)});var c=d3.select(this.$el[0]).append("svg").attr("width",this.width).attr("height",this.height).append("g").attr("transform","translate("+this.width/2+","+this.height/2+")");var k=c.append("g").attr("id","inner-arc"),f=d3.svg.arc().innerRadius(d).outerRadius(d+e),a=k.selectAll("#inner-arc>path").data(i).enter().append("path").attr("d",f).style("stroke","#ccc").style("fill","#ccc").append("title").text(function(l){return l.data.chrom});_.each(b,function(l){if(!l){return}var o=c.append("g"),n=d3.svg.arc().innerRadius(d),m=o.selectAll("path").data(l).enter().append("path").attr("d",n).style("stroke","red").style("fill","red")})}});var TrackBrowserRouter=Backbone.Router.extend({initialize:function(b){this.view=b.view;this.route(/([\w]+)$/,"change_location");this.route(/([\w]+\:[\d,]+-[\d,]+)$/,"change_location");var a=this;a.view.on("navigate",function(c){a.navigate(c)})},change_location:function(a){this.view.go_to(a)}});
+39
View File
@@ -0,0 +1,39 @@
<%inherit file="/base.mako"/>
<%def name="stylesheets()">
${parent.stylesheets()}
</%def>
<%def name="javascripts()">
${parent.javascripts()}
${h.js( "libs/d3", "mvc/visualization" )}
<script type="text/javascript">
$(function() {
// -- Viz set up. --
var genome = new Genome(JSON.parse('${ h.to_json_string( genome ) }')),
dataset = new HistogramDataset(JSON.parse('${ h.to_json_string( dataset_summary ) }')),
circos = new CircosView({
width: 600,
height: 600,
// Gap is difficult to set because it very dependent on chromosome size and organization.
total_gap: 2 * Math.PI * 0.5,
genome: genome,
dataset: dataset,
radius_start: 100,
dataset_arc_height: 50
});
// -- Render viz. --
circos.render();
$('body').append(circos.$el);
});
</script>
</%def>
<%def name="body()">
<h1>Circos plot for '${dataset.name}'</h1>
</%def>