Browse files

Cubism hacks.

  • Loading branch information...
1 parent 64a48e4 commit 2edbc5cecedcd730deca1c083c665221dc5441a2 @dustin dustin committed Jun 12, 2012
Showing with 1,294 additions and 0 deletions.
  1. +986 −0 static/cubism.js
  2. +135 −0 static/horizon.html
  3. +173 −0 static/horizon.js
View
986 static/cubism.js
@@ -0,0 +1,986 @@
+(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);
View
135 static/horizon.html
@@ -0,0 +1,135 @@
+<html>
+ <head>
+ <title>Cubin'</title>
+ <meta http-equiv="refresh" content="21600">
+ <script type="text/javascript" src="/static/jquery.min.js"></script>
+ <script type="text/javascript" src="/static/d3.v2.min.js"></script>
+ <script type="text/javascript" src="/static/cubism.js"></script>
+ <script type="text/javascript" src="/static/vbmap.js"></script>
+ <script type="text/javascript" src="/static/horizon.js"></script>
+ <style type="text/css">
+body {
+ margin: 0;
+}
+
+.axis {
+ font: 10px sans-serif;
+ pointer-events: none;
+ z-index: 2;
+}
+
+.axis text {
+ -webkit-transition: fill-opacity 250ms linear;
+}
+
+.axis path {
+ display: none;
+}
+
+.axis line {
+ stroke: #000;
+ shape-rendering: crispEdges;
+}
+
+.axis.top {
+ background-image: linear-gradient(top, #fff 0%, rgba(255,255,255,0) 100%);
+ background-image: -o-linear-gradient(top, #fff 0%, rgba(255,255,255,0) 100%);
+ background-image: -moz-linear-gradient(top, #fff 0%, rgba(255,255,255,0) 100%);
+ background-image: -webkit-linear-gradient(top, #fff 0%, rgba(255,255,255,0) 100%);
+ background-image: -ms-linear-gradient(top, #fff 0%, rgba(255,255,255,0) 100%);
+ top: 0px;
+ /* padding: 0 0 24px 0; */
+ padding: 0;
+}
+
+.axis.bottom {
+ background-image: linear-gradient(bottom, #fff 0%, rgba(255,255,255,0) 100%);
+ background-image: -o-linear-gradient(bottom, #fff 0%, rgba(255,255,255,0) 100%);
+ background-image: -moz-linear-gradient(bottom, #fff 0%, rgba(255,255,255,0) 100%);
+ background-image: -webkit-linear-gradient(bottom, #fff 0%, rgba(255,255,255,0) 100%);
+ background-image: -ms-linear-gradient(bottom, #fff 0%, rgba(255,255,255,0) 100%);
+ bottom: 0px;
+ padding: 24px 0 0 0;
+ position: fixed;
+}
+
+.horizon {
+ border-bottom: solid 1px #000;
+ overflow: hidden;
+ position: relative;
+}
+
+.horizon {
+ border-top: solid 1px #000;
+ border-bottom: solid 1px #000;
+}
+
+.horizon + .horizon {
+ border-top: none;
+}
+
+.horizon canvas {
+ display: block;
+}
+
+.horizon .title,
+.horizon .value {
+ bottom: 0;
+ line-height: 30px;
+ margin: 0 6px;
+ position: absolute;
+ text-shadow: 0 1px 0 rgba(255,255,255,.5);
+ white-space: nowrap;
+}
+
+.horizon .title {
+ left: 0;
+}
+
+.horizon .value {
+ right: 0;
+}
+
+.line {
+ background: #000;
+ opacity: .2;
+ z-index: 2;
+}
+ </style>
+ </head>
+ <body>
+ <div id="net"></div>
+ <div id="ram"></div>
+ <div id="ops"></div>
+ <div id="items"></div>
+
+<script type="text/javascript">
+var updaters = [];
+var statData = [];
+var timestamps = [];
+
+ $(document).ready(function() {
+ var clusterInfo = getClusterParams();
+
+ function update() {
+ doGenericStatRequest(clusterInfo, "", function(data) {
+ timestamps.push(new Date().getTime());
+ statData.push(data);
+
+ for (var i = 0; i < updaters.length; i++) {
+ updaters[i](data);
+ }
+ });
+ }
+
+ update();
+ setInterval(update, 1000);
+
+ drawHorizon("#net", clusterInfo, 'net', 'top');
+ drawHorizon("#ram", clusterInfo, 'mem');
+ drawHorizon("#ops", clusterInfo, 'ops');
+ drawHorizon("#items", clusterInfo, 'items');
+ });
+ </script>
+ </body>
+</html>
View
173 static/horizon.js
@@ -0,0 +1,173 @@
+statRequestBase = "http://cbvis.west.spy.net/stats";
+
+function drawHorizon(here, clusterInfo, which, axified) {
+
+ var context = cubism.context()
+ .step(1e4)
+ .size(1440);
+
+ if (axified) {
+ d3.select(here).selectAll(".axis")
+ .data([axified])
+ .enter().append("div")
+ .attr("class", function(d) { return d + " axis"; })
+ .each(function(d) { d3.select(this).call(context.axis().ticks(12).orient(d)); });
+ }
+
+ d3.select(here).append("div")
+ .attr("class", "rule")
+ .call(context.rule());
+
+ context.on("focus", function(i) {
+ d3.selectAll(".value").style("right", i == null ? null : context.size() - i + "px");
+ });
+
+ updaters.push(function(data) {
+
+ var nodes = [];
+ for (var k in data) {
+ nodes.push(k + " " + which);
+ }
+
+ d3.select(here).selectAll(".horizon")
+ .data(nodes.map(justDoBoth))
+ .enter().insert("div", ".bottom")
+ .attr("class", "horizon")
+ .call(context.horizon().height(30).extent(null));
+
+ // d3.select(here).selectAll(".horizon")
+ // .data(d3.keys(data).map(nodeData))
+ // .exit().remove();
+ });
+
+ function justDoBoth(nodeNameThing) {
+ var handlers = {
+ net: networkBytes,
+ mem: function(a, b) { return getStat(a, b, 'mem_used'); },
+ items: function(a, b) { return getStat(a, b, 'curr_items'); },
+ ops: function(a, b) {
+ return sumStats(a, b,
+ [
+ "cas_hits",
+ "cas_misses",
+ "decr_hits",
+ "decr_misses",
+ "delete_hits",
+ "delete_misses",
+ "get_hits",
+ "get_misses",
+ "incr_hits",
+ "incr_misses",
+ "cmd_set"
+ ]);
+ }
+ };
+ var parts = nodeNameThing.split(" ");
+ return handlers[parts[1]](parts[0], nodeNameThing);
+ }
+
+ function getStat(nodeName, label, stat) {
+ return nodeData(nodeName, function(h) {
+ return parseFloat(h[stat]);
+ }, label);
+ }
+
+ function sum(a) {
+ var rv = 0;
+ for(var i = 0; i < a.length; i++) {
+ rv += parseFloat(a[i]);
+ }
+ return rv;
+ }
+
+ function sumStats(nodeName, label, stats) {
+ var prev = 0;
+ return nodeData(nodeName, function(h) {
+ var vals = [];
+ for (var i = 0; i < stats.length; i++) {
+ vals.push(h[stats[i]]);
+ }
+ var v = sum(vals);
+ var rv = v - prev;
+ prev = v;
+ return rv;
+ }, label);
+ }
+
+ function ops(nodeName, label) {
+ var prev;
+ return nodeData(nodeName, function(h) {
+ var val = 0;
+ for (var k in h) {
+ if (k.match(/(cmd_|hits|misses)/)) {
+ val += parseFloat(h[k]);
+ }
+ }
+ var rv = Math.max(0, isNaN(prev) ? NaN : val - prev);
+ prev = val;
+ return rv;
+ }, label);
+ }
+
+ function networkBytes(nodeName, label) {
+ var prev;
+ return nodeData(nodeName, function(h) {
+ var val = 0;
+ for (var k in h) {
+ if (k.substring(0, 6) === "bytes_") {
+ val += parseFloat(h[k]);
+ }
+ }
+ var rv = Math.max(0, isNaN(prev) ? NaN : val - prev);
+ prev = val;
+ return rv;
+ }, label);
+ }
+
+ function nodeData(nodeName, extractf, label) {
+ label = label || nodeName;
+ return context.metric(function(start, stop, step, callback) {
+ console.log("Fetching data for", nodeName, "from", start, "to", stop);
+ var sti = 0;
+ while (sti < timestamps.length && timestamps[sti] < +start) {
+ sti++;
+ }
+ var stuff = [];
+ while (sti < timestamps.length && timestamps[sti] <= +stop) {
+ var val = 0;
+ if (statData[sti][nodeName]) {
+ stuff.push({key: timestamps[sti],
+ value: extractf(statData[sti][nodeName])});
+ }
+ sti++;
+ }
+ callback(null, binRows(stuff, start, step));
+ }, label);
+ }
+
+ // Stolen from jchris
+ function binRows(rows, start, step) {
+ var times, gap, i, j, val = 0, vals = [];
+ for (i=0; i < rows.length; i++) {
+ var rowTime = rows[i].key;
+ if (rowTime < start + step) {
+ val += rows[i].value;
+ } else if (rowTime > start + step) {
+ vals.push(val);
+ gap = rowTime - start + step;
+ times = gap / step;
+ for (j=0; j < times; j++) {
+ vals.push(NaN); // nothing for that bin
+ };
+ val = rows[i].value;
+ start += (step * (times+1));
+ } else {
+ vals.push(val);
+ val = rows[i].value;
+ start += step;
+ }
+ };
+ return vals;
+ }
+
+}

0 comments on commit 2edbc5c

Please sign in to comment.