blob: be9ff4d0f88950a20e1df8f8d7c1bfde59028b4e [file] [log] [blame]
(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);