mirror of
https://github.com/ntop/ntopng.git
synced 2026-04-30 16:09:32 +00:00
1331 lines
39 KiB
JavaScript
1331 lines
39 KiB
JavaScript
(function(exports){
|
|
var cubism = exports.cubism = {version: "1.6.0"};
|
|
var cubism_id = 0;
|
|
function cubism_identity(d) { return d; }
|
|
cubism.option = function(name, defaultValue) {
|
|
var values = cubism.options(name);
|
|
return values.length ? values[0] : defaultValue;
|
|
};
|
|
|
|
cubism.options = function(name, defaultValues) {
|
|
var options = location.search.substring(1).split("&"),
|
|
values = [],
|
|
i = -1,
|
|
n = options.length,
|
|
o;
|
|
while (++i < n) {
|
|
if ((o = options[i].split("="))[0] == name) {
|
|
values.push(decodeURIComponent(o[1]));
|
|
}
|
|
}
|
|
return values.length || arguments.length < 2 ? values : defaultValues;
|
|
};
|
|
cubism.context = function() {
|
|
var context = new cubism_context,
|
|
step = 1e4, // ten seconds, in milliseconds
|
|
size = 1440, // four hours at ten seconds, in pixels
|
|
start0, stop0, // the start and stop for the previous change event
|
|
start1, stop1, // the start and stop for the next prepare event
|
|
serverDelay = 5e3,
|
|
clientDelay = 5e3,
|
|
event = d3.dispatch("prepare", "beforechange", "change", "focus"),
|
|
scale = context.scale = d3.time.scale().range([0, size]),
|
|
timeout,
|
|
focus;
|
|
|
|
function update() {
|
|
var now = Date.now();
|
|
stop0 = new Date(Math.floor((now - serverDelay - clientDelay) / step) * step);
|
|
start0 = new Date(stop0 - size * step);
|
|
stop1 = new Date(Math.floor((now - serverDelay) / step) * step);
|
|
start1 = new Date(stop1 - size * step);
|
|
scale.domain([start0, stop0]);
|
|
return context;
|
|
}
|
|
|
|
context.start = function() {
|
|
if (timeout) clearTimeout(timeout);
|
|
var delay = +stop1 + serverDelay - Date.now();
|
|
|
|
// If we're too late for the first prepare event, skip it.
|
|
if (delay < clientDelay) delay += step;
|
|
|
|
timeout = setTimeout(function prepare() {
|
|
stop1 = new Date(Math.floor((Date.now() - serverDelay) / step) * step);
|
|
start1 = new Date(stop1 - size * step);
|
|
event.prepare.call(context, start1, stop1);
|
|
|
|
setTimeout(function() {
|
|
scale.domain([start0 = start1, stop0 = stop1]);
|
|
event.beforechange.call(context, start1, stop1);
|
|
event.change.call(context, start1, stop1);
|
|
event.focus.call(context, focus);
|
|
}, clientDelay);
|
|
|
|
timeout = setTimeout(prepare, step);
|
|
}, delay);
|
|
return context;
|
|
};
|
|
|
|
context.stop = function() {
|
|
timeout = clearTimeout(timeout);
|
|
return context;
|
|
};
|
|
|
|
timeout = setTimeout(context.start, 10);
|
|
|
|
// Set or get the step interval in milliseconds.
|
|
// Defaults to ten seconds.
|
|
context.step = function(_) {
|
|
if (!arguments.length) return step;
|
|
step = +_;
|
|
return update();
|
|
};
|
|
|
|
// Set or get the context size (the count of metric values).
|
|
// Defaults to 1440 (four hours at ten seconds).
|
|
context.size = function(_) {
|
|
if (!arguments.length) return size;
|
|
scale.range([0, size = +_]);
|
|
return update();
|
|
};
|
|
|
|
// The server delay is the amount of time we wait for the server to compute a
|
|
// metric. This delay may result from clock skew or from delays collecting
|
|
// metrics from various hosts. Defaults to 4 seconds.
|
|
context.serverDelay = function(_) {
|
|
if (!arguments.length) return serverDelay;
|
|
serverDelay = +_;
|
|
return update();
|
|
};
|
|
|
|
// The client delay is the amount of additional time we wait to fetch those
|
|
// metrics from the server. The client and server delay combined represent the
|
|
// age of the most recent displayed metric. Defaults to 1 second.
|
|
context.clientDelay = function(_) {
|
|
if (!arguments.length) return clientDelay;
|
|
clientDelay = +_;
|
|
return update();
|
|
};
|
|
|
|
// Sets the focus to the specified index, and dispatches a "focus" event.
|
|
context.focus = function(i) {
|
|
event.focus.call(context, focus = i);
|
|
return context;
|
|
};
|
|
|
|
// Add, remove or get listeners for events.
|
|
context.on = function(type, listener) {
|
|
if (arguments.length < 2) return event.on(type);
|
|
|
|
event.on(type, listener);
|
|
|
|
// Notify the listener of the current start and stop time, as appropriate.
|
|
// This way, metrics can make requests for data immediately,
|
|
// and likewise the axis can display itself synchronously.
|
|
if (listener != null) {
|
|
if (/^prepare(\.|$)/.test(type)) listener.call(context, start1, stop1);
|
|
if (/^beforechange(\.|$)/.test(type)) listener.call(context, start0, stop0);
|
|
if (/^change(\.|$)/.test(type)) listener.call(context, start0, stop0);
|
|
if (/^focus(\.|$)/.test(type)) listener.call(context, focus);
|
|
}
|
|
|
|
return context;
|
|
};
|
|
|
|
d3.select(window).on("keydown.context-" + ++cubism_id, function() {
|
|
switch (!d3.event.metaKey && d3.event.keyCode) {
|
|
case 37: // left
|
|
if (focus == null) focus = size - 1;
|
|
if (focus > 0) context.focus(--focus);
|
|
break;
|
|
case 39: // right
|
|
if (focus == null) focus = size - 2;
|
|
if (focus < size - 1) context.focus(++focus);
|
|
break;
|
|
default: return;
|
|
}
|
|
d3.event.preventDefault();
|
|
});
|
|
|
|
return update();
|
|
};
|
|
|
|
function cubism_context() {}
|
|
|
|
var cubism_contextPrototype = cubism.context.prototype = cubism_context.prototype;
|
|
|
|
cubism_contextPrototype.constant = function(value) {
|
|
return new cubism_metricConstant(this, +value);
|
|
};
|
|
cubism_contextPrototype.cube = function(host) {
|
|
if (!arguments.length) host = "";
|
|
var source = {},
|
|
context = this;
|
|
|
|
source.metric = function(expression) {
|
|
return context.metric(function(start, stop, step, callback) {
|
|
d3.json(host + "/1.0/metric"
|
|
+ "?expression=" + encodeURIComponent(expression)
|
|
+ "&start=" + cubism_cubeFormatDate(start)
|
|
+ "&stop=" + cubism_cubeFormatDate(stop)
|
|
+ "&step=" + step, function(data) {
|
|
if (!data) return callback(new Error("unable to load data"));
|
|
callback(null, data.map(function(d) { return d.value; }));
|
|
});
|
|
}, expression += "");
|
|
};
|
|
|
|
// Returns the Cube host.
|
|
source.toString = function() {
|
|
return host;
|
|
};
|
|
|
|
return source;
|
|
};
|
|
|
|
var cubism_cubeFormatDate = d3.time.format.iso;
|
|
/* librato (http://dev.librato.com/v1/post/metrics) source
|
|
* If you want to see an example of how to use this source, check: https://gist.github.com/drio/5792680
|
|
*/
|
|
cubism_contextPrototype.librato = function(user, token) {
|
|
var source = {},
|
|
context = this;
|
|
auth_string = "Basic " + btoa(user + ":" + token);
|
|
avail_rsts = [ 1, 60, 900, 3600 ];
|
|
|
|
/* Given a step, find the best librato resolution to use.
|
|
*
|
|
* Example:
|
|
*
|
|
* (s) : cubism step
|
|
*
|
|
* avail_rsts 1 --------------- 60 --------------- 900 ---------------- 3600
|
|
* | (s) |
|
|
* | |
|
|
* [low_res top_res]
|
|
*
|
|
* return: low_res (60)
|
|
*/
|
|
function find_ideal_librato_resolution(step) {
|
|
var highest_res = avail_rsts[0],
|
|
lowest_res = avail_rsts[avail_rsts.length]; // high and lowest available resolution from librato
|
|
|
|
/* If step is outside the highest or lowest librato resolution, pick them and we are done */
|
|
if (step >= lowest_res)
|
|
return lowest_res;
|
|
|
|
if (step <= highest_res)
|
|
return highest_res;
|
|
|
|
/* If not, find in what resolution interval the step lands. */
|
|
var iof, top_res, i;
|
|
for (i=step; i<=lowest_res; i++) {
|
|
iof = avail_rsts.indexOf(i);
|
|
if (iof > -1) {
|
|
top_res = avail_rsts[iof];
|
|
break;
|
|
}
|
|
}
|
|
|
|
var low_res;
|
|
for (i=step; i>=highest_res; i--) {
|
|
iof = avail_rsts.indexOf(i);
|
|
if (iof > -1) {
|
|
low_res = avail_rsts[iof];
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* What's the closest librato resolution given the step ? */
|
|
return ((top_res-step) < (step-low_res)) ? top_res : low_res;
|
|
}
|
|
|
|
function find_librato_resolution(sdate, edate, step) {
|
|
var i_size = edate - sdate, // interval size
|
|
month = 2419200,
|
|
week = 604800,
|
|
two_days = 172800,
|
|
ideal_res;
|
|
|
|
if (i_size > month)
|
|
return 3600;
|
|
|
|
ideal_res = find_ideal_librato_resolution(step);
|
|
|
|
/*
|
|
* Now we have the ideal resolution, but due to the retention policies at librato, maybe we have
|
|
* to use a higher resolution.
|
|
* http://support.metrics.librato.com/knowledgebase/articles/66838-understanding-metrics-roll-ups-retention-and-grap
|
|
*/
|
|
if (i_size > week && ideal_res < 900)
|
|
return 900;
|
|
else if (i_size > two_days && ideal_res < 60)
|
|
return 60;
|
|
else
|
|
return ideal_res;
|
|
}
|
|
|
|
/* All the logic to query the librato API is here */
|
|
var librato_request = function(composite) {
|
|
var url_prefix = "https://metrics-api.librato.com/v1/metrics";
|
|
|
|
function make_url(sdate, edate, step) {
|
|
var params = "compose=" + composite +
|
|
"&start_time=" + sdate +
|
|
"&end_time=" + edate +
|
|
"&resolution=" + find_librato_resolution(sdate, edate, step);
|
|
return url_prefix + "?" + params;
|
|
}
|
|
|
|
/*
|
|
* We are most likely not going to get the same number of measurements
|
|
* cubism expects for a particular context: We have to perform down/up
|
|
* sampling
|
|
*/
|
|
function down_up_sampling(isdate, iedate, step, librato_mm) {
|
|
var av = [];
|
|
|
|
for (i=isdate; i<=iedate; i+=step) {
|
|
var int_mes = [];
|
|
while (librato_mm.length && librato_mm[0].measure_time <= i) {
|
|
int_mes.push(librato_mm.shift().value);
|
|
}
|
|
|
|
var v;
|
|
if (int_mes.length) { /* Compute the average */
|
|
v = int_mes.reduce(function(a, b) { return a + b }) / int_mes.length;
|
|
} else { /* No librato values on interval */
|
|
v = (av.length) ? av[av.length-1] : 0;
|
|
}
|
|
av.push(v);
|
|
}
|
|
|
|
return av;
|
|
}
|
|
|
|
request = {};
|
|
|
|
request.fire = function(isdate, iedate, step, callback_done) {
|
|
var a_values = []; /* Store partial values from librato */
|
|
|
|
/*
|
|
* Librato has a limit in the number of measurements we get back in a request (100).
|
|
* We recursively perform requests to the API to ensure we have all the data points
|
|
* for the interval we are working on.
|
|
*/
|
|
function actual_request(full_url) {
|
|
d3.json(full_url)
|
|
.header("X-Requested-With", "XMLHttpRequest")
|
|
.header("Authorization", auth_string)
|
|
.header("Librato-User-Agent", 'cubism/' + cubism.version)
|
|
.get(function (error, data) { /* Callback; data available */
|
|
if (!error) {
|
|
if (data.measurements.length === 0) {
|
|
return
|
|
}
|
|
data.measurements[0].series.forEach(function(o) { a_values.push(o); });
|
|
|
|
var still_more_values = 'query' in data && 'next_time' in data.query;
|
|
if (still_more_values) {
|
|
actual_request(make_url(data.query.next_time, iedate, step));
|
|
} else {
|
|
var a_adjusted = down_up_sampling(isdate, iedate, step, a_values);
|
|
callback_done(a_adjusted);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
actual_request(make_url(isdate, iedate, step));
|
|
};
|
|
|
|
return request;
|
|
};
|
|
|
|
/*
|
|
* The user will use this method to create a cubism source (librato in this case)
|
|
* and call .metric() as necessary to create metrics.
|
|
*/
|
|
source.metric = function(m_composite) {
|
|
return context.metric(function(start, stop, step, callback) {
|
|
/* All the librato logic is here; .fire() retrieves the metrics' data */
|
|
librato_request(m_composite)
|
|
.fire(cubism_libratoFormatDate(start),
|
|
cubism_libratoFormatDate(stop),
|
|
cubism_libratoFormatDate(step),
|
|
function(a_values) { callback(null, a_values); });
|
|
|
|
}, m_composite += "");
|
|
};
|
|
|
|
/* This is not used when the source is librato */
|
|
source.toString = function() {
|
|
return "librato";
|
|
};
|
|
|
|
return source;
|
|
};
|
|
|
|
var cubism_libratoFormatDate = function(time) {
|
|
return Math.floor(time / 1000);
|
|
};
|
|
cubism_contextPrototype.graphite = function(host) {
|
|
if (!arguments.length) host = "";
|
|
var source = {},
|
|
context = this;
|
|
|
|
source.metric = function(expression) {
|
|
var sum = "sum";
|
|
|
|
var metric = context.metric(function(start, stop, step, callback) {
|
|
var target = expression;
|
|
|
|
// Apply the summarize, if necessary.
|
|
if (step !== 1e4) target = "summarize(" + target + ",'"
|
|
+ (!(step % 36e5) ? step / 36e5 + "hour" : !(step % 6e4) ? step / 6e4 + "min" : step / 1e3 + "sec")
|
|
+ "','" + sum + "')";
|
|
|
|
d3.text(host + "/render?format=raw"
|
|
+ "&target=" + encodeURIComponent("alias(" + target + ",'')")
|
|
+ "&from=" + cubism_graphiteFormatDate(start - 2 * step) // off-by-two?
|
|
+ "&until=" + cubism_graphiteFormatDate(stop - 1000), function(text) {
|
|
if (!text) return callback(new Error("unable to load data"));
|
|
callback(null, cubism_graphiteParse(text));
|
|
});
|
|
}, expression += "");
|
|
|
|
metric.summarize = function(_) {
|
|
sum = _;
|
|
return metric;
|
|
};
|
|
|
|
return metric;
|
|
};
|
|
|
|
source.find = function(pattern, callback) {
|
|
d3.json(host + "/metrics/find?format=completer"
|
|
+ "&query=" + encodeURIComponent(pattern), function(result) {
|
|
if (!result) return callback(new Error("unable to find metrics"));
|
|
callback(null, result.metrics.map(function(d) { return d.path; }));
|
|
});
|
|
};
|
|
|
|
// Returns the graphite host.
|
|
source.toString = function() {
|
|
return host;
|
|
};
|
|
|
|
return source;
|
|
};
|
|
|
|
// Graphite understands seconds since UNIX epoch.
|
|
function cubism_graphiteFormatDate(time) {
|
|
return Math.floor(time / 1000);
|
|
}
|
|
|
|
// Helper method for parsing graphite's raw format.
|
|
function cubism_graphiteParse(text) {
|
|
var i = text.indexOf("|"),
|
|
meta = text.substring(0, i),
|
|
c = meta.lastIndexOf(","),
|
|
b = meta.lastIndexOf(",", c - 1),
|
|
a = meta.lastIndexOf(",", b - 1),
|
|
start = meta.substring(a + 1, b) * 1000,
|
|
step = meta.substring(c + 1) * 1000;
|
|
return text
|
|
.substring(i + 1)
|
|
.split(",")
|
|
.slice(1) // the first value is always None?
|
|
.map(function(d) { return +d; });
|
|
}
|
|
cubism_contextPrototype.gangliaWeb = function(config) {
|
|
var host = '',
|
|
uriPathPrefix = '/ganglia2/';
|
|
|
|
if (arguments.length) {
|
|
if (config.host) {
|
|
host = config.host;
|
|
}
|
|
|
|
if (config.uriPathPrefix) {
|
|
uriPathPrefix = config.uriPathPrefix;
|
|
|
|
/* Add leading and trailing slashes, as appropriate. */
|
|
if( uriPathPrefix[0] != '/' ) {
|
|
uriPathPrefix = '/' + uriPathPrefix;
|
|
}
|
|
|
|
if( uriPathPrefix[uriPathPrefix.length - 1] != '/' ) {
|
|
uriPathPrefix += '/';
|
|
}
|
|
}
|
|
}
|
|
|
|
var source = {},
|
|
context = this;
|
|
|
|
source.metric = function(metricInfo) {
|
|
|
|
/* Store the members from metricInfo into local variables. */
|
|
var clusterName = metricInfo.clusterName,
|
|
metricName = metricInfo.metricName,
|
|
hostName = metricInfo.hostName,
|
|
isReport = metricInfo.isReport || false,
|
|
titleGenerator = metricInfo.titleGenerator ||
|
|
/* Reasonable (not necessarily pretty) default for titleGenerator. */
|
|
function(unusedMetricInfo) {
|
|
/* unusedMetricInfo is, well, unused in this default case. */
|
|
return ('clusterName:' + clusterName +
|
|
' metricName:' + metricName +
|
|
(hostName ? ' hostName:' + hostName : ''));
|
|
},
|
|
onChangeCallback = metricInfo.onChangeCallback;
|
|
|
|
/* Default to plain, simple metrics. */
|
|
var metricKeyName = isReport ? 'g' : 'm';
|
|
|
|
var gangliaWebMetric = context.metric(function(start, stop, step, callback) {
|
|
|
|
function constructGangliaWebRequestQueryParams() {
|
|
return ('c=' + clusterName +
|
|
'&' + metricKeyName + '=' + metricName +
|
|
(hostName ? '&h=' + hostName : '') +
|
|
'&cs=' + start/1000 + '&ce=' + stop/1000 + '&step=' + step/1000 + '&graphlot=1');
|
|
}
|
|
|
|
d3.json(host + uriPathPrefix + 'graph.php?' + constructGangliaWebRequestQueryParams(),
|
|
function(result) {
|
|
if( !result ) {
|
|
return callback(new Error("Unable to fetch GangliaWeb data"));
|
|
}
|
|
|
|
callback(null, result[0].data);
|
|
});
|
|
|
|
}, titleGenerator(metricInfo));
|
|
|
|
gangliaWebMetric.toString = function() {
|
|
return titleGenerator(metricInfo);
|
|
};
|
|
|
|
/* Allow users to run their custom code each time a gangliaWebMetric changes.
|
|
*
|
|
* TODO Consider abstracting away the naked Cubism call, and instead exposing
|
|
* a callback that takes in the values array (maybe alongwith the original
|
|
* start and stop 'naked' parameters), since it's handy to have the entire
|
|
* dataset at your disposal (and users will likely implement onChangeCallback
|
|
* primarily to get at this dataset).
|
|
*/
|
|
if (onChangeCallback) {
|
|
gangliaWebMetric.on('change', onChangeCallback);
|
|
}
|
|
|
|
return gangliaWebMetric;
|
|
};
|
|
|
|
// Returns the gangliaWeb host + uriPathPrefix.
|
|
source.toString = function() {
|
|
return host + uriPathPrefix;
|
|
};
|
|
|
|
return source;
|
|
};
|
|
|
|
function cubism_metric(context) {
|
|
if (!(context instanceof cubism_context)) throw new Error("invalid context");
|
|
this.context = context;
|
|
}
|
|
|
|
var cubism_metricPrototype = cubism_metric.prototype;
|
|
|
|
cubism.metric = cubism_metric;
|
|
|
|
cubism_metricPrototype.valueAt = function() {
|
|
return NaN;
|
|
};
|
|
|
|
cubism_metricPrototype.alias = function(name) {
|
|
this.toString = function() { return name; };
|
|
return this;
|
|
};
|
|
|
|
cubism_metricPrototype.extent = function() {
|
|
var i = 0,
|
|
n = this.context.size(),
|
|
value,
|
|
min = Infinity,
|
|
max = -Infinity;
|
|
while (++i < n) {
|
|
value = this.valueAt(i);
|
|
if (value < min) min = value;
|
|
if (value > max) max = value;
|
|
}
|
|
return [min, max];
|
|
};
|
|
|
|
cubism_metricPrototype.on = function(type, listener) {
|
|
return arguments.length < 2 ? null : this;
|
|
};
|
|
|
|
cubism_metricPrototype.shift = function() {
|
|
return this;
|
|
};
|
|
|
|
cubism_metricPrototype.on = function() {
|
|
return arguments.length < 2 ? null : this;
|
|
};
|
|
|
|
cubism_contextPrototype.metric = function(request, name) {
|
|
var context = this,
|
|
metric = new cubism_metric(context),
|
|
id = ".metric-" + ++cubism_id,
|
|
start = -Infinity,
|
|
stop,
|
|
step = context.step(),
|
|
size = context.size(),
|
|
values = [],
|
|
event = d3.dispatch("change"),
|
|
listening = 0,
|
|
fetching;
|
|
|
|
// Prefetch new data into a temporary array.
|
|
function prepare(start1, stop) {
|
|
var steps = Math.min(size, Math.round((start1 - start) / step));
|
|
if (!steps || fetching) return; // already fetched, or fetching!
|
|
fetching = true;
|
|
steps = Math.min(size, steps + cubism_metricOverlap);
|
|
var start0 = new Date(stop - steps * step);
|
|
request(start0, stop, step, function(error, data) {
|
|
fetching = false;
|
|
if (error) return console.warn(error);
|
|
var i = isFinite(start) ? Math.round((start0 - start) / step) : 0;
|
|
for (var j = 0, m = data.length; j < m; ++j) values[j + i] = data[j];
|
|
event.change.call(metric, start, stop);
|
|
});
|
|
}
|
|
|
|
// When the context changes, switch to the new data, ready-or-not!
|
|
function beforechange(start1, stop1) {
|
|
if (!isFinite(start)) start = start1;
|
|
values.splice(0, Math.max(0, Math.min(size, Math.round((start1 - start) / step))));
|
|
start = start1;
|
|
stop = stop1;
|
|
}
|
|
|
|
//
|
|
metric.valueAt = function(i) {
|
|
return values[i];
|
|
};
|
|
|
|
//
|
|
metric.shift = function(offset) {
|
|
return context.metric(cubism_metricShift(request, +offset));
|
|
};
|
|
|
|
//
|
|
metric.on = function(type, listener) {
|
|
if (!arguments.length) return event.on(type);
|
|
|
|
// If there are no listeners, then stop listening to the context,
|
|
// and avoid unnecessary fetches.
|
|
if (listener == null) {
|
|
if (event.on(type) != null && --listening == 0) {
|
|
context.on("prepare" + id, null).on("beforechange" + id, null);
|
|
}
|
|
} else {
|
|
if (event.on(type) == null && ++listening == 1) {
|
|
context.on("prepare" + id, prepare).on("beforechange" + id, beforechange);
|
|
}
|
|
}
|
|
|
|
event.on(type, listener);
|
|
|
|
// Notify the listener of the current start and stop time, as appropriate.
|
|
// This way, charts can display synchronous metrics immediately.
|
|
if (listener != null) {
|
|
if (/^change(\.|$)/.test(type)) listener.call(context, start, stop);
|
|
}
|
|
|
|
return metric;
|
|
};
|
|
|
|
//
|
|
if (arguments.length > 1) metric.toString = function() {
|
|
return name;
|
|
};
|
|
|
|
return metric;
|
|
};
|
|
|
|
// Number of metric to refetch each period, in case of lag.
|
|
var cubism_metricOverlap = 6;
|
|
|
|
// Wraps the specified request implementation, and shifts time by the given offset.
|
|
function cubism_metricShift(request, offset) {
|
|
return function(start, stop, step, callback) {
|
|
request(new Date(+start + offset), new Date(+stop + offset), step, callback);
|
|
};
|
|
}
|
|
function cubism_metricConstant(context, value) {
|
|
cubism_metric.call(this, context);
|
|
value = +value;
|
|
var name = value + "";
|
|
this.valueOf = function() { return value; };
|
|
this.toString = function() { return name; };
|
|
}
|
|
|
|
var cubism_metricConstantPrototype = cubism_metricConstant.prototype = Object.create(cubism_metric.prototype);
|
|
|
|
cubism_metricConstantPrototype.valueAt = function() {
|
|
return +this;
|
|
};
|
|
|
|
cubism_metricConstantPrototype.extent = function() {
|
|
return [+this, +this];
|
|
};
|
|
function cubism_metricOperator(name, operate) {
|
|
|
|
function cubism_metricOperator(left, right) {
|
|
if (!(right instanceof cubism_metric)) right = new cubism_metricConstant(left.context, right);
|
|
else if (left.context !== right.context) throw new Error("mismatch context");
|
|
cubism_metric.call(this, left.context);
|
|
this.left = left;
|
|
this.right = right;
|
|
this.toString = function() { return left + " " + name + " " + right; };
|
|
}
|
|
|
|
var cubism_metricOperatorPrototype = cubism_metricOperator.prototype = Object.create(cubism_metric.prototype);
|
|
|
|
cubism_metricOperatorPrototype.valueAt = function(i) {
|
|
return operate(this.left.valueAt(i), this.right.valueAt(i));
|
|
};
|
|
|
|
cubism_metricOperatorPrototype.shift = function(offset) {
|
|
return new cubism_metricOperator(this.left.shift(offset), this.right.shift(offset));
|
|
};
|
|
|
|
cubism_metricOperatorPrototype.on = function(type, listener) {
|
|
if (arguments.length < 2) return this.left.on(type);
|
|
this.left.on(type, listener);
|
|
this.right.on(type, listener);
|
|
return this;
|
|
};
|
|
|
|
return function(right) {
|
|
return new cubism_metricOperator(this, right);
|
|
};
|
|
}
|
|
|
|
cubism_metricPrototype.add = cubism_metricOperator("+", function(left, right) {
|
|
return left + right;
|
|
});
|
|
|
|
cubism_metricPrototype.subtract = cubism_metricOperator("-", function(left, right) {
|
|
return left - right;
|
|
});
|
|
|
|
cubism_metricPrototype.multiply = cubism_metricOperator("*", function(left, right) {
|
|
return left * right;
|
|
});
|
|
|
|
cubism_metricPrototype.divide = cubism_metricOperator("/", function(left, right) {
|
|
return left / right;
|
|
});
|
|
cubism_contextPrototype.horizon = function() {
|
|
var context = this,
|
|
mode = "offset",
|
|
buffer = document.createElement("canvas"),
|
|
width = buffer.width = context.size(),
|
|
height = buffer.height = 30,
|
|
scale = d3.scale.linear().interpolate(d3.interpolateRound),
|
|
metric = cubism_identity,
|
|
extent = null,
|
|
title = cubism_identity,
|
|
format = d3.format(".2s"),
|
|
colors = ["#08519c","#3182bd","#6baed6","#bdd7e7","#bae4b3","#74c476","#31a354","#006d2c"];
|
|
|
|
function horizon(selection) {
|
|
|
|
selection
|
|
.on("mousemove.horizon", function() { context.focus(Math.round(d3.mouse(this)[0])); })
|
|
.on("mouseout.horizon", function() { context.focus(null); });
|
|
|
|
selection.append("canvas")
|
|
.attr("width", width)
|
|
.attr("height", height);
|
|
|
|
selection.append("span")
|
|
.attr("class", "title")
|
|
.text(title);
|
|
|
|
selection.append("span")
|
|
.attr("class", "value");
|
|
|
|
selection.each(function(d, i) {
|
|
var that = this,
|
|
id = ++cubism_id,
|
|
metric_ = typeof metric === "function" ? metric.call(that, d, i) : metric,
|
|
colors_ = typeof colors === "function" ? colors.call(that, d, i) : colors,
|
|
extent_ = typeof extent === "function" ? extent.call(that, d, i) : extent,
|
|
start = -Infinity,
|
|
step = context.step(),
|
|
canvas = d3.select(that).select("canvas"),
|
|
span = d3.select(that).select(".value"),
|
|
max_,
|
|
m = colors_.length >> 1,
|
|
ready;
|
|
|
|
canvas.datum({id: id, metric: metric_});
|
|
canvas = canvas.node().getContext("2d");
|
|
|
|
function change(start1, stop) {
|
|
canvas.save();
|
|
|
|
// compute the new extent and ready flag
|
|
var extent = metric_.extent();
|
|
ready = extent.every(isFinite);
|
|
if (extent_ != null) extent = extent_;
|
|
|
|
// if this is an update (with no extent change), copy old values!
|
|
var i0 = 0, max = Math.max(-extent[0], extent[1]);
|
|
if (this === context) {
|
|
if (max == max_) {
|
|
i0 = width - cubism_metricOverlap;
|
|
var dx = (start1 - start) / step;
|
|
if (dx < width) {
|
|
var canvas0 = buffer.getContext("2d");
|
|
canvas0.clearRect(0, 0, width, height);
|
|
canvas0.drawImage(canvas.canvas, dx, 0, width - dx, height, 0, 0, width - dx, height);
|
|
canvas.clearRect(0, 0, width, height);
|
|
canvas.drawImage(canvas0.canvas, 0, 0);
|
|
}
|
|
}
|
|
start = start1;
|
|
}
|
|
|
|
// update the domain
|
|
scale.domain([0, max_ = max]);
|
|
|
|
// clear for the new data
|
|
canvas.clearRect(i0, 0, width - i0, height);
|
|
|
|
// record whether there are negative values to display
|
|
var negative;
|
|
|
|
// positive bands
|
|
for (var j = 0; j < m; ++j) {
|
|
canvas.fillStyle = colors_[m + j];
|
|
|
|
// Adjust the range based on the current band index.
|
|
var y0 = (j - m + 1) * height;
|
|
scale.range([m * height + y0, y0]);
|
|
y0 = scale(0);
|
|
|
|
for (var i = i0, n = width, y1; i < n; ++i) {
|
|
y1 = metric_.valueAt(i);
|
|
if (y1 <= 0) { negative = true; continue; }
|
|
if (y1 === undefined) continue;
|
|
canvas.fillRect(i, y1 = scale(y1), 1, y0 - y1);
|
|
}
|
|
}
|
|
|
|
if (negative) {
|
|
// enable offset mode
|
|
if (mode === "offset") {
|
|
canvas.translate(0, height);
|
|
canvas.scale(1, -1);
|
|
}
|
|
|
|
// negative bands
|
|
for (var j = 0; j < m; ++j) {
|
|
canvas.fillStyle = colors_[m - 1 - j];
|
|
|
|
// Adjust the range based on the current band index.
|
|
var y0 = (j - m + 1) * height;
|
|
scale.range([m * height + y0, y0]);
|
|
y0 = scale(0);
|
|
|
|
for (var i = i0, n = width, y1; i < n; ++i) {
|
|
y1 = metric_.valueAt(i);
|
|
if (y1 >= 0) continue;
|
|
canvas.fillRect(i, scale(-y1), 1, y0 - scale(-y1));
|
|
}
|
|
}
|
|
}
|
|
|
|
canvas.restore();
|
|
}
|
|
|
|
function focus(i) {
|
|
if (i == null) i = width - 1;
|
|
var value = metric_.valueAt(i);
|
|
span.datum(value).text(isNaN(value) ? null : format);
|
|
}
|
|
|
|
// Update the chart when the context changes.
|
|
context.on("change.horizon-" + id, change);
|
|
context.on("focus.horizon-" + id, focus);
|
|
|
|
// Display the first metric change immediately,
|
|
// but defer subsequent updates to the canvas change.
|
|
// Note that someone still needs to listen to the metric,
|
|
// so that it continues to update automatically.
|
|
metric_.on("change.horizon-" + id, function(start, stop) {
|
|
change(start, stop), focus();
|
|
if (ready) metric_.on("change.horizon-" + id, cubism_identity);
|
|
});
|
|
});
|
|
}
|
|
|
|
horizon.remove = function(selection) {
|
|
|
|
selection
|
|
.on("mousemove.horizon", null)
|
|
.on("mouseout.horizon", null);
|
|
|
|
selection.selectAll("canvas")
|
|
.each(remove)
|
|
.remove();
|
|
|
|
selection.selectAll(".title,.value")
|
|
.remove();
|
|
|
|
function remove(d) {
|
|
d.metric.on("change.horizon-" + d.id, null);
|
|
context.on("change.horizon-" + d.id, null);
|
|
context.on("focus.horizon-" + d.id, null);
|
|
}
|
|
};
|
|
|
|
horizon.mode = function(_) {
|
|
if (!arguments.length) return mode;
|
|
mode = _ + "";
|
|
return horizon;
|
|
};
|
|
|
|
horizon.height = function(_) {
|
|
if (!arguments.length) return height;
|
|
buffer.height = height = +_;
|
|
return horizon;
|
|
};
|
|
|
|
horizon.metric = function(_) {
|
|
if (!arguments.length) return metric;
|
|
metric = _;
|
|
return horizon;
|
|
};
|
|
|
|
horizon.scale = function(_) {
|
|
if (!arguments.length) return scale;
|
|
scale = _;
|
|
return horizon;
|
|
};
|
|
|
|
horizon.extent = function(_) {
|
|
if (!arguments.length) return extent;
|
|
extent = _;
|
|
return horizon;
|
|
};
|
|
|
|
horizon.title = function(_) {
|
|
if (!arguments.length) return title;
|
|
title = _;
|
|
return horizon;
|
|
};
|
|
|
|
horizon.format = function(_) {
|
|
if (!arguments.length) return format;
|
|
format = _;
|
|
return horizon;
|
|
};
|
|
|
|
horizon.colors = function(_) {
|
|
if (!arguments.length) return colors;
|
|
colors = _;
|
|
return horizon;
|
|
};
|
|
|
|
return horizon;
|
|
};
|
|
cubism_contextPrototype.comparison = function() {
|
|
var context = this,
|
|
width = context.size(),
|
|
height = 120,
|
|
scale = d3.scale.linear().interpolate(d3.interpolateRound),
|
|
primary = function(d) { return d[0]; },
|
|
secondary = function(d) { return d[1]; },
|
|
extent = null,
|
|
title = cubism_identity,
|
|
formatPrimary = cubism_comparisonPrimaryFormat,
|
|
formatChange = cubism_comparisonChangeFormat,
|
|
colors = ["#9ecae1", "#225b84", "#a1d99b", "#22723a"],
|
|
strokeWidth = 1.5;
|
|
|
|
function comparison(selection) {
|
|
|
|
selection
|
|
.on("mousemove.comparison", function() { context.focus(Math.round(d3.mouse(this)[0])); })
|
|
.on("mouseout.comparison", function() { context.focus(null); });
|
|
|
|
selection.append("canvas")
|
|
.attr("width", width)
|
|
.attr("height", height);
|
|
|
|
selection.append("span")
|
|
.attr("class", "title")
|
|
.text(title);
|
|
|
|
selection.append("span")
|
|
.attr("class", "value primary");
|
|
|
|
selection.append("span")
|
|
.attr("class", "value change");
|
|
|
|
selection.each(function(d, i) {
|
|
var that = this,
|
|
id = ++cubism_id,
|
|
primary_ = typeof primary === "function" ? primary.call(that, d, i) : primary,
|
|
secondary_ = typeof secondary === "function" ? secondary.call(that, d, i) : secondary,
|
|
extent_ = typeof extent === "function" ? extent.call(that, d, i) : extent,
|
|
div = d3.select(that),
|
|
canvas = div.select("canvas"),
|
|
spanPrimary = div.select(".value.primary"),
|
|
spanChange = div.select(".value.change"),
|
|
ready;
|
|
|
|
canvas.datum({id: id, primary: primary_, secondary: secondary_});
|
|
canvas = canvas.node().getContext("2d");
|
|
|
|
function change(start, stop) {
|
|
canvas.save();
|
|
canvas.clearRect(0, 0, width, height);
|
|
|
|
// update the scale
|
|
var primaryExtent = primary_.extent(),
|
|
secondaryExtent = secondary_.extent(),
|
|
extent = extent_ == null ? primaryExtent : extent_;
|
|
scale.domain(extent).range([height, 0]);
|
|
ready = primaryExtent.concat(secondaryExtent).every(isFinite);
|
|
|
|
// consistent overplotting
|
|
var round = start / context.step() & 1
|
|
? cubism_comparisonRoundOdd
|
|
: cubism_comparisonRoundEven;
|
|
|
|
// positive changes
|
|
canvas.fillStyle = colors[2];
|
|
for (var i = 0, n = width; i < n; ++i) {
|
|
var y0 = scale(primary_.valueAt(i)),
|
|
y1 = scale(secondary_.valueAt(i));
|
|
if (y0 < y1) canvas.fillRect(round(i), y0, 1, y1 - y0);
|
|
}
|
|
|
|
// negative changes
|
|
canvas.fillStyle = colors[0];
|
|
for (i = 0; i < n; ++i) {
|
|
var y0 = scale(primary_.valueAt(i)),
|
|
y1 = scale(secondary_.valueAt(i));
|
|
if (y0 > y1) canvas.fillRect(round(i), y1, 1, y0 - y1);
|
|
}
|
|
|
|
// positive values
|
|
canvas.fillStyle = colors[3];
|
|
for (i = 0; i < n; ++i) {
|
|
var y0 = scale(primary_.valueAt(i)),
|
|
y1 = scale(secondary_.valueAt(i));
|
|
if (y0 <= y1) canvas.fillRect(round(i), y0, 1, strokeWidth);
|
|
}
|
|
|
|
// negative values
|
|
canvas.fillStyle = colors[1];
|
|
for (i = 0; i < n; ++i) {
|
|
var y0 = scale(primary_.valueAt(i)),
|
|
y1 = scale(secondary_.valueAt(i));
|
|
if (y0 > y1) canvas.fillRect(round(i), y0 - strokeWidth, 1, strokeWidth);
|
|
}
|
|
|
|
canvas.restore();
|
|
}
|
|
|
|
function focus(i) {
|
|
if (i == null) i = width - 1;
|
|
var valuePrimary = primary_.valueAt(i),
|
|
valueSecondary = secondary_.valueAt(i),
|
|
valueChange = (valuePrimary - valueSecondary) / valueSecondary;
|
|
|
|
spanPrimary
|
|
.datum(valuePrimary)
|
|
.text(isNaN(valuePrimary) ? null : formatPrimary);
|
|
|
|
spanChange
|
|
.datum(valueChange)
|
|
.text(isNaN(valueChange) ? null : formatChange)
|
|
.attr("class", "value change " + (valueChange > 0 ? "positive" : valueChange < 0 ? "negative" : ""));
|
|
}
|
|
|
|
// Display the first primary change immediately,
|
|
// but defer subsequent updates to the context change.
|
|
// Note that someone still needs to listen to the metric,
|
|
// so that it continues to update automatically.
|
|
primary_.on("change.comparison-" + id, firstChange);
|
|
secondary_.on("change.comparison-" + id, firstChange);
|
|
function firstChange(start, stop) {
|
|
change(start, stop), focus();
|
|
if (ready) {
|
|
primary_.on("change.comparison-" + id, cubism_identity);
|
|
secondary_.on("change.comparison-" + id, cubism_identity);
|
|
}
|
|
}
|
|
|
|
// Update the chart when the context changes.
|
|
context.on("change.comparison-" + id, change);
|
|
context.on("focus.comparison-" + id, focus);
|
|
});
|
|
}
|
|
|
|
comparison.remove = function(selection) {
|
|
|
|
selection
|
|
.on("mousemove.comparison", null)
|
|
.on("mouseout.comparison", null);
|
|
|
|
selection.selectAll("canvas")
|
|
.each(remove)
|
|
.remove();
|
|
|
|
selection.selectAll(".title,.value")
|
|
.remove();
|
|
|
|
function remove(d) {
|
|
d.primary.on("change.comparison-" + d.id, null);
|
|
d.secondary.on("change.comparison-" + d.id, null);
|
|
context.on("change.comparison-" + d.id, null);
|
|
context.on("focus.comparison-" + d.id, null);
|
|
}
|
|
};
|
|
|
|
comparison.height = function(_) {
|
|
if (!arguments.length) return height;
|
|
height = +_;
|
|
return comparison;
|
|
};
|
|
|
|
comparison.primary = function(_) {
|
|
if (!arguments.length) return primary;
|
|
primary = _;
|
|
return comparison;
|
|
};
|
|
|
|
comparison.secondary = function(_) {
|
|
if (!arguments.length) return secondary;
|
|
secondary = _;
|
|
return comparison;
|
|
};
|
|
|
|
comparison.scale = function(_) {
|
|
if (!arguments.length) return scale;
|
|
scale = _;
|
|
return comparison;
|
|
};
|
|
|
|
comparison.extent = function(_) {
|
|
if (!arguments.length) return extent;
|
|
extent = _;
|
|
return comparison;
|
|
};
|
|
|
|
comparison.title = function(_) {
|
|
if (!arguments.length) return title;
|
|
title = _;
|
|
return comparison;
|
|
};
|
|
|
|
comparison.formatPrimary = function(_) {
|
|
if (!arguments.length) return formatPrimary;
|
|
formatPrimary = _;
|
|
return comparison;
|
|
};
|
|
|
|
comparison.formatChange = function(_) {
|
|
if (!arguments.length) return formatChange;
|
|
formatChange = _;
|
|
return comparison;
|
|
};
|
|
|
|
comparison.colors = function(_) {
|
|
if (!arguments.length) return colors;
|
|
colors = _;
|
|
return comparison;
|
|
};
|
|
|
|
comparison.strokeWidth = function(_) {
|
|
if (!arguments.length) return strokeWidth;
|
|
strokeWidth = _;
|
|
return comparison;
|
|
};
|
|
|
|
return comparison;
|
|
};
|
|
|
|
var cubism_comparisonPrimaryFormat = d3.format(".2s"),
|
|
cubism_comparisonChangeFormat = d3.format("+.0%");
|
|
|
|
function cubism_comparisonRoundEven(i) {
|
|
return i & 0xfffffe;
|
|
}
|
|
|
|
function cubism_comparisonRoundOdd(i) {
|
|
return ((i + 1) & 0xfffffe) - 1;
|
|
}
|
|
cubism_contextPrototype.axis = function() {
|
|
var context = this,
|
|
scale = context.scale,
|
|
axis_ = d3.svg.axis().scale(scale);
|
|
|
|
var formatDefault = context.step() < 6e4 ? cubism_axisFormatSeconds
|
|
: context.step() < 864e5 ? cubism_axisFormatMinutes
|
|
: cubism_axisFormatDays;
|
|
var format = formatDefault;
|
|
|
|
function axis(selection) {
|
|
var id = ++cubism_id,
|
|
tick;
|
|
|
|
var g = selection.append("svg")
|
|
.datum({id: id})
|
|
.attr("width", context.size())
|
|
.attr("height", Math.max(28, -axis.tickSize()))
|
|
.append("g")
|
|
.attr("transform", "translate(0," + (axis_.orient() === "top" ? 27 : 4) + ")")
|
|
.call(axis_);
|
|
|
|
context.on("change.axis-" + id, function() {
|
|
g.call(axis_);
|
|
if (!tick) tick = d3.select(g.node().appendChild(g.selectAll("text").node().cloneNode(true)))
|
|
.style("display", "none")
|
|
.text(null);
|
|
});
|
|
|
|
context.on("focus.axis-" + id, function(i) {
|
|
if (tick) {
|
|
if (i == null) {
|
|
tick.style("display", "none");
|
|
g.selectAll("text").style("fill-opacity", null);
|
|
} else {
|
|
tick.style("display", null).attr("x", i).text(format(scale.invert(i)));
|
|
var dx = tick.node().getComputedTextLength() + 6;
|
|
g.selectAll("text").style("fill-opacity", function(d) { return Math.abs(scale(d) - i) < dx ? 0 : 1; });
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
axis.remove = function(selection) {
|
|
|
|
selection.selectAll("svg")
|
|
.each(remove)
|
|
.remove();
|
|
|
|
function remove(d) {
|
|
context.on("change.axis-" + d.id, null);
|
|
context.on("focus.axis-" + d.id, null);
|
|
}
|
|
};
|
|
|
|
axis.focusFormat = function(_) {
|
|
if (!arguments.length) return format == formatDefault ? null : _;
|
|
format = _ == null ? formatDefault : _;
|
|
return axis;
|
|
};
|
|
|
|
return d3.rebind(axis, axis_,
|
|
"orient",
|
|
"ticks",
|
|
"tickSubdivide",
|
|
"tickSize",
|
|
"tickPadding",
|
|
"tickFormat");
|
|
};
|
|
|
|
var cubism_axisFormatSeconds = d3.time.format("%I:%M:%S %p"),
|
|
cubism_axisFormatMinutes = d3.time.format("%I:%M %p"),
|
|
cubism_axisFormatDays = d3.time.format("%B %d");
|
|
cubism_contextPrototype.rule = function() {
|
|
var context = this,
|
|
metric = cubism_identity;
|
|
|
|
function rule(selection) {
|
|
var id = ++cubism_id;
|
|
|
|
var line = selection.append("div")
|
|
.datum({id: id})
|
|
.attr("class", "line")
|
|
.call(cubism_ruleStyle);
|
|
|
|
selection.each(function(d, i) {
|
|
var that = this,
|
|
id = ++cubism_id,
|
|
metric_ = typeof metric === "function" ? metric.call(that, d, i) : metric;
|
|
|
|
if (!metric_) return;
|
|
|
|
function change(start, stop) {
|
|
var values = [];
|
|
|
|
for (var i = 0, n = context.size(); i < n; ++i) {
|
|
if (metric_.valueAt(i)) {
|
|
values.push(i);
|
|
}
|
|
}
|
|
|
|
var lines = selection.selectAll(".metric").data(values);
|
|
lines.exit().remove();
|
|
lines.enter().append("div").attr("class", "metric line").call(cubism_ruleStyle);
|
|
lines.style("left", cubism_ruleLeft);
|
|
}
|
|
|
|
context.on("change.rule-" + id, change);
|
|
metric_.on("change.rule-" + id, change);
|
|
});
|
|
|
|
context.on("focus.rule-" + id, function(i) {
|
|
line.datum(i)
|
|
.style("display", i == null ? "none" : null)
|
|
.style("left", i == null ? null : cubism_ruleLeft);
|
|
});
|
|
}
|
|
|
|
rule.remove = function(selection) {
|
|
|
|
selection.selectAll(".line")
|
|
.each(remove)
|
|
.remove();
|
|
|
|
function remove(d) {
|
|
context.on("focus.rule-" + d.id, null);
|
|
}
|
|
};
|
|
|
|
rule.metric = function(_) {
|
|
if (!arguments.length) return metric;
|
|
metric = _;
|
|
return rule;
|
|
};
|
|
|
|
return rule;
|
|
};
|
|
|
|
function cubism_ruleStyle(line) {
|
|
line
|
|
.style("position", "absolute")
|
|
.style("top", 0)
|
|
.style("bottom", 0)
|
|
.style("width", "1px")
|
|
.style("pointer-events", "none");
|
|
}
|
|
|
|
function cubism_ruleLeft(i) {
|
|
return i + "px";
|
|
}
|
|
})(this);
|