(function(exports){ var cubism = exports.cubism = {version: "1.6.0"}; 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_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; /* librato (http://dev.librato.com/v1/post/metrics) source * If you want to see an example of how to use this source, check: https://gist.github.com/drio/5792680 */ cubism_contextPrototype.librato = function(user, token) { var source = {}, context = this; auth_string = "Basic " + btoa(user + ":" + token); avail_rsts = [ 1, 60, 900, 3600 ]; /* Given a step, find the best librato resolution to use. * * Example: * * (s) : cubism step * * avail_rsts 1 --------------- 60 --------------- 900 ---------------- 3600 * | (s) | * | | * [low_res top_res] * * return: low_res (60) */ function find_ideal_librato_resolution(step) { var highest_res = avail_rsts[0], lowest_res = avail_rsts[avail_rsts.length]; // high and lowest available resolution from librato /* If step is outside the highest or lowest librato resolution, pick them and we are done */ if (step >= lowest_res) return lowest_res; if (step <= highest_res) return highest_res; /* If not, find in what resolution interval the step lands. */ 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; } } /* What's the closest librato resolution given the step ? */ return ((top_res-step) < (step-low_res)) ? top_res : low_res; } function find_librato_resolution(sdate, edate, step) { var i_size = edate - sdate, // interval size month = 2419200, week = 604800, two_days = 172800, ideal_res; if (i_size > month) return 3600; ideal_res = find_ideal_librato_resolution(step); /* * Now we have the ideal resolution, but due to the retention policies at librato, maybe we have * to use a higher resolution. * http://support.metrics.librato.com/knowledgebase/articles/66838-understanding-metrics-roll-ups-retention-and-grap */ if (i_size > week && ideal_res < 900) return 900; else if (i_size > two_days && ideal_res < 60) return 60; else return ideal_res; } /* All the logic to query the librato API is here */ var librato_request = function(composite) { var url_prefix = "https://metrics-api.librato.com/v1/metrics"; function make_url(sdate, edate, step) { var params = "compose=" + composite + "&start_time=" + sdate + "&end_time=" + edate + "&resolution=" + find_librato_resolution(sdate, edate, step); return url_prefix + "?" + params; } /* * We are most likely not going to get the same number of measurements * cubism expects for a particular context: We have to perform down/up * sampling */ 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) { /* Compute the average */ v = int_mes.reduce(function(a, b) { return a + b }) / int_mes.length; } else { /* No librato values on interval */ 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 = []; /* Store partial values from librato */ /* * Librato has a limit in the number of measurements we get back in a request (100). * We recursively perform requests to the API to ensure we have all the data points * for the interval we are working on. */ 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) { /* Callback; data available */ if (!error) { if (data.measurements.length === 0) { return } data.measurements[0].series.forEach(function(o) { a_values.push(o); }); var still_more_values = 'query' in data && 'next_time' in data.query; if (still_more_values) { actual_request(make_url(data.query.next_time, iedate, step)); } else { var a_adjusted = down_up_sampling(isdate, iedate, step, a_values); callback_done(a_adjusted); } } }); } actual_request(make_url(isdate, iedate, step)); }; return request; }; /* * The user will use this method to create a cubism source (librato in this case) * and call .metric() as necessary to create metrics. */ source.metric = function(m_composite) { return context.metric(function(start, stop, step, callback) { /* All the librato logic is here; .fire() retrieves the metrics' data */ librato_request(m_composite) .fire(cubism_libratoFormatDate(start), cubism_libratoFormatDate(stop), cubism_libratoFormatDate(step), function(a_values) { callback(null, a_values); }); }, m_composite += ""); }; /* This is not used when the source is librato */ source.toString = function() { return "librato"; }; return source; }; var cubism_libratoFormatDate = function(time) { return Math.floor(time / 1000); }; 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; // Apply the summarize, if necessary. 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) // 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 += ""); 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; })); }); }; // 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; }); } cubism_contextPrototype.gangliaWeb = function(config) { var host = '', uriPathPrefix = '/ganglia2/'; if (arguments.length) { if (config.host) { host = config.host; } if (config.uriPathPrefix) { uriPathPrefix = config.uriPathPrefix; /* Add leading and trailing slashes, as appropriate. */ if( uriPathPrefix[0] != '/' ) { uriPathPrefix = '/' + uriPathPrefix; } if( uriPathPrefix[uriPathPrefix.length - 1] != '/' ) { uriPathPrefix += '/'; } } } var source = {}, context = this; source.metric = function(metricInfo) { /* Store the members from metricInfo into local variables. */ var clusterName = metricInfo.clusterName, metricName = metricInfo.metricName, hostName = metricInfo.hostName, isReport = metricInfo.isReport || false, titleGenerator = metricInfo.titleGenerator || /* Reasonable (not necessarily pretty) default for titleGenerator. */ function(unusedMetricInfo) { /* unusedMetricInfo is, well, unused in this default case. */ return ('clusterName:' + clusterName + ' metricName:' + metricName + (hostName ? ' hostName:' + hostName : '')); }, onChangeCallback = metricInfo.onChangeCallback; /* Default to plain, simple metrics. */ 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/1000 + '&ce=' + stop/1000 + '&step=' + step/1000 + '&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); }; /* Allow users to run their custom code each time a gangliaWebMetric changes. * * TODO Consider abstracting away the naked Cubism call, and instead exposing * a callback that takes in the values array (maybe alongwith the original * start and stop 'naked' parameters), since it's handy to have the entire * dataset at your disposal (and users will likely implement onChangeCallback * primarily to get at this dataset). */ if (onChangeCallback) { gangliaWebMetric.on('change', onChangeCallback); } return gangliaWebMetric; }; // Returns the gangliaWeb host + uriPathPrefix. 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 (++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(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(); // 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; } if (y1 === undefined) 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(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); // 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); var formatDefault = context.step() < 6e4 ? cubism_axisFormatSeconds : context.step() < 72e5 ? cubism_axisFormatMinutes : cubism_axisFormatDays; var format = formatDefault; 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) < dx ? 0 : 1; }); } } }); } 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); } }; axis.focusFormat = function(_) { if (!arguments.length) return format == formatDefault ? null : _; format = _ == null ? formatDefault : _; return axis; }; 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_axisFormatDays = d3.time.format("%B %d"); cubism_contextPrototype.rule = function() { var context = this, metric = cubism_identity; function rule(selection) { var id = ++cubism_id; var line = selection.append("div") .datum({id: id}) .attr("class", "line") .call(cubism_ruleStyle); selection.each(function(d, i) { var that = this, id = ++cubism_id, metric_ = typeof metric === "function" ? metric.call(that, d, i) : metric; if (!metric_) return; function change(start, stop) { var values = []; for (var i = 0, n = context.size(); i < n; ++i) { if (metric_.valueAt(i)) { values.push(i); } } var lines = selection.selectAll(".metric").data(values); lines.exit().remove(); lines.enter().append("div").attr("class", "metric line").call(cubism_ruleStyle); lines.style("left", cubism_ruleLeft); } context.on("change.rule-" + id, change); 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); }); } rule.remove = function(selection) { selection.selectAll(".line") .each(remove) .remove(); function remove(d) { context.on("focus.rule-" + d.id, null); } }; rule.metric = function(_) { if (!arguments.length) return metric; metric = _; return rule; }; return rule; }; function cubism_ruleStyle(line) { line .style("position", "absolute") .style("top", 0) .style("bottom", 0) .style("width", "1px") .style("pointer-events", "none"); } function cubism_ruleLeft(i) { return i + "px"; } })(this);