trackster:

- Fixed incorrect drawing of thick strands
- Linetrack: can now set min and max values for y-axis
- Featuretrack: can set color for label and block
- Drawing delays 50ms so that scrolling quickly across browser does not load everything travelled through
- Fixed another memory leak in arraytree provider
- Made intron tracks lighter in color
This commit is contained in:
Kanwei Li
2010-01-26 16:37:25 -05:00
parent c47680c1eb
commit ef5cff784a
8 changed files with 243 additions and 80 deletions
@@ -22,6 +22,7 @@ class ArrayTreeDataProvider( object ):
try:
chrom_array_tree = d[chrom]
except KeyError:
f.close()
return "no data"
root_summary = chrom_array_tree.get_summary( 0, chrom_array_tree.levels )
@@ -36,6 +37,7 @@ class ArrayTreeDataProvider( object ):
try:
chrom_array_tree = d[chrom]
except KeyError:
f.close()
return None
block_size = chrom_array_tree.block_size
+7
View File
@@ -132,6 +132,7 @@ class TracksController( BaseController ):
error( "No chromosome lengths file found for '%s'" % dataset.name )
return trans.fill_template( 'tracks/browser.mako',
#dataset_ids=dataset_ids,
title = vis.title,
id=id,
tracks=tracks,
chrom=chrom,
@@ -249,3 +250,9 @@ class TracksController( BaseController ):
trans.sa_session.add( assoc )
trans.sa_session.flush()
return new_dataset
@web.json
def update_config( self, trans, **kwargs ):
raise RuntimeError
+1 -1
View File
@@ -8,7 +8,7 @@ from shutil import copyfile
from os import path
# Scripts that should not be packed -- just copied
do_not_pack = set( [ "ie_pngfix.js", "aflax.js" ] )
do_not_pack = set()
cmd = "java -jar ../../scripts/yuicompressor.jar --type js %(fname)s -o packed/%(fname)s"
+110 -34
View File
@@ -4,7 +4,7 @@
var DEBUG = false;
var DENSITY = 1000,
FEATURE_LEVELS = 100,
FEATURE_LEVELS = 10,
DATA_ERROR = "There was an error in indexing this dataset.",
DATA_NONE = "No data for this chrom/contig.",
DATA_PENDING = "Currently indexing... please wait",
@@ -47,8 +47,7 @@ function commatize( number ) {
var Cache = function( num_elements ) {
this.num_elements = num_elements;
this.obj_cache = {};
this.key_ary = [];
this.clear();
};
$.extend( Cache.prototype, {
get: function( key ) {
@@ -71,11 +70,16 @@ $.extend( Cache.prototype, {
}
this.obj_cache[key] = value;
return value;
},
clear: function() {
this.obj_cache = {};
this.key_ary = [];
}
});
var View = function( chrom, max_high ) {
var View = function( chrom, max_high, config ) {
this.chrom = chrom;
this.config = config;
this.tracks = [];
this.max_low = 0;
this.max_high = max_high;
@@ -181,7 +185,6 @@ $.extend( TiledTrack.prototype, Track.prototype, {
var w_scale = this.content_div.width() / range;
var max_height = 20;
var tile_element;
// Index of first tile that overlaps visible region
var tile_index = Math.floor( low / resolution / DENSITY );
@@ -201,22 +204,30 @@ $.extend( TiledTrack.prototype, Track.prototype, {
});
// Our responsibility to move the element to the new parent
parent_element.append( cached );
max_height = Math.max( max_height, cached.height() );
this.max_height = Math.max( this.max_height, cached.height() );
} else {
tile_element = this.draw_tile( resolution, tile_index, parent_element, w_scale );
if ( tile_element ) {
this.tile_cache.set(key, tile_element);
max_height = Math.max( max_height, tile_element.height() );
}
this.delayed_draw(this, key, low, high, tile_index, resolution, parent_element, w_scale);
}
this.content_div.css( "height", max_height );
tile_index += 1;
}
}, delayed_draw: function(track, key, low, high, tile_index, resolution, parent_element, w_scale) {
// Put a 50ms delay on drawing so that if the user scrolls fast, we don't load extra data
setTimeout(function() {
if ( !(low > track.view.high || high < track.view.low) ) {
tile_element = track.draw_tile( resolution, tile_index, parent_element, w_scale );
if ( tile_element ) {
track.tile_cache.set(key, tile_element);
track.max_height = Math.max( track.max_height, tile_element.height() );
track.content_div.css( "height", track.max_height );
}
}
}, 50);
}
});
var LabelTrack = function ( parent_element ) {
Track.call( this, null, parent_element );
this.hidden = true;
this.container_div.addClass( "label-track" );
};
$.extend( LabelTrack.prototype, Track.prototype, {
@@ -241,7 +252,7 @@ $.extend( LabelTrack.prototype, Track.prototype, {
}
});
var LineTrack = function ( name, dataset_id, indexer, height ) {
var LineTrack = function ( name, dataset_id, indexer, height, minval, maxval ) {
this.tile_cache = new Cache(CACHED_TILES_LINE);
Track.call( this, name, $("#viewport") );
TiledTrack.call( this );
@@ -251,12 +262,19 @@ var LineTrack = function ( name, dataset_id, indexer, height ) {
this.container_div.addClass( "line-track" );
this.dataset_id = dataset_id;
this.data_queue = {};
this.cache = new Cache(CACHED_DATA); // We need to cache some data because of
this.data_cache = new Cache(CACHED_DATA); // We need to cache some data because of
// asynchronous calls
if (minval !== undefined && maxval !== undefined) {
this.min_value = minval;
this.max_value = maxval;
this.vertical_range = maxval - minval;
}
};
$.extend( LineTrack.prototype, TiledTrack.prototype, {
init: function() {
var track = this;
var track = this,
track_id = track.view.tracks.indexOf(track);
track.content_div.text(DATA_LOADING);
$.getJSON( data_url, { stats: true, indexer: track.indexer,
chrom: track.view.chrom, low: null, high: null,
@@ -274,13 +292,20 @@ $.extend( LineTrack.prototype, TiledTrack.prototype, {
} else {
track.content_div.text("");
track.content_div.css( "height", track.height_px + "px" );
track.min_value = data.min;
track.max_value = data.max;
track.vertical_range = track.max_value - track.min_value;
if (track.min_value === undefined || track.max_value === undefined) {
track.min_value = data.min;
track.max_value = data.max;
track.vertical_range = track.max_value - track.min_value;
// Update the config
$('#track_' + track_id + '_minval').val(track.min_value);
$('#track_' + track_id + '_maxval').val(track.max_value);
}
// Draw y-axis labels
var min_label = $("<div class='yaxislabel'>" + track.min_value + "</div>");
var max_label = $("<div class='yaxislabel'>" + track.max_value + "</div>");
var min_label = $("<div></div>").addClass('yaxislabel').attr("id", 'linetrack_' + track_id + '_minval').text(track.min_value);
var max_label = $("<div></div>").addClass('yaxislabel').attr("id", 'linetrack_' + track_id + '_maxval').text(track.max_value);
max_label.css({ position: "relative", top: "25px" });
max_label.prependTo(track.container_div);
@@ -303,11 +328,11 @@ $.extend( LineTrack.prototype, TiledTrack.prototype, {
$.getJSON( data_url, { indexer: this.indexer, chrom: this.view.chrom,
low: low, high: high, dataset_id: this.dataset_id,
resolution: this.view.resolution }, function ( data ) {
track.cache.set(key, data);
track.data_cache.set(key, data);
delete track.data_queue[key];
track.draw();
});
}
}
},
draw_tile: function( resolution, tile_index, parent_element, w_scale ) {
if (!this.vertical_range) { // We don't have the necessary information yet
@@ -319,12 +344,12 @@ $.extend( LineTrack.prototype, TiledTrack.prototype, {
canvas = $("<canvas class='tile'></canvas>"),
key = resolution + "_" + tile_index;
if (!this.cache.get(key)) {
if (!this.data_cache.get(key)) {
this.get_data( resolution, tile_index );
return;
}
var data = this.cache.get(key);
var data = this.data_cache.get(key);
canvas.css( {
position: "absolute",
top: 0,
@@ -345,7 +370,14 @@ $.extend( LineTrack.prototype, TiledTrack.prototype, {
} else {
// Translate
x = x * w_scale;
y = (y - this.min_value) / this.vertical_range * this.height_px;
// console.log(y, this.min_value, this.vertical_range, (y - this.min_value) / this.vertical_range * this.height_px);
if (y <= this.min_value) {
y = this.min_value;
} else if (y >= this.max_value) {
y = this.max_value;
}
y = Math.round( this.height_px - (y - this.min_value) / this.vertical_range * this.height_px );
// console.log(canvas.get(0).height, canvas.get(0).width);
if ( in_path ) {
ctx.lineTo( x, y );
} else {
@@ -357,6 +389,29 @@ $.extend( LineTrack.prototype, TiledTrack.prototype, {
ctx.stroke();
parent_element.append( canvas );
return canvas;
}, gen_options: function(track_id) {
var container = $("<div></div>").addClass("form-row");
var minval = 'track_' + track_id + '_minval',
maxval = 'track_' + track_id + '_maxval',
min_label = $('<label></label>').attr("for", minval).text("Min value:"),
min_input = $('<input></input>').attr("id", minval).val(this.min_value || ""),
max_label = $('<label></label>').attr("for", maxval).text("Max value:"),
max_input = $('<input></input>').attr("id", maxval).val(this.max_value || "");
return container.append(min_label).append(min_input).append(max_label).append(max_input);
}, update_options: function(track_id) {
var min_value = $('#track_' + track_id + '_minval').val(),
max_value = $('#track_' + track_id + '_maxval').val();
if ( min_value !== this.min_value || max_value !== this.max_value) {
this.min_value = parseFloat(min_value);
this.max_value = parseFloat(max_value);
this.vertical_range = this.max_value - this.min_value;
$('#linetrack_' + track_id + '_minval').text(this.min_value);
$('#linetrack_' + track_id + '_maxval').text(this.max_value);
this.tile_cache.clear();
this.draw();
}
}
});
@@ -374,7 +429,8 @@ var FeatureTrack = function ( name, dataset_id, indexer, height ) {
this.showing_details = false;
this.vertical_detail_px = 10;
this.vertical_nodetail_px = 3;
this.base_color = "#2C3143";
this.block_color = "black";
this.label_color = "black";
this.default_font = "9px Monaco, Lucida Console, monospace";
this.left_offset = 200;
this.inc_slots = {};
@@ -564,7 +620,7 @@ $.extend( FeatureTrack.prototype, TiledTrack.prototype, {
new_canvas.get(0).height = required_height;
// console.log(( tile_low - this.view.low ) * w_scale, tile_index, w_scale);
var ctx = new_canvas.get(0).getContext("2d");
ctx.fillStyle = this.base_color;
ctx.fillStyle = this.block_color;
ctx.font = this.default_font;
ctx.textAlign = "right";
@@ -586,10 +642,10 @@ $.extend( FeatureTrack.prototype, TiledTrack.prototype, {
ctx.fillRect(f_start + this.left_offset, y_center + 5, f_end - f_start, 1);
} else {
// Showing labels, blocks, details
if (ctx.fillText && feature.start > tile_low) {
if (feature.start > tile_low) {
ctx.fillStyle = this.label_color;
ctx.fillText(feature.name, f_start - 1 + this.left_offset, y_center + 8);
// ctx.fillText(commatize(feature.start), f_start - 1 + this.left_offset, y_center + 8);
// ctx.fillText(slots[feature.uid], f_start - 1 + this.left_offset, y_center + 8);
ctx.fillStyle = this.block_color;
}
var blocks = feature.blocks;
if (blocks) {
@@ -601,7 +657,7 @@ $.extend( FeatureTrack.prototype, TiledTrack.prototype, {
ctx.fillStyle = LEFT_STRAND;
}
ctx.fillRect(f_start + this.left_offset, y_center, f_end - f_start, 10);
ctx.fillStyle = this.base_color;
ctx.fillStyle = this.block_color;
}
for (var k = 0, k_len = blocks.length; k < k_len; k++) {
@@ -614,7 +670,8 @@ $.extend( FeatureTrack.prototype, TiledTrack.prototype, {
y_start = 3;
ctx.fillRect(block_start + this.left_offset, y_center + y_start, block_end - block_start, thickness);
if (thick_start !== undefined && (block_start <= thick_end || block_end >= thick_start) ) {
// Draw thick regions: check if block intersects with thick region
if (thick_start !== undefined && !(block_start > thick_end || block_end < thick_start) ) {
thickness = 9;
y_start = 1;
var block_thick_start = Math.max(block_start, thick_start),
@@ -635,7 +692,7 @@ $.extend( FeatureTrack.prototype, TiledTrack.prototype, {
ctx.fillStyle = LEFT_STRAND_INV;
}
ctx.fillRect(f_start + this.left_offset, y_center, f_end - f_start, 10);
ctx.fillStyle = this.base_color;
ctx.fillStyle = this.block_color;
}
}
}
@@ -645,6 +702,25 @@ $.extend( FeatureTrack.prototype, TiledTrack.prototype, {
parent_element.append( new_canvas );
return new_canvas;
}, gen_options: function(track_id) {
var container = $("<div></div>").addClass("form-row");
var block_color = 'track_' + track_id + '_block_color',
block_color_label = $('<label></label>').attr("for", block_color).text("Block color:"),
block_color_input = $('<input></input>').attr("id", block_color).attr("name", block_color).val(this.block_color),
label_color = 'track_' + track_id + '_label_color',
label_color_label = $('<label></label>').attr("for", label_color).text("Label color:"),
label_color_input = $('<input></input>').attr("id", label_color).attr("name", label_color).val(this.label_color);
return container.append(block_color_label).append(block_color_input).append(label_color_label).append(label_color_input);
}, update_options: function(track_id) {
var block_color = $('#track_' + track_id + '_block_color').val(),
label_color = $('#track_' + track_id + '_label_color').val();
if (block_color !== this.block_color || label_color !== this.label_color) {
this.block_color = block_color;
this.label_color = label_color;
this.tile_cache.clear();
this.draw();
}
}
});
@@ -684,7 +760,7 @@ $.extend( ReadTrack.prototype, TiledTrack.prototype, FeatureTrack.prototype, {
new_canvas.get(0).height = required_height;
// console.log(( tile_low - this.view.low ) * w_scale, tile_index, w_scale);
var ctx = new_canvas.get(0).getContext("2d");
ctx.fillStyle = this.base_color;
ctx.fillStyle = this.block_color;
ctx.font = this.default_font;
ctx.textAlign = "right";
var px_per_char = ctx.measureText("A").width;
+1 -1
View File
@@ -109,7 +109,7 @@ input {
}
.track-header {
text-align: left;
text-align: center;
padding: 4px;
color: #666;
}
+1 -1
View File
@@ -20,7 +20,7 @@ ${h.js( "jquery", "json2", "jquery.jstore-all", "jquery.autocomplete", "autocomp
<script type="text/javascript">
$(function() {
// Load jStore for local storage
$.extend(jQuery.jStore.defaults, { project: 'galaxy', flash: '/static/jStore.Flash.html' })
$.extend(jQuery.jStore.defaults, { project: 'galaxy', flash: '${h.url_for("/static/jStore.Flash.html")}' })
$.jStore.load(); // Auto-select best storage
$.jStore.ready(function(engine) {
+120 -42
View File
@@ -11,12 +11,84 @@
<%def name="stylesheets()">
${parent.stylesheets()}
<link rel="stylesheet" type="text/css" href="/static/trackster.css" />
${h.css( "history" )}
<link rel="stylesheet" type="text/css" href="${h.url_for('/static/trackster.css')}" />
<link rel="stylesheet" type="text/css" href="${h.url_for('/static/ui.theme.css')}" />
<style type="text/css">
ul#sortable-ul {
list-style: none;
padding: 0;
margin: 5px;
}
ul#sortable-ul li {
display: block;
margin: 5px 0;
background: #eee;
}
.delete-button {
background: transparent url(history-buttons.png) no-repeat scroll 0 -104px;
height: 20px;
width: 20px;
}
.delete-button:hover {
background-position: 0 -130px;
}
</style>
</%def>
<%def name="center_panel()">
<div id="content">
<div id="top-labeltrack"></div>
<div id="viewport-container" style="overflow-x: hidden; overflow-y: auto;">
<div id="viewport"></div>
</div>
</div>
<div id="nav-container">
<div id="nav-labeltrack"></div>
<div id="nav">
<div id="overview">
<div id="overview-viewport">
<div id="overview-box"></div>
</div>
</div>
<div id="nav-controls">
<form name="chr" id="chr" method="get">
<select id="chrom" name="chrom" style="width: 15em;">
<option value="">Loading</option>
</select>
<input id="low" size="12" />:<input id="high" size="12" />
## <input type="hidden" name="dataset_ids" value="${dataset_ids}" />
<input type="hidden" name="id" value="${id}" />
<a href="#" onclick="javascript:view.zoom_in();view.redraw();">+</a>
<a href="#" onclick="javascript:view.zoom_out();view.redraw();">-</a>
</form>
<div id="debug" style="float: right"></div>
</div>
</div>
</div>
</%def>
<%def name="right_panel()">
<div class="unified-panel-header" unselectable="on">
<div class="unified-panel-header-inner">Configuration</div>
</div>
<form action="${h.url_for( action='update_config' )}">
## <input name="title" id="title" value="${title}" />
<div id="show-hide-move">
<ul id="sortable-ul"></ul>
## <input type="submit" id="update-config" value="Save settings" />
<input type="button" id="refresh-button" value="Refresh" />
</div>
</form>
</%def>
<%def name="javascripts()">
${parent.javascripts()}
${h.js( "jquery", "jquery.event.drag", "jquery.mousewheel", "trackster" )}
${h.js( "jquery", "jquery.event.drag", "jquery.mousewheel", "trackster", "ui.core", "ui.sortable" )}
<script type="text/javascript">
@@ -36,7 +108,7 @@ ${h.js( "jquery", "jquery.event.drag", "jquery.mousewheel", "trackster" )}
view.redraw();
});
$(document).bind("mousewheel", function( e, delta ) {
$("#content").bind("mousewheel", function( e, delta ) {
if (delta > 0) {
view.zoom_in(e.pageX);
} else {
@@ -45,7 +117,7 @@ ${h.js( "jquery", "jquery.event.drag", "jquery.mousewheel", "trackster" )}
e.preventDefault();
});
$(document).bind("dblclick", function( e ) {
$("#content").bind("dblclick", function( e ) {
view.zoom_in(e.pageX);
});
@@ -83,7 +155,6 @@ ${h.js( "jquery", "jquery.event.drag", "jquery.mousewheel", "trackster" )}
var new_scroll = container.scrollTop() - (e.clientY - this.current_height);
if ( new_scroll < container.get(0).scrollHeight - container.height() ) {
container.scrollTop(new_scroll);
}
this.current_height = e.clientY;
this.current_x = e.offsetX;
@@ -92,6 +163,17 @@ ${h.js( "jquery", "jquery.event.drag", "jquery.mousewheel", "trackster" )}
view.center -= delta_chrom;
view.redraw();
});
$("#refresh-button").bind( "click", function(e) {
for (var track_id in view.tracks) {
var track = view.tracks[track_id];
if (track.update_options) {
track.update_options(track_id);
}
}
});
// Execute this on page load
(function () {
$.getJSON( "${h.url_for( action='chroms' )}", { dbkey: "${dbkey}" }, function ( data ) {
var chrom_options = '<option value="">Select Chrom/Contig</option>';
@@ -108,6 +190,39 @@ ${h.js( "jquery", "jquery.event.drag", "jquery.mousewheel", "trackster" )}
$("#chr").submit();
});
});
// Populate sort/move ul
for (var track_id in view.tracks) {
var track = view.tracks[track_id];
if (!track.hidden) {
var label = $('<label for="track_' + track_id + 'title">' + track.name + '</label>');
var title = $('<div class="toolFormTitle"></div>');
var del_icon = $('<a style="display:block; float:right" href="#" class="icon-button delete" />');
var body = $('<div class="toolFormBody"></div>');
// var checkbox = $('<input type="checkbox" checked="checked"></input>').attr("id", "track_" + track_id + "title");
var li = $('<li class="sortable"></li>').attr("id", "track_" + track_id);
var div = $('<div class="toolForm"></div>');
del_icon.prependTo(title);
label.appendTo(title);
// checkbox.prependTo(title);
if (track.gen_options) {
body.append(track.gen_options(track_id));
}
title.prependTo(div);
body.appendTo(div);
li.append(div);
$("ul#sortable-ul").append(li);
}
}
$("ul#sortable-ul").sortable({
update: function(event, ui) {
for (var track_id in view.tracks) {
var track = view.tracks[track_id];
}
}
});
})();
$(window).trigger("resize");
});
@@ -115,40 +230,3 @@ ${h.js( "jquery", "jquery.event.drag", "jquery.mousewheel", "trackster" )}
</script>
</%def>
<%def name="center_panel()">
<div id="content">
<div id="top-labeltrack"></div>
<div id="viewport-container" style="overflow-x: hidden; overflow-y: auto;">
<div id="viewport"></div>
</div>
</div>
<div id="nav-container">
<div id="nav-labeltrack"></div>
<div id="nav">
<div id="overview">
<div id="overview-viewport">
<div id="overview-box"></div>
</div>
</div>
<div id="nav-controls">
<form name="chr" id="chr" method="get">
<select id="chrom" name="chrom" style="width: 15em;">
<option value="">Loading</option>
</select>
<input id="low" size="12" />:<input id="high" size="12" />
## <input type="hidden" name="dataset_ids" value="${dataset_ids}" />
<input type="hidden" name="id" value="${id}" />
<a href="#" onclick="javascript:view.zoom_in();view.redraw();">+</a>
<a href="#" onclick="javascript:view.zoom_out();view.redraw();">-</a>
</form>
<div id="debug" style="float: right"></div>
</div>
</div>
</div>
</%def>
<%def name="right_panel()">
<div>Configs</div>
</%def>
+1 -1
View File
@@ -59,7 +59,7 @@
}
// Load jStore for local storage
$.extend(jQuery.jStore.defaults, { project: 'galaxy', flash: '/static/jStore.Flash.html' })
$.extend(jQuery.jStore.defaults, { project: 'galaxy', flash: '${h.url_for("/static/jStore.Flash.html")}' })
$.jStore.load(); // Auto-select best storage
// Canvas overview management