diff --git a/cubism.v1.js b/cubism.v1.js index 375c35e..8d898f5 100644 --- a/cubism.v1.js +++ b/cubism.v1.js @@ -28,42 +28,60 @@ cubism.context = function() { start1, stop1, // the start and stop for the next prepare event serverDelay = 5e3, clientDelay = 5e3, + seek = 0, event = d3.dispatch("prepare", "beforechange", "change", "focus"), scale = context.scale = d3.time.scale().range([0, size]), timeout, focus; function update() { - var now = Date.now(); - stop0 = new Date(Math.floor((now - serverDelay - clientDelay) / step) * step); - start0 = new Date(stop0 - size * step); - stop1 = new Date(Math.floor((now - serverDelay) / step) * step); - start1 = new Date(stop1 - size * step); - scale.domain([start0, stop0]); + if(seek != 0){ + stop0 = new Date(Math.floor(seek / step) * step); + start0 = new Date(stop0 - size * step); + stop1 = stop0; + start1 = start0; + scale.domain([start0, stop0]); + } else { + var now = Date.now(); + stop0 = new Date(Math.floor((now - serverDelay - clientDelay) / step) * step); + start0 = new Date(stop0 - size * step); + stop1 = new Date(Math.floor((now - serverDelay) / step) * step); + start1 = new Date(stop1 - size * step); + scale.domain([start0, stop0]); + } return context; } context.start = function() { - if (timeout) clearTimeout(timeout); - var delay = +stop1 + serverDelay - Date.now(); - - // If we're too late for the first prepare event, skip it. - if (delay < clientDelay) delay += step; - - timeout = setTimeout(function prepare() { - stop1 = new Date(Math.floor((Date.now() - serverDelay) / step) * step); - start1 = new Date(stop1 - size * step); - event.prepare.call(context, start1, stop1); - - setTimeout(function() { - scale.domain([start0 = start1, stop0 = stop1]); + if(seek != 0) { + event.prepare.call(context, start1, stop1, function(){ event.beforechange.call(context, start1, stop1); event.change.call(context, start1, stop1); - event.focus.call(context, focus); - }, clientDelay); - - timeout = setTimeout(prepare, step); - }, delay); + event.focus.call(context, focus); + }); + } else { + // Realtime + if (timeout) clearTimeout(timeout); + var delay = +stop1 + serverDelay - Date.now(); + + // If we're too late for the first prepare event, skip it. + if (delay < clientDelay) delay += step; + + timeout = setTimeout(function prepare() { + stop1 = new Date(Math.floor((Date.now() - serverDelay) / step) * step); + start1 = new Date(stop1 - size * step); + event.prepare.call(context, start1, stop1); + + setTimeout(function() { + scale.domain([start0 = start1, stop0 = stop1]); + event.beforechange.call(context, start1, stop1); + event.change.call(context, start1, stop1); + event.focus.call(context, focus); + }, clientDelay); + + timeout = setTimeout(prepare, step); + }, delay); + } return context; }; @@ -74,6 +92,14 @@ cubism.context = function() { timeout = setTimeout(context.start, 10); + // seek = fixed end time + // if seek = 0 moves back to real time + context.seek = function(_) { + if(!arguments.length) return seek; + seek = +_; + return update().stop().start(); + }; + // Set or get the step interval in milliseconds. // Defaults to ten seconds. context.step = function(_) { @@ -609,25 +635,49 @@ cubism_contextPrototype.metric = function(request, name) { fetching; // Prefetch new data into a temporary array. - function prepare(start1, stop) { - var steps = Math.min(size, Math.round((start1 - start) / step)); - if (!steps || fetching) return; // already fetched, or fetching! - fetching = true; - steps = Math.min(size, steps + cubism_metricOverlap); - var start0 = new Date(stop - steps * step); - request(start0, stop, step, function(error, data) { - fetching = false; - if (error) return console.warn(error); - var i = isFinite(start) ? Math.round((start0 - start) / step) : 0; - for (var j = 0, m = data.length; j < m; ++j) values[j + i] = data[j]; - event.change.call(metric, start, stop); - }); + function prepare(start1, stop, callback) { + if(start1 > start) { + var steps = Math.min(size, Math.round((start1 - start) / step)); + if (!steps || fetching) return; // already fetched, or fetching! + fetching = true; + steps = Math.min(size, steps + cubism_metricOverlap); + var start0 = new Date(stop - steps * step); + request(start0, stop, step, function(error, data) { + fetching = false; + if (error) return console.warn(error); + var i = isFinite(start) ? Math.round((start0 - start) / step) : 0; + for (var j = 0, m = data.length; j < m; ++j) values[j + i] = data[j]; + event.change.call(metric, start, stop); + if(callback != null) callback(); + }); + } else { + var steps = Math.min(size, Math.round((start - start1) / step)); + if (!steps || fetching) return; + fetching = true; + // Must be seeking backward... don't need to overlap since we are + // not in realtime + //steps = Math.min(size, steps + cubism_metricOverlap); + var stop0 = new Date(+start1 + steps * step); + request(start1, stop0, step, function(error, data) { + fetching = false; + if(error) return console.warn(error); + prevalues = [] + for (var j = 0, m = data.length; j < m; ++j) prevalues[j] = data[j]; + values = prevalues.concat(values); + event.change.call(metric, start, stop); + if(callback != null) callback(); + }); + } } // When the context changes, switch to the new data, ready-or-not! function beforechange(start1, stop1) { if (!isFinite(start)) start = start1; - values.splice(0, Math.max(0, Math.min(size, Math.round((start1 - start) / step)))); + + if(start1 > start) + values.splice(0, Math.max(0, Math.min(size, Math.round((start1 - start) / step)))); + else + values.splice(stop1, Math.max(0, Math.min(size, Math.round((start - start1) / step)))); start = start1; stop = stop1; } @@ -808,17 +858,34 @@ cubism_contextPrototype.horizon = function() { // if this is an update (with no extent change), copy old values! var i0 = 0, max = Math.max(-extent[0], extent[1]); + var iEnd = width; + if (this === context) { if (max == max_) { - i0 = width - cubism_metricOverlap; - var dx = (start1 - start) / step; - if (dx < width) { - var canvas0 = buffer.getContext("2d"); - canvas0.clearRect(0, 0, width, height); - canvas0.drawImage(canvas.canvas, dx, 0, width - dx, height, 0, 0, width - dx, height); - canvas.clearRect(0, 0, width, height); - canvas.drawImage(canvas0.canvas, 0, 0); + if(start1 < start) { + // Need to repaint the left side (seeking, not realtime) + var dx = (start - start1) / step; + iEnd = dx; + if(dx < width) { + var canvas0 = buffer.getContext("2d"); + canvas0.clearRect(0, 0, width, height); + canvas0.drawImage(canvas.canvas, 0, 0, width - dx, height, dx, 0, width - dx, height); + canvas.clearRect(0,0, width, height); + canvas.drawImage(canvas0.canvas, 0, 0); + } + } else { + var dx = (start1 - start) / step; + i0 = width - dx - cubism_metricOverlap; + if (dx < width) { + var canvas0 = buffer.getContext("2d"); + canvas0.clearRect(0, 0, width, height); + canvas0.drawImage(canvas.canvas, dx, 0, width - dx, height, 0, 0, width - dx, height); + canvas.clearRect(0, 0, width, height); + canvas.drawImage(canvas0.canvas, 0, 0); + } } + } else { + canvas.clearRect(0,0,width,height); } start = start1; } @@ -827,7 +894,7 @@ cubism_contextPrototype.horizon = function() { scale.domain([0, max_ = max]); // clear for the new data - canvas.clearRect(i0, 0, width - i0, height); + canvas.clearRect(i0, 0, iEnd, height); // record whether there are negative values to display var negative; @@ -841,7 +908,7 @@ cubism_contextPrototype.horizon = function() { scale.range([m * height + y0, y0]); y0 = scale(0); - for (var i = i0, n = width, y1; i < n; ++i) { + for (var i = i0, n = iEnd, y1; i < n; ++i) { y1 = metric_.valueAt(i); if (y1 <= 0) { negative = true; continue; } if (y1 === undefined) continue; @@ -865,7 +932,7 @@ cubism_contextPrototype.horizon = function() { scale.range([m * height + y0, y0]); y0 = scale(0); - for (var i = i0, n = width, y1; i < n; ++i) { + for (var i = i0, n = iEnd, y1; i < n; ++i) { y1 = metric_.valueAt(i); if (y1 >= 0) continue; canvas.fillRect(i, scale(-y1), 1, y0 - scale(-y1)); diff --git a/cubism.v1.min.js b/cubism.v1.min.js index d8c13b1..56bab6d 100644 --- a/cubism.v1.min.js +++ b/cubism.v1.min.js @@ -1 +1 @@ -(function(a){function d(a){return a}function e(){}function j(a){return Math.floor(a/1e3)}function k(a){var b=a.indexOf("|"),c=a.substring(0,b),d=c.lastIndexOf(","),e=c.lastIndexOf(",",d-1),f=c.lastIndexOf(",",e-1),g=c.substring(f+1,e)*1e3,h=c.substring(d+1)*1e3;return a.substring(b+1).split(",").slice(1).map(function(a){return+a})}function l(a){if(!(a instanceof e))throw new Error("invalid context");this.context=a}function o(a,b){return function(c,d,e,f){a(new Date(+c+b),new Date(+d+b),e,f)}}function p(a,b){l.call(this,a),b=+b;var c=b+"";this.valueOf=function(){return b},this.toString=function(){return c}}function r(a,b){function c(b,c){if(c instanceof l){if(b.context!==c.context)throw new Error("mismatch context")}else c=new p(b.context,c);l.call(this,b.context),this.left=b,this.right=c,this.toString=function(){return b+" "+a+" "+c}}var d=c.prototype=Object.create(l.prototype);return d.valueAt=function(a){return b(this.left.valueAt(a),this.right.valueAt(a))},d.shift=function(a){return new c(this.left.shift(a),this.right.shift(a))},d.on=function(a,b){return arguments.length<2?this.left.on(a):(this.left.on(a,b),this.right.on(a,b),this)},function(a){return new c(this,a)}}function u(a){return a&16777214}function v(a){return(a+1&16777214)-1}function z(a){a.style("position","absolute").style("top",0).style("bottom",0).style("width","1px").style("pointer-events","none")}function A(a){return a+"px"}var b=a.cubism={version:"1.4.0"},c=0;b.option=function(a,c){var d=b.options(a);return d.length?d[0]:c},b.options=function(a,b){var c=location.search.substring(1).split("&"),d=[],e=-1,f=c.length,g;while(++e0&&a.focus(--o);break;case 39:o==null&&(o=d-2),o=c)return c;if(a<=b)return b;var d,e,f;for(f=a;f<=c;f++){d=avail_rsts.indexOf(f);if(d>-1){e=avail_rsts[d];break}}var g;for(f=a;f>=b;f--){d=avail_rsts.indexOf(f);if(d>-1){g=avail_rsts[d];break}}return e-ae?3600:(h=j(c),d>f&&h<900?900:d>g&&h<60?60:h)}var d={},e=this;auth_string="Basic "+btoa(a+":"+c),enable_log=!0,avail_rsts=[1,60,900,3600];var l=function(a,c){function e(b,c,e){var h="start_time="+b+"&end_time="+c+"&resolution="+k(b,c,e);return full_url=d+"/"+a+"?"+h,f("full_url = "+full_url),g(b,c,e),full_url}function h(a,b,c,d){var e=[];for(i=a;i<=b;i+=c){var f=[];while(d.length&&d[0].measure_time<=i)f.push(d.shift().value);var g;f.length?g=f.reduce(function(a,b){return a+b})/f.length:g=e.length?e[e.length-1]:0,e.push(g)}return e}var d="https://metrics-api.librato.com/v1/metrics";return request={},request.fire=function(a,d,g,i){function k(l){d3.json(l).header("X-Requested-With","XMLHttpRequest").header("Authorization",auth_string).header("Librato-User-Agent","cubism/"+b.version).get(function(b,l){if(!b){f("# of partial measurements: "+l.measurements[c].length),l.measurements[c].forEach(function(a){j.push(a)});var m="query"in l&&"next_time"in l.query;if(m)f("Requesting more values"),k(e(l.query.next_time,d,g));else{f("total number of measurements from librato: "+j.length);var n=h(a,d,g,j);f("number of measurements after adjusting time values: "+n.length),i(n)}}else f("There was an error when performing the librato request:"),f(b)})}var j=[];k(e(a,d,g))},request};return d.metric=function(a,b){return e.metric(function(c,d,e,f){l(a,b).fire(h(c),h(d),h(e),function(a){f(null,a)})},a+="")},d.toString=function(){return"librato"},d};var h=function(a){return Math.floor(a/1e3)};f.graphite=function(a){arguments.length||(a="");var b={},c=this;return b.metric=function(b){var d="sum",e=c.metric(function(c,e,f,g){var h=b;f!==1e4&&(h="summarize("+h+",'"+(f%36e5?f%6e4?f/1e3+"sec":f/6e4+"min":f/36e5+"hour")+"','"+d+"')"),d3.text(a+"/render?format=raw"+"&target="+encodeURIComponent("alias("+h+",'')")+"&from="+j(c-2*f)+"&until="+j(e-1e3),function(a){if(!a)return g(new Error("unable to load data"));g(null,k(a))})},b+="");return e.summarize=function(a){return d=a,e},e},b.find=function(b,c){d3.json(a+"/metrics/find?format=completer"+"&query="+encodeURIComponent(b),function(a){if(!a)return c(new Error("unable to find metrics"));c(null,a.metrics.map(function(a){return a.path}))})},b.toString=function(){return a},b},f.gangliaWeb=function(a){var b="",c="/ganglia2/";arguments.length&&(a.host&&(b=a.host),a.uriPathPrefix&&(c=a.uriPathPrefix,c[0]!="/"&&(c="/"+c),c[c.length-1]!="/"&&(c+="/")));var d={},e=this;return d.metric=function(a){var d=a.clusterName,f=a.metricName,g=a.hostName,h=a.isReport||!1,i=a.titleGenerator||function(a){return"clusterName:"+d+" metricName:"+f+(g?" hostName:"+g:"")},j=a.onChangeCallback,k=h?"g":"m",l=e.metric(function(a,e,h,i){function j(){return"c="+d+"&"+k+"="+f+(g?"&h="+g:"")+"&cs="+a/1e3+"&ce="+e/1e3+"&step="+h/1e3+"&graphlot=1"}d3.json(b+c+"graph.php?"+j(),function(a){if(!a)return i(new Error("Unable to fetch GangliaWeb data"));i(null,a[0].data)})},i(a));return l.toString=function(){return i(a)},j&&l.on("change",j),l},d.toString=function(){return b+c},d};var m=l.prototype;b.metric=l,m.valueAt=function(){return NaN},m.alias=function(a){return this.toString=function(){return a},this},m.extent=function(){var a=0,b=this.context.size(),c,d=Infinity,e=-Infinity;while(++ae&&(e=c);return[d,e]},m.on=function(a,b){return arguments.length<2?null:this},m.shift=function(){return this},m.on=function(){return arguments.length<2?null:this},f.metric=function(a,b){function r(b,c){var d=Math.min(j,Math.round((b-g)/i));if(!d||q)return;q=!0,d=Math.min(j,d+n);var f=new Date(c-d*i);a(f,c,i,function(a,b){q=!1;if(a)return console.warn(a);var d=isFinite(g)?Math.round((f-g)/i):0;for(var h=0,j=b.length;h1&&(e.toString=function(){return b}),e};var n=6,q=p.prototype=Object.create(l.prototype);q.valueAt=function(){return+this},q.extent=function(){return[+this,+this]},m.add=r("+",function(a,b){return a+b}),m.subtract=r("-",function(a,b){return a-b}),m.multiply=r("*",function(a,b){return a*b}),m.divide=r("/",function(a,b){return a/b}),f.horizon=function(){function o(o){o.on("mousemove.horizon",function(){a.focus(Math.round(d3.mouse(this)[0]))}).on("mouseout.horizon",function(){a.focus(null)}),o.append("canvas").attr("width",f).attr("height",g),o.append("span").attr("class","title").text(k),o.append("span").attr("class","value"),o.each(function(k,o){function B(c,d){w.save();var i=r.extent();A=i.every(isFinite),t!=null&&(i=t);var j=0,k=Math.max(-i[0],i[1]);if(this===a){if(k==y){j=f-n;var l=(c-u)/v;if(l=0)continue;w.fillRect(x,h(-C),1,q-h(-C))}}}w.restore()}function C(a){a==null&&(a=f-1);var b=r.valueAt(a);x.datum(b).text(isNaN(b)?null:l)}var p=this,q=++c,r=typeof i=="function"?i.call(p,k,o):i,s=typeof m=="function"?m.call(p,k,o):m,t=typeof j=="function"?j.call(p,k,o):j,u=-Infinity,v=a.step(),w=d3.select(p).select("canvas"),x=d3.select(p).select(".value"),y,z=s.length>>1,A;w.datum({id:q,metric:r}),w=w.node().getContext("2d"),a.on("change.horizon-"+q,B),a.on("focus.horizon-"+q,C),r.on("change.horizon-"+q,function(a,b){B(a,b),C(),A&&r.on("change.horizon-"+q,d)})})}var a=this,b="offset",e=document.createElement("canvas"),f=e.width=a.size(),g=e.height=30,h=d3.scale.linear().interpolate(d3.interpolateRound),i=d,j=null,k=d,l=d3.format(".2s"),m=["#08519c","#3182bd","#6baed6","#bdd7e7","#bae4b3","#74c476","#31a354","#006d2c"];return o.remove=function(b){function c(b){b.metric.on("change.horizon-"+b.id,null),a.on("change.horizon-"+b.id,null),a.on("focus.horizon-"+b.id,null)}b.on("mousemove.horizon",null).on("mouseout.horizon",null),b.selectAll("canvas").each(c).remove(),b.selectAll(".title,.value").remove()},o.mode=function(a){return arguments.length?(b=a+"",o):b},o.height=function(a){return arguments.length?(e.height=g=+a,o):g},o.metric=function(a){return arguments.length?(i=a,o):i},o.scale=function(a){return arguments.length?(h=a,o):h},o.extent=function(a){return arguments.length?(j=a,o):j},o.title=function(a){return arguments.length?(k=a,o):k},o.format=function(a){return arguments.length?(l=a,o):l},o.colors=function(a){return arguments.length?(m=a,o):m},o},f.comparison=function(){function o(o){o.on("mousemove.comparison",function(){a.focus(Math.round(d3.mouse(this)[0]))}).on("mouseout.comparison",function(){a.focus(null)}),o.append("canvas").attr("width",b).attr("height",e),o.append("span").attr("class","title").text(j),o.append("span").attr("class","value primary"),o.append("span").attr("class","value change"),o.each(function(j,o){function B(c,d){x.save(),x.clearRect(0,0,b,e);var g=r.extent(),h=s.extent(),i=t==null?g:t;f.domain(i).range([e,0]),A=g.concat(h).every(isFinite);var j=c/a.step()&1?v:u;x.fillStyle=m[2];for(var k=0,l=b;kp&&x.fillRect(j(k),p,1,o-p)}x.fillStyle=m[3];for(k=0;kp&&x.fillRect(j(k),o-n,1,n)}x.restore()}function C(a){a==null&&(a=b-1);var c=r.valueAt(a),d=s.valueAt(a),e=(c-d)/d;y.datum(c).text(isNaN(c)?null:k),z.datum(e).text(isNaN(e)?null:l).attr("class","value change "+(e>0?"positive":e<0?"negative":""))}function D(a,b){B(a,b),C(),A&&(r.on("change.comparison-"+q,d),s.on("change.comparison-"+q,d))}var p=this,q=++c,r=typeof g=="function"?g.call(p,j,o):g,s=typeof h=="function"?h.call(p,j,o):h,t=typeof i=="function"?i.call(p,j,o):i,w=d3.select(p),x=w.select("canvas"),y=w.select(".value.primary"),z=w.select(".value.change"),A;x.datum({id:q,primary:r,secondary:s}),x=x.node().getContext("2d"),r.on("change.comparison-"+q,D),s.on("change.comparison-"+q,D),a.on("change.comparison-"+q,B),a.on("focus.comparison-"+q,C)})}var a=this,b=a.size(),e=120,f=d3.scale.linear().interpolate(d3.interpolateRound),g=function(a){return a[0]},h=function(a){return a[1]},i=null,j=d,k=s,l=t,m=["#9ecae1","#225b84","#a1d99b","#22723a"],n=1.5;return o.remove=function(b){function c(b){b.primary.on("change.comparison-"+b.id,null),b.secondary.on("change.comparison-"+b.id,null),a.on("change.comparison-"+b.id,null),a.on("focus.comparison-"+b.id,null)}b.on("mousemove.comparison",null).on("mouseout.comparison",null),b.selectAll("canvas").each(c).remove(),b.selectAll(".title,.value").remove()},o.height=function(a){return arguments.length?(e=+a,o):e},o.primary=function(a){return arguments.length?(g=a,o):g},o.secondary=function(a){return arguments.length?(h=a,o):h},o.scale=function(a){return arguments.length?(f=a,o):f},o.extent=function(a){return arguments.length?(i=a,o):i},o.title=function(a){return arguments.length?(j=a,o):j},o.formatPrimary=function(a){return arguments.length?(k=a,o):k},o.formatChange=function(a){return arguments.length?(l=a,o):l},o.colors=function(a){return arguments.length?(m=a,o):m},o.strokeWidth=function(a){return arguments.length?(n=a,o):n},o};var s=d3.format(".2s"),t=d3.format("+.0%");f.axis=function(){function f(g){var h=++c,i,j=g.append("svg").datum({id:h}).attr("width",a.size()).attr("height",Math.max(28,-f.tickSize())).append("g").attr("transform","translate(0,"+(d.orient()==="top"?27:4)+")").call(d);a.on("change.axis-"+h,function(){j.call(d),i||(i=d3.select(j.node().appendChild(j.selectAll("text").node().cloneNode(!0))).style("display","none").text(null))}),a.on("focus.axis-"+h,function(a){if(i)if(a==null)i.style("display","none"),j.selectAll("text").style("fill-opacity",null);else{i.style("display",null).attr("x",a).text(e(b.invert(a)));var c=i.node().getComputedTextLength()+6;j.selectAll("text").style("fill-opacity",function(d){return Math.abs(b(d)-a)0)context.focus(--focus);break;case 39:if(focus==null)focus=size-2;if(focus=lowest_res)return lowest_res;if(step<=highest_res)return highest_res;var iof,top_res,i;for(i=step;i<=lowest_res;i++){iof=avail_rsts.indexOf(i);if(iof>-1){top_res=avail_rsts[iof];break}}var low_res;for(i=step;i>=highest_res;i--){iof=avail_rsts.indexOf(i);if(iof>-1){low_res=avail_rsts[iof];break}}return top_res-stepmonth)return 3600;ideal_res=find_ideal_librato_resolution(step);if(i_size>week&&ideal_res<900)return 900;else if(i_size>two_days&&ideal_res<60)return 60;else return ideal_res}var librato_request=function(metric,source){var url_prefix="https://metrics-api.librato.com/v1/metrics";function make_url(sdate,edate,step){var params="start_time="+sdate+"&end_time="+edate+"&resolution="+find_librato_resolution(sdate,edate,step);full_url=url_prefix+"/"+metric+"?"+params;log("full_url = "+full_url);log_interval(sdate,edate,step);return full_url}function down_up_sampling(isdate,iedate,step,librato_mm){var av=[];for(i=isdate;i<=iedate;i+=step){var int_mes=[];while(librato_mm.length&&librato_mm[0].measure_time<=i){int_mes.push(librato_mm.shift().value)}var v;if(int_mes.length){v=int_mes.reduce(function(a,b){return a+b})/int_mes.length}else{v=av.length?av[av.length-1]:0}av.push(v)}return av}request={};request.fire=function(isdate,iedate,step,callback_done){var a_values=[];function actual_request(full_url){d3.json(full_url).header("X-Requested-With","XMLHttpRequest").header("Authorization",auth_string).header("Librato-User-Agent","cubism/"+cubism.version).get(function(error,data){if(!error){log("# of partial measurements: "+data.measurements[source].length);data.measurements[source].forEach(function(o){a_values.push(o)});var still_more_values="query"in data&&"next_time"in data.query;if(still_more_values){log("Requesting more values");actual_request(make_url(data.query.next_time,iedate,step))}else{log("total number of measurements from librato: "+a_values.length);var a_adjusted=down_up_sampling(isdate,iedate,step,a_values);log("number of measurements after adjusting time values: "+a_adjusted.length);callback_done(a_adjusted)}}else{log("There was an error when performing the librato request:");log(error)}})}actual_request(make_url(isdate,iedate,step))};return request};source.metric=function(m_name,m_source){return context.metric(function(start,stop,step,callback){librato_request(m_name,m_source).fire(cubism_libratoFormatDate(start),cubism_libratoFormatDate(stop),cubism_libratoFormatDate(step),function(a_values){callback(null,a_values)})},m_name+="")};source.toString=function(){return"librato"};return source};var cubism_libratoFormatDate=function(time){return Math.floor(time/1e3)};cubism_contextPrototype.graphite=function(host){if(!arguments.length)host="";var source={},context=this;source.metric=function(expression){var sum="sum";var metric=context.metric(function(start,stop,step,callback){var target=expression;if(step!==1e4)target="summarize("+target+",'"+(!(step%36e5)?step/36e5+"hour":!(step%6e4)?step/6e4+"min":step/1e3+"sec")+"','"+sum+"')";d3.text(host+"/render?format=raw"+"&target="+encodeURIComponent("alias("+target+",'')")+"&from="+cubism_graphiteFormatDate(start-2*step)+"&until="+cubism_graphiteFormatDate(stop-1e3),function(text){if(!text)return callback(new Error("unable to load data"));callback(null,cubism_graphiteParse(text))})},expression+="");metric.summarize=function(_){sum=_;return metric};return metric};source.find=function(pattern,callback){d3.json(host+"/metrics/find?format=completer"+"&query="+encodeURIComponent(pattern),function(result){if(!result)return callback(new Error("unable to find metrics"));callback(null,result.metrics.map(function(d){return d.path}))})};source.toString=function(){return host};return source};function cubism_graphiteFormatDate(time){return Math.floor(time/1e3)}function cubism_graphiteParse(text){var i=text.indexOf("|"),meta=text.substring(0,i),c=meta.lastIndexOf(","),b=meta.lastIndexOf(",",c-1),a=meta.lastIndexOf(",",b-1),start=meta.substring(a+1,b)*1e3,step=meta.substring(c+1)*1e3;return text.substring(i+1).split(",").slice(1).map(function(d){return+d})}cubism_contextPrototype.gangliaWeb=function(config){var host="",uriPathPrefix="/ganglia2/";if(arguments.length){if(config.host){host=config.host}if(config.uriPathPrefix){uriPathPrefix=config.uriPathPrefix;if(uriPathPrefix[0]!="/"){uriPathPrefix="/"+uriPathPrefix}if(uriPathPrefix[uriPathPrefix.length-1]!="/"){uriPathPrefix+="/"}}}var source={},context=this;source.metric=function(metricInfo){var clusterName=metricInfo.clusterName,metricName=metricInfo.metricName,hostName=metricInfo.hostName,isReport=metricInfo.isReport||false,titleGenerator=metricInfo.titleGenerator||function(unusedMetricInfo){return"clusterName:"+clusterName+" metricName:"+metricName+(hostName?" hostName:"+hostName:"")},onChangeCallback=metricInfo.onChangeCallback;var metricKeyName=isReport?"g":"m";var gangliaWebMetric=context.metric(function(start,stop,step,callback){function constructGangliaWebRequestQueryParams(){return"c="+clusterName+"&"+metricKeyName+"="+metricName+(hostName?"&h="+hostName:"")+"&cs="+start/1e3+"&ce="+stop/1e3+"&step="+step/1e3+"&graphlot=1"}d3.json(host+uriPathPrefix+"graph.php?"+constructGangliaWebRequestQueryParams(),function(result){if(!result){return callback(new Error("Unable to fetch GangliaWeb data"))}callback(null,result[0].data)})},titleGenerator(metricInfo));gangliaWebMetric.toString=function(){return titleGenerator(metricInfo)};if(onChangeCallback){gangliaWebMetric.on("change",onChangeCallback)}return gangliaWebMetric};source.toString=function(){return host+uriPathPrefix};return source};function cubism_metric(context){if(!(context instanceof cubism_context))throw new Error("invalid context");this.context=context}var cubism_metricPrototype=cubism_metric.prototype;cubism.metric=cubism_metric;cubism_metricPrototype.valueAt=function(){return NaN};cubism_metricPrototype.alias=function(name){this.toString=function(){return name};return this};cubism_metricPrototype.extent=function(){var i=0,n=this.context.size(),value,min=Infinity,max=-Infinity;while(++imax)max=value}return[min,max]};cubism_metricPrototype.on=function(type,listener){return arguments.length<2?null:this};cubism_metricPrototype.shift=function(){return this};cubism_metricPrototype.on=function(){return arguments.length<2?null:this};cubism_contextPrototype.metric=function(request,name){var context=this,metric=new cubism_metric(context),id=".metric-"+ ++cubism_id,start=-Infinity,stop,step=context.step(),size=context.size(),values=[],event=d3.dispatch("change"),listening=0,fetching;function prepare(start1,stop,callback){if(start1>start){var steps=Math.min(size,Math.round((start1-start)/step));if(!steps||fetching)return;fetching=true;steps=Math.min(size,steps+cubism_metricOverlap);var start0=new Date(stop-steps*step);request(start0,stop,step,function(error,data){fetching=false;if(error)return console.warn(error);var i=isFinite(start)?Math.round((start0-start)/step):0;for(var j=0,m=data.length;jstart)values.splice(0,Math.max(0,Math.min(size,Math.round((start1-start)/step))));else values.splice(stop1,Math.max(0,Math.min(size,Math.round((start-start1)/step))));start=start1;stop=stop1}metric.valueAt=function(i){return values[i]};metric.shift=function(offset){return context.metric(cubism_metricShift(request,+offset))};metric.on=function(type,listener){if(!arguments.length)return event.on(type);if(listener==null){if(event.on(type)!=null&&--listening==0){context.on("prepare"+id,null).on("beforechange"+id,null)}}else{if(event.on(type)==null&&++listening==1){context.on("prepare"+id,prepare).on("beforechange"+id,beforechange)}}event.on(type,listener);if(listener!=null){if(/^change(\.|$)/.test(type))listener.call(context,start,stop)}return metric};if(arguments.length>1)metric.toString=function(){return name};return metric};var cubism_metricOverlap=6;function cubism_metricShift(request,offset){return function(start,stop,step,callback){request(new Date(+start+offset),new Date(+stop+offset),step,callback)}}function cubism_metricConstant(context,value){cubism_metric.call(this,context);value=+value;var name=value+"";this.valueOf=function(){return value};this.toString=function(){return name}}var cubism_metricConstantPrototype=cubism_metricConstant.prototype=Object.create(cubism_metric.prototype);cubism_metricConstantPrototype.valueAt=function(){return+this};cubism_metricConstantPrototype.extent=function(){return[+this,+this]};function cubism_metricOperator(name,operate){function cubism_metricOperator(left,right){if(!(right instanceof cubism_metric))right=new cubism_metricConstant(left.context,right);else if(left.context!==right.context)throw new Error("mismatch context");cubism_metric.call(this,left.context);this.left=left;this.right=right;this.toString=function(){return left+" "+name+" "+right}}var cubism_metricOperatorPrototype=cubism_metricOperator.prototype=Object.create(cubism_metric.prototype);cubism_metricOperatorPrototype.valueAt=function(i){return operate(this.left.valueAt(i),this.right.valueAt(i))};cubism_metricOperatorPrototype.shift=function(offset){return new cubism_metricOperator(this.left.shift(offset),this.right.shift(offset))};cubism_metricOperatorPrototype.on=function(type,listener){if(arguments.length<2)return this.left.on(type);this.left.on(type,listener);this.right.on(type,listener);return this};return function(right){return new cubism_metricOperator(this,right)}}cubism_metricPrototype.add=cubism_metricOperator("+",function(left,right){return left+right});cubism_metricPrototype.subtract=cubism_metricOperator("-",function(left,right){return left-right});cubism_metricPrototype.multiply=cubism_metricOperator("*",function(left,right){return left*right});cubism_metricPrototype.divide=cubism_metricOperator("/",function(left,right){return left/right});cubism_contextPrototype.horizon=function(){var context=this,mode="offset",buffer=document.createElement("canvas"),width=buffer.width=context.size(),height=buffer.height=30,scale=d3.scale.linear().interpolate(d3.interpolateRound),metric=cubism_identity,extent=null,title=cubism_identity,format=d3.format(".2s"),colors=["#08519c","#3182bd","#6baed6","#bdd7e7","#bae4b3","#74c476","#31a354","#006d2c"];function horizon(selection){selection.on("mousemove.horizon",function(){context.focus(Math.round(d3.mouse(this)[0]))}).on("mouseout.horizon",function(){context.focus(null)});selection.append("canvas").attr("width",width).attr("height",height);selection.append("span").attr("class","title").text(title);selection.append("span").attr("class","value");selection.each(function(d,i){var that=this,id=++cubism_id,metric_=typeof metric==="function"?metric.call(that,d,i):metric,colors_=typeof colors==="function"?colors.call(that,d,i):colors,extent_=typeof extent==="function"?extent.call(that,d,i):extent,start=-Infinity,step=context.step(),canvas=d3.select(that).select("canvas"),span=d3.select(that).select(".value"),max_,m=colors_.length>>1,ready;canvas.datum({id:id,metric:metric_});canvas=canvas.node().getContext("2d");function change(start1,stop){canvas.save();var extent=metric_.extent();ready=extent.every(isFinite);if(extent_!=null)extent=extent_;var i0=0,max=Math.max(-extent[0],extent[1]);var iEnd=width;if(this===context){if(max==max_){if(start1=0)continue;canvas.fillRect(i,scale(-y1),1,y0-scale(-y1))}}}canvas.restore()}function focus(i){if(i==null)i=width-1;var value=metric_.valueAt(i);span.datum(value).text(isNaN(value)?null:format)}context.on("change.horizon-"+id,change);context.on("focus.horizon-"+id,focus);metric_.on("change.horizon-"+id,function(start,stop){change(start,stop),focus();if(ready)metric_.on("change.horizon-"+id,cubism_identity)})})}horizon.remove=function(selection){selection.on("mousemove.horizon",null).on("mouseout.horizon",null);selection.selectAll("canvas").each(remove).remove();selection.selectAll(".title,.value").remove();function remove(d){d.metric.on("change.horizon-"+d.id,null);context.on("change.horizon-"+d.id,null);context.on("focus.horizon-"+d.id,null)}};horizon.mode=function(_){if(!arguments.length)return mode;mode=_+"";return horizon};horizon.height=function(_){if(!arguments.length)return height;buffer.height=height=+_;return horizon};horizon.metric=function(_){if(!arguments.length)return metric;metric=_;return horizon};horizon.scale=function(_){if(!arguments.length)return scale;scale=_;return horizon};horizon.extent=function(_){if(!arguments.length)return extent;extent=_;return horizon};horizon.title=function(_){if(!arguments.length)return title;title=_;return horizon};horizon.format=function(_){if(!arguments.length)return format;format=_;return horizon};horizon.colors=function(_){if(!arguments.length)return colors;colors=_;return horizon};return horizon};cubism_contextPrototype.comparison=function(){var context=this,width=context.size(),height=120,scale=d3.scale.linear().interpolate(d3.interpolateRound),primary=function(d){return d[0]},secondary=function(d){return d[1]},extent=null,title=cubism_identity,formatPrimary=cubism_comparisonPrimaryFormat,formatChange=cubism_comparisonChangeFormat,colors=["#9ecae1","#225b84","#a1d99b","#22723a"],strokeWidth=1.5;function comparison(selection){selection.on("mousemove.comparison",function(){context.focus(Math.round(d3.mouse(this)[0]))}).on("mouseout.comparison",function(){context.focus(null)});selection.append("canvas").attr("width",width).attr("height",height);selection.append("span").attr("class","title").text(title);selection.append("span").attr("class","value primary");selection.append("span").attr("class","value change");selection.each(function(d,i){var that=this,id=++cubism_id,primary_=typeof primary==="function"?primary.call(that,d,i):primary,secondary_=typeof secondary==="function"?secondary.call(that,d,i):secondary,extent_=typeof extent==="function"?extent.call(that,d,i):extent,div=d3.select(that),canvas=div.select("canvas"),spanPrimary=div.select(".value.primary"),spanChange=div.select(".value.change"),ready;canvas.datum({id:id,primary:primary_,secondary:secondary_});canvas=canvas.node().getContext("2d");function change(start,stop){canvas.save();canvas.clearRect(0,0,width,height);var primaryExtent=primary_.extent(),secondaryExtent=secondary_.extent(),extent=extent_==null?primaryExtent:extent_;scale.domain(extent).range([height,0]);ready=primaryExtent.concat(secondaryExtent).every(isFinite);var round=start/context.step()&1?cubism_comparisonRoundOdd:cubism_comparisonRoundEven;canvas.fillStyle=colors[2];for(var i=0,n=width;iy1)canvas.fillRect(round(i),y1,1,y0-y1)}canvas.fillStyle=colors[3];for(i=0;iy1)canvas.fillRect(round(i),y0-strokeWidth,1,strokeWidth)}canvas.restore()}function focus(i){if(i==null)i=width-1;var valuePrimary=primary_.valueAt(i),valueSecondary=secondary_.valueAt(i),valueChange=(valuePrimary-valueSecondary)/valueSecondary;spanPrimary.datum(valuePrimary).text(isNaN(valuePrimary)?null:formatPrimary);spanChange.datum(valueChange).text(isNaN(valueChange)?null:formatChange).attr("class","value change "+(valueChange>0?"positive":valueChange<0?"negative":""))}primary_.on("change.comparison-"+id,firstChange);secondary_.on("change.comparison-"+id,firstChange);function firstChange(start,stop){change(start,stop),focus();if(ready){primary_.on("change.comparison-"+id,cubism_identity);secondary_.on("change.comparison-"+id,cubism_identity)}}context.on("change.comparison-"+id,change);context.on("focus.comparison-"+id,focus)})}comparison.remove=function(selection){selection.on("mousemove.comparison",null).on("mouseout.comparison",null);selection.selectAll("canvas").each(remove).remove();selection.selectAll(".title,.value").remove();function remove(d){d.primary.on("change.comparison-"+d.id,null);d.secondary.on("change.comparison-"+d.id,null);context.on("change.comparison-"+d.id,null);context.on("focus.comparison-"+d.id,null)}};comparison.height=function(_){if(!arguments.length)return height;height=+_;return comparison};comparison.primary=function(_){if(!arguments.length)return primary;primary=_;return comparison};comparison.secondary=function(_){if(!arguments.length)return secondary;secondary=_;return comparison};comparison.scale=function(_){if(!arguments.length)return scale;scale=_;return comparison};comparison.extent=function(_){if(!arguments.length)return extent;extent=_;return comparison};comparison.title=function(_){if(!arguments.length)return title;title=_;return comparison};comparison.formatPrimary=function(_){if(!arguments.length)return formatPrimary;formatPrimary=_;return comparison};comparison.formatChange=function(_){if(!arguments.length)return formatChange;formatChange=_;return comparison};comparison.colors=function(_){if(!arguments.length)return colors;colors=_;return comparison};comparison.strokeWidth=function(_){if(!arguments.length)return strokeWidth;strokeWidth=_;return comparison};return comparison};var cubism_comparisonPrimaryFormat=d3.format(".2s"),cubism_comparisonChangeFormat=d3.format("+.0%");function cubism_comparisonRoundEven(i){return i&16777214}function cubism_comparisonRoundOdd(i){return(i+1&16777214)-1}cubism_contextPrototype.axis=function(){var context=this,scale=context.scale,axis_=d3.svg.axis().scale(scale);var format=context.step()<6e4?cubism_axisFormatSeconds:context.step()<864e5?cubism_axisFormatMinutes:cubism_axisFormatDays;function axis(selection){var id=++cubism_id,tick;var g=selection.append("svg").datum({id:id}).attr("width",context.size()).attr("height",Math.max(28,-axis.tickSize())).append("g").attr("transform","translate(0,"+(axis_.orient()==="top"?27:4)+")").call(axis_);context.on("change.axis-"+id,function(){g.call(axis_);if(!tick)tick=d3.select(g.node().appendChild(g.selectAll("text").node().cloneNode(true))).style("display","none").text(null)});context.on("focus.axis-"+id,function(i){if(tick){if(i==null){tick.style("display","none");g.selectAll("text").style("fill-opacity",null)}else{tick.style("display",null).attr("x",i).text(format(scale.invert(i)));var dx=tick.node().getComputedTextLength()+6;g.selectAll("text").style("fill-opacity",function(d){return Math.abs(scale(d)-i)= 0) continue; canvas.fillRect(i, scale(-y1), 1, y0 - scale(-y1)); diff --git a/src/metric.js b/src/metric.js index ff3e75f..4217136 100644 --- a/src/metric.js +++ b/src/metric.js @@ -56,25 +56,49 @@ cubism_contextPrototype.metric = function(request, name) { fetching; // Prefetch new data into a temporary array. - function prepare(start1, stop) { - var steps = Math.min(size, Math.round((start1 - start) / step)); - if (!steps || fetching) return; // already fetched, or fetching! - fetching = true; - steps = Math.min(size, steps + cubism_metricOverlap); - var start0 = new Date(stop - steps * step); - request(start0, stop, step, function(error, data) { - fetching = false; - if (error) return console.warn(error); - var i = isFinite(start) ? Math.round((start0 - start) / step) : 0; - for (var j = 0, m = data.length; j < m; ++j) values[j + i] = data[j]; - event.change.call(metric, start, stop); - }); + function prepare(start1, stop, callback) { + if(start1 > start) { + var steps = Math.min(size, Math.round((start1 - start) / step)); + if (!steps || fetching) return; // already fetched, or fetching! + fetching = true; + steps = Math.min(size, steps + cubism_metricOverlap); + var start0 = new Date(stop - steps * step); + request(start0, stop, step, function(error, data) { + fetching = false; + if (error) return console.warn(error); + var i = isFinite(start) ? Math.round((start0 - start) / step) : 0; + for (var j = 0, m = data.length; j < m; ++j) values[j + i] = data[j]; + event.change.call(metric, start, stop); + if(callback != null) callback(); + }); + } else { + var steps = Math.min(size, Math.round((start - start1) / step)); + if (!steps || fetching) return; + fetching = true; + // Must be seeking backward... don't need to overlap since we are + // not in realtime + //steps = Math.min(size, steps + cubism_metricOverlap); + var stop0 = new Date(+start1 + steps * step); + request(start1, stop0, step, function(error, data) { + fetching = false; + if(error) return console.warn(error); + prevalues = [] + for (var j = 0, m = data.length; j < m; ++j) prevalues[j] = data[j]; + values = prevalues.concat(values); + event.change.call(metric, start, stop); + if(callback != null) callback(); + }); + } } // When the context changes, switch to the new data, ready-or-not! function beforechange(start1, stop1) { if (!isFinite(start)) start = start1; - values.splice(0, Math.max(0, Math.min(size, Math.round((start1 - start) / step)))); + + if(start1 > start) + values.splice(0, Math.max(0, Math.min(size, Math.round((start1 - start) / step)))); + else + values.splice(stop1, Math.max(0, Math.min(size, Math.round((start - start1) / step)))); start = start1; stop = stop1; }