diff --git a/.gitignore b/.gitignore index 55cee51..b800968 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,60 @@ .DS_Store -node_modules demo + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm + +/*.iml + +## Directory-based project format: +.idea/ + +## File-based project format: +*.ipr +*.iws + +## Plugin-specific files: + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml + + +### Node ### +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# Commenting this out is preferred by some people, see +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- +node_modules + +# Users Environment Variables +.lock-wscript + diff --git a/Makefile b/Makefile index 97bb1ee..ddb76c3 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ JS_COMPILER = ./node_modules/uglify-js/bin/uglifyjs .PHONY: test -all: cubism.v1.min.js package.json +all: cubism.v1.js cubism.v1.js: \ src/cubism.js \ @@ -14,6 +14,7 @@ cubism.v1.js: \ src/cube.js \ src/librato.js \ src/graphite.js \ + src/influxdb.js \ src/gangliaWeb.js \ src/metric.js \ src/metric-constant.js \ @@ -24,10 +25,6 @@ cubism.v1.js: \ src/rule.js \ Makefile -%.min.js: %.js Makefile - @rm -f $@ - $(JS_COMPILER) < $< > $@ - %.js: @rm -f $@ @echo '(function(exports){' > $@ @@ -35,13 +32,8 @@ cubism.v1.js: \ @echo '})(this);' >> $@ @chmod a-w $@ -package.json: cubism.v1.js src/package.js - @rm -f $@ - node src/package.js > $@ - @chmod a-w $@ - clean: - rm -f cubism.v1.js cubism.v1.min.js package.json + rm -f cubism.v1.js test: all @$(JS_TESTER) diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..82a3230 --- /dev/null +++ b/bower.json @@ -0,0 +1,24 @@ +{ + "name": "cubism", + "main": "cubism.v1.js", + "version": "1.6.1", + "homepage": "https://github.com/benFlightStats/cubism", + "authors": [ + "Benjamin Corliss " + ], + "description": "Dynamic timeline charts", + "keywords": [ + "d3", + "timeline", + "graphing" + ], + "license": "MIT", + "private": true, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ] +} diff --git a/cubism.iml b/cubism.iml new file mode 100644 index 0000000..284429d --- /dev/null +++ b/cubism.iml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/cubism.v1.js b/cubism.v1.js index 01161aa..05fd9a1 100644 --- a/cubism.v1.js +++ b/cubism.v1.js @@ -24,6 +24,7 @@ cubism.context = function() { var context = new cubism_context, step = 1e4, // ten seconds, in milliseconds size = 1440, // four hours at ten seconds, in pixels + componentWidth = 1440, start0, stop0, // the start and stop for the previous change event start1, stop1, // the start and stop for the next prepare event serverDelay = 5e3, @@ -31,10 +32,14 @@ cubism.context = function() { event = d3.dispatch("prepare", "beforechange", "change", "focus"), scale = context.scale = d3.time.scale().range([0, size]), timeout, + xScale, focus; function update() { - var now = Date.now(); + xScale = componentWidth / size >= 1 ? componentWidth / size : 1; + scale.range([0, size * xScale]); + + 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); @@ -43,6 +48,10 @@ cubism.context = function() { return context; } + context.xScale = function(){ + return xScale; + }; + context.start = function() { if (timeout) clearTimeout(timeout); var delay = +stop1 + serverDelay - Date.now(); @@ -86,10 +95,15 @@ cubism.context = function() { // Defaults to 1440 (four hours at ten seconds). context.size = function(_) { if (!arguments.length) return size; - scale.range([0, size = +_]); + size = +_; return update(); }; + context.componentWidth = function(_) { + if (!arguments.length) return componentWidth; + componentWidth = +_; + return update(); + }; // The server delay is the amount of time we wait for the server to compute a // metric. This delay may result from clock skew or from delays collecting // metrics from various hosts. Defaults to 4 seconds. @@ -439,6 +453,67 @@ function cubism_graphiteParse(text) { .slice(1) // the first value is always None? .map(function(d) { return +d; }); } +cubism_contextPrototype.influxdb = function(host, database) { + if (!arguments.length) host = ""; + // e.g. http://influxdb:8086/, cubism + var source = {}, + context = this; + + // expression: + // .select - data to select + // .from - name of influx series + // .where - array of where clauses + // example: metric({select: "sum(requests)", from: "myapp", where: ["host='host-1'"]}) + source.metric = function(expression) { + return context.metric(function(start, stop, step, callback) { + var influxQuery = buildInfluxQuery(expression.select, expression.from, expression.where, start, stop, step); + var urlquery = `query?q=${encodeURIComponent(influxQuery)}&db=${database}`; + var url = host + urlquery; + + d3.xhr(url, 'application/json', function(error, data) { + if (!data) return callback(new Error("unable to load data")); + callback(null, influxMetrics(data, expression.column)); + }); + }, expression); + }; + + // Returns the InfluxDb host. + source.toString = function() { + return host; + }; + + return source; +}; + +function buildInfluxQuery(select, from, where, start, stop, step) { + where = (where === undefined) ? [] : JSON.parse(JSON.stringify(where)); + where.push(`time < ${influxDateFormat(stop)} AND time > ${influxDateFormat(start)}`) + + return `SELECT ${select} ` + + `FROM ${from} ` + + `WHERE ${where.join(" AND ")} ` + + `GROUP BY time(${step*1000}u) ` + + `FILL(0)`; +}; + +function influxDateFormat(d) { + return (d.getTime() / 1000) + 's'; +}; + +function influxMetrics(dataJson, column) { + var json = JSON.parse(dataJson.response); + if (json.results[0].series === undefined) { + return [] + }; + + var metricIndex = json.results[0].series[0].columns.indexOf(column); + if (metricIndex < 0) { + metricIndex = json.results[0].series[0].columns.length - 1; + } + return json.results[0].series[0].values.map(function(row) { + return row[metricIndex]; + }); +}; cubism_contextPrototype.gangliaWeb = function(config) { var host = '', uriPathPrefix = '/ganglia2/'; @@ -618,6 +693,10 @@ cubism_contextPrototype.metric = function(request, name) { return values[i]; }; + metric.values = function() { + return values; + }; + // metric.shift = function(offset) { return context.metric(cubism_metricShift(request, +offset)); @@ -734,9 +813,10 @@ cubism_metricPrototype.divide = cubism_metricOperator("/", function(left, right) }); cubism_contextPrototype.horizon = function() { var context = this, + xScale = context.xScale(), mode = "offset", buffer = document.createElement("canvas"), - width = buffer.width = context.size(), + dataSize = context.size(), height = buffer.height = 30, scale = d3.scale.linear().interpolate(d3.interpolateRound), metric = cubism_identity, @@ -744,15 +824,19 @@ cubism_contextPrototype.horizon = function() { title = cubism_identity, format = d3.format(".2s"), colors = ["#08519c","#3182bd","#6baed6","#bdd7e7","#bae4b3","#74c476","#31a354","#006d2c"]; + var rLeft, rWidth; + buffer.width = dataSize * xScale; function horizon(selection) { selection + //here focus is being set to mouse x, but other places it corresponds to the data array index. .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("width", dataSize * xScale) .attr("height", height); selection.append("span") @@ -791,13 +875,18 @@ cubism_contextPrototype.horizon = function() { var i0 = 0, max = Math.max(-extent[0], extent[1]); if (this === context) { if (max == max_) { - i0 = width - cubism_metricOverlap; + i0 = dataSize - (cubism_metricOverlap); var dx = (start1 - start) / step; - if (dx < width) { + if (dx < dataSize) { + buffer.width = dataSize * xScale; 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); + rWidth = dataSize * xScale; + canvas0.clearRect(0, 0, rWidth, height); + rLeft = dx * xScale; + rWidth = (dataSize - dx) * xScale; + canvas0.drawImage(canvas.canvas, rLeft, 0, rWidth, height, 0, 0, rWidth, height); + rWidth = dataSize * xScale ; + canvas.clearRect(0, 0, rWidth, height); canvas.drawImage(canvas0.canvas, 0, 0); } } @@ -808,12 +897,16 @@ cubism_contextPrototype.horizon = function() { scale.domain([0, max_ = max]); // clear for the new data - canvas.clearRect(i0, 0, width - i0, height); + rWidth = (dataSize - i0) * xScale; + rLeft = i0 * xScale; + canvas.clearRect(rLeft, 0, rWidth, height); // record whether there are negative values to display var negative; // positive bands + var time = new Date(); + for (var j = 0; j < m; ++j) { canvas.fillStyle = colors_[m + j]; @@ -822,11 +915,13 @@ 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 = dataSize, y1; i < n; ++i) { y1 = metric_.valueAt(i); + rLeft = Math.round(i*xScale); + rWidth = Math.round(xScale); if (y1 <= 0) { negative = true; continue; } if (y1 === undefined) continue; - canvas.fillRect(i, y1 = scale(y1), 1, y0 - y1); + canvas.fillRect(rLeft, y1 = scale(y1), rWidth, y0 - y1); } } @@ -846,10 +941,11 @@ 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 = dataSize, y1; i < n; ++i) { y1 = metric_.valueAt(i); if (y1 >= 0) continue; - canvas.fillRect(i, scale(-y1), 1, y0 - scale(-y1)); + canvas.fillRect(Math.round(i*xScale), scale(-y1), + Math.round(xScale), y0 - scale(-y1)); } } } @@ -857,10 +953,14 @@ cubism_contextPrototype.horizon = function() { canvas.restore(); } - function focus(i) { - if (i == null) i = width - 1; - var value = metric_.valueAt(i); + function focus(mouse_x) { + if (mouse_x == null) mouse_x = dataSize - 1; + // since we are getting mouse_x and it's a scaled value, + // need to convert this back to an index to look up + var index = Math.round(mouse_x/xScale); + var value = metric_.valueAt(index); span.datum(value).text(isNaN(value) ? null : format); + span.style("left", mouse_x+"px"); } // Update the chart when the context changes. @@ -1181,7 +1281,8 @@ function cubism_comparisonRoundOdd(i) { cubism_contextPrototype.axis = function() { var context = this, scale = context.scale, - axis_ = d3.svg.axis().scale(scale); + axis_ = d3.svg.axis().scale(scale), + xScale = context.xScale(); var formatDefault = context.step() < 6e4 ? cubism_axisFormatSeconds : context.step() < 864e5 ? cubism_axisFormatMinutes @@ -1191,10 +1292,10 @@ cubism_contextPrototype.axis = function() { function axis(selection) { var id = ++cubism_id, tick; - + var axisWidth = Math.floor(context.size() * xScale); var g = selection.append("svg") .datum({id: id}) - .attr("width", context.size()) + .attr("width", axisWidth) .attr("height", Math.max(28, -axis.tickSize())) .append("g") .attr("transform", "translate(0," + (axis_.orient() === "top" ? 27 : 4) + ")") @@ -1213,7 +1314,7 @@ cubism_contextPrototype.axis = function() { tick.style("display", "none"); g.selectAll("text").style("fill-opacity", null); } else { - tick.style("display", null).attr("x", i).text(format(scale.invert(i))); + tick.style("display", null).attr("x", i).text(format(scale.invert(i))); //affects where the hover time displayes var dx = tick.node().getComputedTextLength() + 6; g.selectAll("text").style("fill-opacity", function(d) { return Math.abs(scale(d) - i) < dx ? 0 : 1; }); } @@ -1289,10 +1390,10 @@ cubism_contextPrototype.rule = function() { metric_.on("change.rule-" + id, change); }); - context.on("focus.rule-" + id, function(i) { - line.datum(i) - .style("display", i == null ? "none" : null) - .style("left", i == null ? null : cubism_ruleLeft); + context.on("focus.rule-" + id, function(mouse_x) { + line.datum(mouse_x) + .style("display", mouse_x == null ? "none" : null) + .style("left", mouse_x == null ? null : cubism_ruleLeft); }); } diff --git a/cubism.v1.min.js b/cubism.v1.min.js index e04942c..e69de29 100644 --- a/cubism.v1.min.js +++ b/cubism.v1.min.js @@ -1 +0,0 @@ -(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.6.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:(i=f(c),d>g&&i<900?900:d>h&&i<60?60:i)}var d={},e=this;auth_string="Basic "+btoa(a+":"+c),avail_rsts=[1,60,900,3600];var j=function(a){function d(b,d,e){var f="compose="+a+"&start_time="+b+"&end_time="+d+"&resolution="+g(b,d,e);return c+"?"+f}function e(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 c="https://metrics-api.librato.com/v1/metrics";return request={},request.fire=function(a,c,f,g){function i(j){d3.json(j).header("X-Requested-With","XMLHttpRequest").header("Authorization",auth_string).header("Librato-User-Agent","cubism/"+b.version).get(function(b,j){if(!b){if(j.measurements.length===0)return;j.measurements[0].series.forEach(function(a){h.push(a)});var k="query"in j&&"next_time"in j.query;if(k)i(d(j.query.next_time,c,f));else{var l=e(a,c,f,h);g(l)}}})}var h=[];i(d(a,c,f))},request};return d.metric=function(a){return e.metric(function(b,c,d,e){j(a).fire(h(b),h(c),h(d),function(a){e(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 g(e){var h=++c,i,j=e.append("svg").datum({id:h}).attr("width",a.size()).attr("height",Math.max(28,-g.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(f(b.invert(a)));var c=i.node().getComputedTextLength()+6;j.selectAll("text").style("fill-opacity",function(d){return Math.abs(b(d)-a)= 1 ? componentWidth / size : 1; + scale.range([0, size * xScale]); + + 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); @@ -21,6 +26,10 @@ cubism.context = function() { return context; } + context.xScale = function(){ + return xScale; + }; + context.start = function() { if (timeout) clearTimeout(timeout); var delay = +stop1 + serverDelay - Date.now(); @@ -64,10 +73,15 @@ cubism.context = function() { // Defaults to 1440 (four hours at ten seconds). context.size = function(_) { if (!arguments.length) return size; - scale.range([0, size = +_]); + size = +_; return update(); }; + context.componentWidth = function(_) { + if (!arguments.length) return componentWidth; + componentWidth = +_; + return update(); + }; // The server delay is the amount of time we wait for the server to compute a // metric. This delay may result from clock skew or from delays collecting // metrics from various hosts. Defaults to 4 seconds. diff --git a/src/horizon.js b/src/horizon.js index 3c779c0..95c2eaa 100644 --- a/src/horizon.js +++ b/src/horizon.js @@ -1,8 +1,9 @@ cubism_contextPrototype.horizon = function() { var context = this, + xScale = context.xScale(), mode = "offset", buffer = document.createElement("canvas"), - width = buffer.width = context.size(), + dataSize = context.size(), height = buffer.height = 30, scale = d3.scale.linear().interpolate(d3.interpolateRound), metric = cubism_identity, @@ -10,15 +11,19 @@ cubism_contextPrototype.horizon = function() { title = cubism_identity, format = d3.format(".2s"), colors = ["#08519c","#3182bd","#6baed6","#bdd7e7","#bae4b3","#74c476","#31a354","#006d2c"]; + var rLeft, rWidth; + buffer.width = dataSize * xScale; function horizon(selection) { selection + //here focus is being set to mouse x, but other places it corresponds to the data array index. .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("width", dataSize * xScale) .attr("height", height); selection.append("span") @@ -57,13 +62,18 @@ cubism_contextPrototype.horizon = function() { var i0 = 0, max = Math.max(-extent[0], extent[1]); if (this === context) { if (max == max_) { - i0 = width - cubism_metricOverlap; + i0 = dataSize - (cubism_metricOverlap); var dx = (start1 - start) / step; - if (dx < width) { + if (dx < dataSize) { + buffer.width = dataSize * xScale; 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); + rWidth = dataSize * xScale; + canvas0.clearRect(0, 0, rWidth, height); + rLeft = dx * xScale; + rWidth = (dataSize - dx) * xScale; + canvas0.drawImage(canvas.canvas, rLeft, 0, rWidth, height, 0, 0, rWidth, height); + rWidth = dataSize * xScale ; + canvas.clearRect(0, 0, rWidth, height); canvas.drawImage(canvas0.canvas, 0, 0); } } @@ -74,12 +84,16 @@ cubism_contextPrototype.horizon = function() { scale.domain([0, max_ = max]); // clear for the new data - canvas.clearRect(i0, 0, width - i0, height); + rWidth = (dataSize - i0) * xScale; + rLeft = i0 * xScale; + canvas.clearRect(rLeft, 0, rWidth, height); // record whether there are negative values to display var negative; // positive bands + var time = new Date(); + for (var j = 0; j < m; ++j) { canvas.fillStyle = colors_[m + j]; @@ -88,11 +102,13 @@ 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 = dataSize, y1; i < n; ++i) { y1 = metric_.valueAt(i); + rLeft = Math.round(i*xScale); + rWidth = Math.round(xScale); if (y1 <= 0) { negative = true; continue; } if (y1 === undefined) continue; - canvas.fillRect(i, y1 = scale(y1), 1, y0 - y1); + canvas.fillRect(rLeft, y1 = scale(y1), rWidth, y0 - y1); } } @@ -112,10 +128,11 @@ 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 = dataSize, y1; i < n; ++i) { y1 = metric_.valueAt(i); if (y1 >= 0) continue; - canvas.fillRect(i, scale(-y1), 1, y0 - scale(-y1)); + canvas.fillRect(Math.round(i*xScale), scale(-y1), + Math.round(xScale), y0 - scale(-y1)); } } } @@ -123,10 +140,14 @@ cubism_contextPrototype.horizon = function() { canvas.restore(); } - function focus(i) { - if (i == null) i = width - 1; - var value = metric_.valueAt(i); + function focus(mouse_x) { + if (mouse_x == null) mouse_x = dataSize - 1; + // since we are getting mouse_x and it's a scaled value, + // need to convert this back to an index to look up + var index = Math.round(mouse_x/xScale); + var value = metric_.valueAt(index); span.datum(value).text(isNaN(value) ? null : format); + span.style("left", mouse_x+"px"); } // Update the chart when the context changes. diff --git a/src/influxdb.js b/src/influxdb.js new file mode 100644 index 0000000..a4e4f42 --- /dev/null +++ b/src/influxdb.js @@ -0,0 +1,61 @@ +cubism_contextPrototype.influxdb = function(host, database) { + if (!arguments.length) host = ""; + // e.g. http://influxdb:8086/, cubism + var source = {}, + context = this; + + // expression: + // .select - data to select + // .from - name of influx series + // .where - array of where clauses + // example: metric({select: "sum(requests)", from: "myapp", where: ["host='host-1'"]}) + source.metric = function(expression) { + return context.metric(function(start, stop, step, callback) { + var influxQuery = buildInfluxQuery(expression.select, expression.from, expression.where, start, stop, step); + var urlquery = `query?q=${encodeURIComponent(influxQuery)}&db=${database}`; + var url = host + urlquery; + + d3.xhr(url, 'application/json', function(error, data) { + if (!data) return callback(new Error("unable to load data")); + callback(null, influxMetrics(data, expression.column)); + }); + }, expression); + }; + + // Returns the InfluxDb host. + source.toString = function() { + return host; + }; + + return source; +}; + +function buildInfluxQuery(select, from, where, start, stop, step) { + where = (where === undefined) ? [] : JSON.parse(JSON.stringify(where)); + where.push(`time < ${influxDateFormat(stop)} AND time > ${influxDateFormat(start)}`) + + return `SELECT ${select} ` + + `FROM ${from} ` + + `WHERE ${where.join(" AND ")} ` + + `GROUP BY time(${step*1000}u) ` + + `FILL(0)`; +}; + +function influxDateFormat(d) { + return (d.getTime() / 1000) + 's'; +}; + +function influxMetrics(dataJson, column) { + var json = JSON.parse(dataJson.response); + if (json.results[0].series === undefined) { + return [] + }; + + var metricIndex = json.results[0].series[0].columns.indexOf(column); + if (metricIndex < 0) { + metricIndex = json.results[0].series[0].columns.length - 1; + } + return json.results[0].series[0].values.map(function(row) { + return row[metricIndex]; + }); +}; diff --git a/src/metric.js b/src/metric.js index ff3e75f..93fac5f 100644 --- a/src/metric.js +++ b/src/metric.js @@ -84,6 +84,10 @@ cubism_contextPrototype.metric = function(request, name) { return values[i]; }; + metric.values = function() { + return values; + }; + // metric.shift = function(offset) { return context.metric(cubism_metricShift(request, +offset)); diff --git a/src/rule.js b/src/rule.js index 603a677..7abc775 100644 --- a/src/rule.js +++ b/src/rule.js @@ -36,10 +36,10 @@ cubism_contextPrototype.rule = function() { metric_.on("change.rule-" + id, change); }); - context.on("focus.rule-" + id, function(i) { - line.datum(i) - .style("display", i == null ? "none" : null) - .style("left", i == null ? null : cubism_ruleLeft); + context.on("focus.rule-" + id, function(mouse_x) { + line.datum(mouse_x) + .style("display", mouse_x == null ? "none" : null) + .style("left", mouse_x == null ? null : cubism_ruleLeft); }); }