| (function (exports) { |
| var cubism = exports.cubism = {version:"1.2.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; |
| 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 + "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; |
| }); |
| } |
| |
| 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(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); |
| |
| 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) < 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); |
| } |
| }; |
| |
| 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", 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); |