(function(exports){ var cubism = exports.cubism = {version: "1.0.1"}; var cubism_id = 0; function cubism_identity(d) { return d; } cubism.option = function(name, defaultValue) { var values = cubism.options(name); return values.length ? values[0] : defaultValue; }; cubism.options = function(name, defaultValues) { var options = location.search.substring(1).split("&"), values = [], i = -1, n = options.length, o; while (++i < n) { if ((o = options[i].split("="))[0] == name) { values.push(decodeURIComponent(o[1])); } } return values.length || arguments.length < 2 ? values : defaultValues; }; cubism.context = function() { var context = new cubism_context, step = 1e4, // ten seconds, in milliseconds size = 1440, // four hours at ten seconds, in pixels start0, stop0, // the start and stop for the previous change event start1, stop1, // the start and stop for the next prepare event serverDelay = 5e3, clientDelay = 5e3, 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]); 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]); 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; }; context.stop = function() { timeout = clearTimeout(timeout); return context; }; timeout = setTimeout(context.start, 10); // Set or get the step interval in milliseconds. // Defaults to ten seconds. context.step = function(_) { if (!arguments.length) return step; step = +_; return update(); }; // Set or get the context size (the count of metric values). // Defaults to 1440 (four hours at ten seconds). context.size = function(_) { if (!arguments.length) return size; scale.range([0, size = +_]); 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. context.serverDelay = function(_) { if (!arguments.length) return serverDelay; serverDelay = +_; return update(); }; // The client delay is the amount of additional time we wait to fetch those // metrics from the server. The client and server delay combined represent the // age of the most recent displayed metric. Defaults to 1 second. context.clientDelay = function(_) { if (!arguments.length) return clientDelay; clientDelay = +_; return update(); }; // Sets the focus to the specified index, and dispatches a "focus" event. context.focus = function(i) { event.focus.call(context, focus = i); return context; }; // Add, remove or get listeners for events. context.on = function(type, listener) { if (arguments.length < 2) return event.on(type); event.on(type, listener); // Notify the listener of the current start and stop time, as appropriate. // This way, metrics can make requests for data immediately, // and likewise the axis can display itself synchronously. if (listener != null) { if (/^prepare(\.|$)/.test(type)) listener.call(context, start1, stop1); if (/^beforechange(\.|$)/.test(type)) listener.call(context, start0, stop0); if (/^change(\.|$)/.test(type)) listener.call(context, start0, stop0); if (/^focus(\.|$)/.test(type)) listener.call(context, focus); } return context; }; d3.select(window).on("keydown.context-" + ++cubism_id, function() { switch (!d3.event.metaKey && d3.event.keyCode) { case 37: // left if (focus == null) focus = size - 1; if (focus > 0) context.focus(--focus); break; case 39: // right if (focus == null) focus = size - 2; if (focus < size - 1) context.focus(++focus); break; default: return; } d3.event.preventDefault(); }); return update(); }; function cubism_context() {} var cubism_contextPrototype = cubism_context.prototype; cubism_contextPrototype.constant = function(value) { return new cubism_metricConstant(this, +value); }; cubism_contextPrototype.cube = function(host) { if (!arguments.length) host = ""; var source = {}, context = this; source.metric = function(expression) { return context.metric(function(start, stop, step, callback) { d3.json(host + "/1.0/metric" + "?expression=" + encodeURIComponent(expression) + "&start=" + cubism_cubeFormatDate(start) + "&stop=" + cubism_cubeFormatDate(stop) + "&step=" + step, function(data) { if (!data) return callback(new Error("unable to load data")); callback(null, data.map(function(d) { return d.value; })); }); }, expression += ""); }; // Returns the Cube host. source.toString = function() { return host; }; return source; }; var cubism_cubeFormatDate = d3.time.format.iso; cubism_contextPrototype.graphite = function(host) { if (!arguments.length) host = ""; var source = {}, context = this; source.metric = function(expression) { return context.metric(function(start, stop, step, callback) { d3.text(host + "/render?format=raw" + "&target=" + encodeURIComponent("alias(" + expression + ",'')") + "&from=" + cubism_graphiteFormatDate(start - 2 * step) // off-by-two? + "&until=" + cubism_graphiteFormatDate(stop - 1000), function(text) { if (!text) return callback(new Error("unable to load data")); callback(null, cubism_graphiteParse(text)); }); }, expression += ""); }; 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; })); }); }; // Returns the graphite host. source.toString = function() { return host; }; return source; }; // Graphite understands seconds since UNIX epoch. function cubism_graphiteFormatDate(time) { return Math.floor(time / 1000); } // Helper method for parsing graphite's raw format. 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) * 1000, step = meta.substring(c + 1) * 1000; return text .substring(i + 1) .split(",") .slice(1) // the first value is always None? .map(function(d) { return +d; }); } function cubism_metric(context) { if (!(context instanceof cubism_context)) throw new Error("invalid context"); this.context = context; } var cubism_metricPrototype = cubism_metric.prototype; 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 (++i < n) { value = this.valueAt(i); if (value < min) min = value; if (value > max) 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; // 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); }); } // 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)))); 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 there are no listeners, then stop listening to the context, // and avoid unnecessary fetches. 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); // Notify the listener of the current start and stop time, as appropriate. // This way, charts can display synchronous metrics immediately. 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; }; // Number of metric to refetch each period, in case of lag. var cubism_metricOverlap = 6; // Wraps the specified request implementation, and shifts time by the given offset. 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(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(); // compute the new extent and ready flag var extent = metric_.extent(); ready = extent.every(isFinite); if (extent_ != null) extent = extent_; // if this is an update (with no extent change), copy old values! var i0 = 0, max = Math.max(-extent[0], extent[1]); 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); } } start = start1; } // update the domain scale.domain([0, max_ = max]); // clear for the new data canvas.clearRect(i0, 0, width - i0, height); // record whether there are negative values to display var negative; // positive bands for (var j = 0; j < m; ++j) { canvas.fillStyle = colors_[m + j]; // Adjust the range based on the current band index. var y0 = (j - m + 1) * height; scale.range([m * height + y0, y0]); y0 = scale(0); for (var i = i0, n = width, y1; i < n; ++i) { y1 = metric_.valueAt(i); if (y1 <= 0) { negative = true; continue; } canvas.fillRect(i, y1 = scale(y1), 1, y0 - y1); } } if (negative) { // enable offset mode if (mode === "offset") { canvas.translate(0, height); canvas.scale(1, -1); } // negative bands for (var j = 0; j < m; ++j) { canvas.fillStyle = colors_[m - 1 - j]; // Adjust the range based on the current band index. var y0 = (j - m + 1) * height; scale.range([m * height + y0, y0]); y0 = scale(0); for (var i = i0, n = width, y1; i < n; ++i) { y1 = metric_.valueAt(i); if (y1 >= 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); } // Update the chart when the context changes. context.on("change.horizon-" + id, change); context.on("focus.horizon-" + id, focus); // Display the first metric change immediately, // but defer subsequent updates to the canvas change. // Note that someone still needs to listen to the metric, // so that it continues to update automatically. 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(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); // update the scale var primaryExtent = primary_.extent(), secondaryExtent = secondary_.extent(), extent = extent_ == null ? primaryExtent : extent_; scale.domain(extent).range([height, 0]); ready = primaryExtent.concat(secondaryExtent).every(isFinite); // consistent overplotting var round = start / context.step() & 1 ? cubism_comparisonRoundOdd : cubism_comparisonRoundEven; // positive changes canvas.fillStyle = colors[2]; for (var i = 0, n = width; i < n; ++i) { var y0 = scale(primary_.valueAt(i)), y1 = scale(secondary_.valueAt(i)); if (y0 < y1) canvas.fillRect(round(i), y0, 1, y1 - y0); } // negative changes canvas.fillStyle = colors[0]; for (i = 0; i < n; ++i) { var y0 = scale(primary_.valueAt(i)), y1 = scale(secondary_.valueAt(i)); if (y0 > y1) canvas.fillRect(round(i), y1, 1, y0 - y1); } // positive values canvas.fillStyle = colors[3]; for (i = 0; i < n; ++i) { var y0 = scale(primary_.valueAt(i)), y1 = scale(secondary_.valueAt(i)); if (y0 <= y1) canvas.fillRect(round(i), y0, 1, strokeWidth); } // negative values canvas.fillStyle = colors[1]; for (i = 0; i < n; ++i) { var y0 = scale(primary_.valueAt(i)), y1 = scale(secondary_.valueAt(i)); if (y0 > y1) 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" : "")); } // Display the first primary change immediately, // but defer subsequent updates to the context change. // Note that someone still needs to listen to the metric, // so that it continues to update automatically. 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); } } // Update the chart when the context changes. 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 & 0xfffffe; } function cubism_comparisonRoundOdd(i) { return ((i + 1) & 0xfffffe) - 1; } cubism_contextPrototype.axis = function() { var context = this, scale = context.scale, axis_ = d3.svg.axis().scale(scale), format = context.step() < 6e4 ? cubism_axisFormatSeconds : cubism_axisFormatMinutes; 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 = cloneTick(); }); 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) < dx ? 0 : 1; }); } } }); function cloneTick() { return g.select(function() { return this.appendChild(g.select("text").node().cloneNode(true)); }) .style("display", "none") .text(null); } } axis.remove = function(selection) { selection.selectAll("svg") .each(remove) .remove(); function remove(d) { context.on("change.axis-" + d.id, null); context.on("focus.axis-" + d.id, null); } }; return d3.rebind(axis, axis_, "orient", "ticks", "tickSubdivide", "tickSize", "tickPadding", "tickFormat"); }; var cubism_axisFormatSeconds = d3.time.format("%I:%M:%S %p"), cubism_axisFormatMinutes = d3.time.format("%I:%M %p"); cubism_contextPrototype.rule = function() { var context = this; function rule(selection) { var id = ++cubism_id; var line = selection.append("div") .datum({id: id}) .attr("class", "line") .style("position", "fixed") .style("top", 0) .style("right", 0) .style("bottom", 0) .style("width", "1px") .style("pointer-events", "none"); context.on("focus.rule-" + id, function(i) { line .style("display", i == null ? "none" : null) .style("left", function() { return this.parentNode.getBoundingClientRect().left + i + "px"; }); }); } rule.remove = function(selection) { selection.selectAll(".line") .each(remove) .remove(); function remove(d) { context.on("focus.rule-" + d.id, null); } }; return rule; }; })(this);