trackster: feature tracks auto-resize to fit data, prepare for LRU cache

This commit is contained in:
Kanwei Li
2009-10-01 14:01:53 -04:00
parent c41b730f7c
commit ccc06268f9
5 changed files with 280 additions and 31 deletions
+238
View File
@@ -0,0 +1,238 @@
/*
MIT LICENSE
Copyright (c) 2007 Monsur Hossain (http://www.monsur.com)
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/
// ****************************************************************************
// CachePriority ENUM
// An easier way to refer to the priority of a cache item
var CachePriority = {
Low: 1,
Normal: 2,
High: 4
}
// ****************************************************************************
// Cache constructor
// Creates a new cache object
// INPUT: maxSize (optional) - indicates how many items the cache can hold.
// default is -1, which means no limit on the
// number of items.
function Cache(maxSize) {
this.items = {};
this.count = 0;
if (maxSize == null)
maxSize = -1;
this.maxSize = maxSize;
this.fillFactor = .75;
this.purgeSize = Math.round(this.maxSize * this.fillFactor);
this.stats = {}
this.stats.hits = 0;
this.stats.misses = 0;
}
// ****************************************************************************
// Cache.getItem
// retrieves an item from the cache, returns null if the item doesn't exist
// or it is expired.
// INPUT: key - the key to load from the cache
Cache.prototype.getItem = function(key) {
// retrieve the item from the cache
var item = this.items[key];
if (item != null) {
if (!this._isExpired(item)) {
// if the item is not expired
// update its last accessed date
item.lastAccessed = new Date().getTime();
} else {
// if the item is expired, remove it from the cache
this._removeItem(key);
item = null;
}
}
// return the item value (if it exists), or null
var returnVal = null;
if (item != null) {
returnVal = item.value;
this.stats.hits++;
} else {
this.stats.misses++;
}
return returnVal;
}
// ****************************************************************************
// Cache.setItem
// sets an item in the cache
// parameters: key - the key to refer to the object
// value - the object to cache
// options - an optional parameter described below
// the last parameter accepts an object which controls various caching options:
// expirationAbsolute: the datetime when the item should expire
// expirationSliding: an integer representing the seconds since
// the last cache access after which the item
// should expire
// priority: How important it is to leave this item in the cache.
// You can use the values CachePriority.Low, .Normal, or
// .High, or you can just use an integer. Note that
// placing a priority on an item does not guarantee
// it will remain in cache. It can still be purged if
// an expiration is hit, or if the cache is full.
// callback: A function that gets called when the item is purged
// from cache. The key and value of the removed item
// are passed as parameters to the callback function.
Cache.prototype.setItem = function(key, value, options) {
function CacheItem(k, v, o) {
if ((k == null) || (k == ''))
throw new Error("key cannot be null or empty");
this.key = k;
this.value = v;
if (o == null)
o = {};
if (o.expirationAbsolute != null)
o.expirationAbsolute = o.expirationAbsolute.getTime();
if (o.priority == null)
o.priority = CachePriority.Normal;
this.options = o;
this.lastAccessed = new Date().getTime();
}
// add a new cache item to the cache
if (this.items[key] != null)
this._removeItem(key);
this._addItem(new CacheItem(key, value, options));
// if the cache is full, purge it
if ((this.maxSize > 0) && (this.count > this.maxSize)) {
this._purge();
}
}
// ****************************************************************************
// Cache.clear
// Remove all items from the cache
Cache.prototype.clear = function() {
// loop through each item in the cache and remove it
for (var key in this.items) {
this._removeItem(key);
}
}
// ****************************************************************************
// Cache._purge (PRIVATE FUNCTION)
// remove old elements from the cache
Cache.prototype._purge = function() {
var tmparray = new Array();
// loop through the cache, expire items that should be expired
// otherwise, add the item to an array
for (var key in this.items) {
var item = this.items[key];
if (this._isExpired(item)) {
this._removeItem(key);
} else {
tmparray.push(item);
}
}
if (tmparray.length > this.purgeSize) {
// sort this array based on cache priority and the last accessed date
tmparray = tmparray.sort(function(a, b) {
if (a.options.priority != b.options.priority) {
return b.options.priority - a.options.priority;
} else {
return b.lastAccessed - a.lastAccessed;
}
});
// remove items from the end of the array
while (tmparray.length > this.purgeSize) {
var ritem = tmparray.pop();
this._removeItem(ritem.key);
}
}
}
// ****************************************************************************
// Cache._addItem (PRIVATE FUNCTION)
// add an item to the cache
Cache.prototype._addItem = function(item) {
this.items[item.key] = item;
this.count++;
}
// ****************************************************************************
// Cache._removeItem (PRIVATE FUNCTION)
// Remove an item from the cache, call the callback function (if necessary)
Cache.prototype._removeItem = function(key) {
var item = this.items[key];
delete this.items[key];
this.count--;
// if there is a callback function, call it at the end of execution
if (item.options.callback != null) {
var callback = function() {
item.options.callback(item.key, item.value);
}
setTimeout(callback, 0);
}
}
// ****************************************************************************
// Cache._isExpired (PRIVATE FUNCTION)
// Returns true if the item should be expired based on its expiration options
Cache.prototype._isExpired = function(item) {
var now = new Date().getTime();
var expired = false;
if ((item.options.expirationAbsolute) && (item.options.expirationAbsolute < now)) {
// if the absolute expiration has passed, expire the item
expired = true;
}
if ((expired == false) && (item.options.expirationSliding)) {
// if the sliding expiration has passed, expire the item
var lastAccess = item.lastAccessed + (item.options.expirationSliding * 1000);
if (lastAccess < now) {
expired = true;
}
}
return expired;
}
Cache.prototype.toHtmlString = function() {
var returnStr = this.count + " item(s) in cache<br /><ul>";
for (var key in this.items) {
var item = this.items[key];
returnStr = returnStr + "<li>" + item.key.toString() + " = " + item.value.toString() + "</li>";
}
returnStr = returnStr + "</ul>";
return returnStr;
}
+1
View File
@@ -0,0 +1 @@
var CachePriority={Low:1,Normal:2,High:4};function Cache(a){this.items={};this.count=0;if(a==null){a=-1}this.maxSize=a;this.fillFactor=0.75;this.purgeSize=Math.round(this.maxSize*this.fillFactor);this.stats={};this.stats.hits=0;this.stats.misses=0}Cache.prototype.getItem=function(a){var c=this.items[a];if(c!=null){if(!this._isExpired(c)){c.lastAccessed=new Date().getTime()}else{this._removeItem(a);c=null}}var b=null;if(c!=null){b=c.value;this.stats.hits++}else{this.stats.misses++}return b};Cache.prototype.setItem=function(c,d,b){function a(f,e,g){if((f==null)||(f=="")){throw new Error("key cannot be null or empty")}this.key=f;this.value=e;if(g==null){g={}}if(g.expirationAbsolute!=null){g.expirationAbsolute=g.expirationAbsolute.getTime()}if(g.priority==null){g.priority=CachePriority.Normal}this.options=g;this.lastAccessed=new Date().getTime()}if(this.items[c]!=null){this._removeItem(c)}this._addItem(new a(c,d,b));if((this.maxSize>0)&&(this.count>this.maxSize)){this._purge()}};Cache.prototype.clear=function(){for(var a in this.items){this._removeItem(a)}};Cache.prototype._purge=function(){var d=new Array();for(var a in this.items){var b=this.items[a];if(this._isExpired(b)){this._removeItem(a)}else{d.push(b)}}if(d.length>this.purgeSize){d=d.sort(function(f,e){if(f.options.priority!=e.options.priority){return e.options.priority-f.options.priority}else{return e.lastAccessed-f.lastAccessed}});while(d.length>this.purgeSize){var c=d.pop();this._removeItem(c.key)}}};Cache.prototype._addItem=function(a){this.items[a.key]=a;this.count++};Cache.prototype._removeItem=function(a){var b=this.items[a];delete this.items[a];this.count--;if(b.options.callback!=null){var c=function(){b.options.callback(b.key,b.value)};setTimeout(c,0)}};Cache.prototype._isExpired=function(c){var a=new Date().getTime();var b=false;if((c.options.expirationAbsolute)&&(c.options.expirationAbsolute<a)){b=true}if((b==false)&&(c.options.expirationSliding)){var d=c.lastAccessed+(c.options.expirationSliding*1000);if(d<a){b=true}}return b};Cache.prototype.toHtmlString=function(){var b=this.count+" item(s) in cache<br /><ul>";for(var a in this.items){var c=this.items[a];b=b+"<li>"+c.key.toString()+" = "+c.value.toString()+"</li>"}b=b+"</ul>";return b};
File diff suppressed because one or more lines are too long
+39 -29
View File
@@ -2,7 +2,9 @@
2009, James Taylor, Kanwei Li
*/
var DENSITY = 1000;
var DENSITY = 1000,
DATA_ERROR = "There was an error in indexing this dataset.",
DATA_NONE = "No data for this chrom/contig.";
var DataCache = function( type, track ) {
this.type = type;
@@ -239,18 +241,16 @@ $.extend( LineTrack.prototype, TiledTrack.prototype, {
var track = this;
$.getJSON( data_url, { stats: true, track_type: track.track_type, chrom: track.view.chrom,
low: null, high: null, dataset_id: track.dataset_id }, function ( data ) {
if (data) {
if (data == "error") {
track.content_div.addClass("error").text("There was an error in indexing this dataset.");
} else if (data == "no data") {
// console.log(track.content_div);
track.content_div.addClass("nodata").text("No data for this chrom/contig.");
} else {
track.min_value = data['min'];
track.max_value = data['max'];
track.vertical_range = track.max_value - track.min_value;
track.view.redraw();
}
if (!data || data == "error") {
track.content_div.addClass("error").text(DATA_ERROR);
} else if (data == "no data") {
// console.log(track.content_div);
track.content_div.addClass("nodata").text(DATA_NONE);
} else {
track.min_value = data['min'];
track.max_value = data['max'];
track.vertical_range = track.max_value - track.min_value;
track.view.redraw();
}
});
},
@@ -321,27 +321,28 @@ var FeatureTrack = function ( name, dataset_id, height ) {
this.zo_slots = {};
this.show_labels_scale = 0.01;
this.showing_labels = false;
this.vertical_gap = 10;
};
$.extend( FeatureTrack.prototype, TiledTrack.prototype, {
calc_slots: function( include_labels ) {
// console.log("num vals: " + this.values.length);
var end_ary = new Array();
var end_ary = [];
var scale = this.container_div.width() / (this.view.high - this.view.low);
// console.log(scale);
if (include_labels) this.zi_slots = new Object();
// console.log(scale, this.view.high, this.view.low);
if (include_labels) this.zi_slots = {};
var dummy_canvas = $("<canvas></canvas>").get(0).getContext("2d");
for (var i in this.values) {
feature = this.values[i];
f_start = Math.floor( Math.max(this.view.max_low, (feature.start - this.view.max_low) * scale) );
var feature = this.values[i];
var f_start = Math.floor( Math.max(this.view.max_low, (feature.start - this.view.max_low) * scale) );
if (include_labels) {
f_start -= dummy_canvas.measureText(feature.name).width;
f_start -= 10; // Spacing between text and line
}
f_end = Math.ceil( Math.min(this.view.max_high, (feature.end - this.view.max_low) * scale) );
var f_end = Math.ceil( Math.min(this.view.max_high, (feature.end - this.view.max_low) * scale) );
// if (include_labels) { console.log(f_start, f_end); }
j = 0;
var j = 0;
while (true) {
if (end_ary[j] == undefined || end_ary[j] < f_start) {
end_ary[j] = f_end;
@@ -355,17 +356,26 @@ $.extend( FeatureTrack.prototype, TiledTrack.prototype, {
j++;
}
}
this.height_px = end_ary.length * this.vertical_gap + 15;
this.content_div.css( "height", this.height_px + "px" );
},
init: function() {
var track = this;
$.getJSON( data_url, { track_type: track.track_type, low: track.view.max_low, high: track.view.max_high,
dataset_id: track.dataset_id, chrom: track.view.chrom }, function ( data ) {
track.values = data;
track.calc_slots();
track.slots = track.zo_slots;
// console.log(track.zo_slots);
track.draw();
if (data == "error") {
track.content_div.addClass("error").text(DATA_ERROR);
} else if (data.length == 0 || data == "no data") {
// console.log(track.content_div);
track.content_div.addClass("nodata").text(DATA_NONE);
} else {
track.values = data;
track.calc_slots();
track.slots = track.zo_slots;
// console.log(track.zo_slots);
track.draw();
}
});
},
@@ -391,7 +401,7 @@ $.extend( FeatureTrack.prototype, TiledTrack.prototype, {
range = view.high - view.low,
width = Math.ceil( tile_length * w_scale ),
slots = new Array(),
height = 200,
height = this.height_px,
new_canvas = $("<canvas class='tile'></canvas>");
new_canvas.css({
@@ -412,7 +422,7 @@ $.extend( FeatureTrack.prototype, TiledTrack.prototype, {
f_end = Math.ceil( Math.min(width, (feature.end - tile_low) * w_scale) );
// console.log(feature.start, feature.end, f_start, f_end, j);
ctx.fillStyle = "#000";
ctx.fillRect(f_start, this.slots[feature.name] * 10 + 5, f_end - f_start, 1);
ctx.fillRect(f_start, this.slots[feature.name] * this.vertical_gap + 5, f_end - f_start, 1);
if (this.showing_labels && ctx.fillText) {
ctx.font = "10px monospace";
@@ -435,7 +445,7 @@ $.extend( FeatureTrack.prototype, TiledTrack.prototype, {
if (exon_start && block_start >= exon_start && block_end <= exon_end) {
thickness = 5, y_start = 3;
}
ctx.fillRect(exon_start, this.slots[feature.name] * 10 + y_start, block_end - block_start, thickness);
ctx.fillRect(exon_start, this.slots[feature.name] * this.vertical_gap + y_start, block_end - block_start, thickness);
// console.log(block_start, block_end);
}
+1 -1
View File
@@ -86,7 +86,7 @@ body {
.track-content.error {
text-align: center;
padding-top: 30px;
background-color: #600;
background-color: #ECB4AF;
}
.track-content.nodata {
text-align: center;