summaryrefslogtreecommitdiff
path: root/priv/static/js/metricsgraphics/misc
diff options
context:
space:
mode:
authorhref <href@random.sh>2021-09-01 10:30:18 +0200
committerhref <href@random.sh>2021-09-01 10:30:18 +0200
commit75687711f35355bc30e4829439384aab28fcac6d (patch)
tree8f3256f472893c39720a684d390e890a152f7303 /priv/static/js/metricsgraphics/misc
parentlink: post_* callbacks; html & pdftitle. (diff)
Commit all the changes that hasn't been committed + updates.
Diffstat (limited to 'priv/static/js/metricsgraphics/misc')
-rw-r--r--priv/static/js/metricsgraphics/misc/error.js16
-rw-r--r--priv/static/js/metricsgraphics/misc/formatters.js147
-rw-r--r--priv/static/js/metricsgraphics/misc/markup.js66
-rw-r--r--priv/static/js/metricsgraphics/misc/process.js368
-rw-r--r--priv/static/js/metricsgraphics/misc/smoothers.js280
-rw-r--r--priv/static/js/metricsgraphics/misc/transitions.js31
-rw-r--r--priv/static/js/metricsgraphics/misc/utility.js619
7 files changed, 1527 insertions, 0 deletions
diff --git a/priv/static/js/metricsgraphics/misc/error.js b/priv/static/js/metricsgraphics/misc/error.js
new file mode 100644
index 0000000..cf2eee3
--- /dev/null
+++ b/priv/static/js/metricsgraphics/misc/error.js
@@ -0,0 +1,16 @@
+// call this to add a warning icon to a graph and log an error to the console
+function error(args) {
+ console.error('ERROR : ', args.target, ' : ', args.error);
+
+ d3.select(args.target).select('.mg-chart-title')
+ .append('tspan')
+ .attr('class', 'fa fa-x fa-exclamation-circle mg-warning')
+ .attr('dx', '0.3em')
+ .text('\uf06a');
+}
+
+function internal_error(args) {
+ console.error('INTERNAL ERROR : ', args.target, ' : ', args.internal_error);
+}
+
+MG.error = error;
diff --git a/priv/static/js/metricsgraphics/misc/formatters.js b/priv/static/js/metricsgraphics/misc/formatters.js
new file mode 100644
index 0000000..1533fd5
--- /dev/null
+++ b/priv/static/js/metricsgraphics/misc/formatters.js
@@ -0,0 +1,147 @@
+function format_rollover_number(args) {
+ var num;
+ if (args.format === 'count') {
+ num = function(d) {
+ var is_float = d % 1 !== 0;
+ var pf;
+
+ if (is_float) {
+ pf = d3.format(',.' + args.decimals + 'f');
+ } else {
+ pf = d3.format(',.0f');
+ }
+
+ // are we adding units after the value or before?
+ if (args.yax_units_append) {
+ return pf(d) + args.yax_units;
+ } else {
+ return args.yax_units + pf(d);
+ }
+ };
+ } else {
+ num = function(d_) {
+ var fmt_string = (isNumeric(args.decimals) ? '.' + args.decimals : '') + '%';
+ var pf = d3.format(fmt_string);
+ return pf(d_);
+ };
+ }
+ return num;
+}
+
+var time_rollover_format = function(f, d, accessor, utc) {
+ var fd;
+ if (typeof f === 'string') {
+ fd = MG.time_format(utc, f)(d[accessor]);
+ } else if (typeof f === 'function') {
+ fd = f(d);
+ } else {
+ fd = d[accessor];
+ }
+ return fd;
+};
+
+// define our rollover format for numbers
+var number_rollover_format = function(f, d, accessor) {
+ var fd;
+ if (typeof f === 'string') {
+ fd = d3.format('s')(d[accessor]);
+ } else if (typeof f === 'function') {
+ fd = f(d);
+ } else {
+ fd = d[accessor];
+ }
+ return fd;
+};
+
+function mg_format_y_rollover(args, num, d) {
+ var formatted_y;
+ if (args.y_mouseover !== null) {
+ if (args.aggregate_rollover) {
+ formatted_y = number_rollover_format(args.y_mouseover, d, args.y_accessor);
+ } else {
+ formatted_y = number_rollover_format(args.y_mouseover, d, args.y_accessor);
+ }
+ } else {
+ if (args.time_series) {
+ if (args.aggregate_rollover) {
+ formatted_y = num(d[args.y_accessor]);
+ } else {
+ formatted_y = args.yax_units + num(d[args.y_accessor]);
+ }
+ } else {
+ formatted_y = args.y_accessor + ': ' + args.yax_units + num(d[args.y_accessor]);
+ }
+ }
+ return formatted_y;
+}
+
+function mg_format_x_rollover(args, fmt, d) {
+ var formatted_x;
+ if (args.x_mouseover !== null) {
+ if (args.time_series) {
+ if (args.aggregate_rollover) {
+ formatted_x = time_rollover_format(args.x_mouseover, d, 'key', args.utc);
+ } else {
+ formatted_x = time_rollover_format(args.x_mouseover, d, args.x_accessor, args.utc);
+ }
+ } else {
+ formatted_x = number_rollover_format(args.x_mouseover, d, args.x_accessor);
+ }
+ } else {
+ if (args.time_series) {
+ var date;
+
+ if (args.aggregate_rollover && args.data.length > 1) {
+ date = new Date(d.key);
+ } else {
+ date = new Date(+d[args.x_accessor]);
+ date.setDate(date.getDate());
+ }
+
+ formatted_x = fmt(date) + ' ';
+ } else {
+ formatted_x = args.x_accessor + ': ' + d[args.x_accessor] + ' ';
+ }
+ }
+ return formatted_x;
+}
+
+function mg_format_data_for_mouseover(args, d, mouseover_fcn, accessor, check_time) {
+ var formatted_data, formatter;
+ var time_fmt = mg_get_rollover_time_format(args);
+ if (typeof d[accessor] === 'string') {
+ formatter = function(d) {
+ return d;
+ };
+ } else {
+ formatter = format_rollover_number(args);
+ }
+
+ if (mouseover_fcn !== null) {
+ if (check_time) formatted_data = time_rollover_format(mouseover_fcn, d, accessor, args.utc);
+ else formatted_data = number_rollover_format(mouseover_fcn, d, accessor);
+
+ } else {
+ if (check_time) formatted_data = time_fmt(new Date(+d[accessor])) + ' ';
+ else formatted_data = (args.time_series ? '' : accessor + ': ') + formatter(d[accessor]) + ' ';
+ }
+ return formatted_data;
+}
+
+function mg_format_number_mouseover(args, d) {
+ return mg_format_data_for_mouseover(args, d, args.x_mouseover, args.x_accessor, false);
+}
+
+function mg_format_x_mouseover(args, d) {
+ return mg_format_data_for_mouseover(args, d, args.x_mouseover, args.x_accessor, args.time_series);
+}
+
+function mg_format_y_mouseover(args, d) {
+ return mg_format_data_for_mouseover(args, d, args.y_mouseover, args.y_accessor, false);
+}
+
+function mg_format_x_aggregate_mouseover(args, d) {
+ return mg_format_data_for_mouseover(args, d, args.x_mouseover, 'key', args.time_series);
+}
+
+MG.format_rollover_number = format_rollover_number;
diff --git a/priv/static/js/metricsgraphics/misc/markup.js b/priv/static/js/metricsgraphics/misc/markup.js
new file mode 100644
index 0000000..c49f12a
--- /dev/null
+++ b/priv/static/js/metricsgraphics/misc/markup.js
@@ -0,0 +1,66 @@
+// influenced by https://bl.ocks.org/tomgp/c99a699587b5c5465228
+
+function render_markup_for_server(callback) {
+ var virtual_window = MG.virtual_window;
+ var virtual_d3 = d3.select(virtual_window.document);
+ var target = virtual_window.document.createElement('div');
+
+ var original_d3 = global.d3;
+ var original_window = global.window;
+ var original_document = global.document;
+ global.d3 = virtual_d3;
+ global.window = virtual_window;
+ global.document = virtual_window.document;
+
+ var error;
+ try {
+ callback(target);
+ } catch(e) {
+ error = e;
+ }
+
+ global.d3 = original_d3;
+ global.window = original_window;
+ global.document = original_document;
+
+ if (error) {
+ throw error;
+ }
+
+ /* for some reason d3.select parses jsdom elements incorrectly
+ * but it works if we wrap the element in a function.
+ */
+ return virtual_d3.select(function targetFn() {
+ return target;
+ }).html();
+}
+
+function render_markup_for_client(callback) {
+ var target = document.createElement('div');
+ callback(target);
+ return d3.select(target).html();
+}
+
+function render_markup(callback) {
+ switch(typeof window) {
+ case 'undefined':
+ return render_markup_for_server(callback);
+ default:
+ return render_markup_for_client(callback);
+ }
+}
+
+function init_virtual_window(jsdom, force) {
+ if (MG.virtual_window && !force) {
+ return;
+ }
+
+ var doc = jsdom.jsdom({
+ html: '',
+ features: { QuerySelector: true }
+ });
+ MG.virtual_window = doc.defaultView;
+}
+
+MG.render_markup = render_markup;
+MG.init_virtual_window = init_virtual_window;
diff --git a/priv/static/js/metricsgraphics/misc/process.js b/priv/static/js/metricsgraphics/misc/process.js
new file mode 100644
index 0000000..d0d312d
--- /dev/null
+++ b/priv/static/js/metricsgraphics/misc/process.js
@@ -0,0 +1,368 @@
+function mg_process_scale_ticks(args, axis) {
+ var accessor;
+ var scale_ticks;
+ var max;
+
+ if (axis === 'x') {
+ accessor = args.x_accessor;
+ scale_ticks = args.scales.X.ticks(args.xax_count);
+ max = args.processed.max_x;
+ } else if (axis === 'y') {
+ accessor = args.y_accessor;
+ scale_ticks = args.scales.Y.ticks(args.yax_count);
+ max = args.processed.max_y;
+ }
+
+ function log10(val) {
+ if (val === 1000) {
+ return 3;
+ }
+ if (val === 1000000) {
+ return 7;
+ }
+ return Math.log(val) / Math.LN10;
+ }
+
+ if ((axis === 'x' && args.x_scale_type === 'log') || (axis === 'y' && args.y_scale_type === 'log')) {
+ // get out only whole logs
+ scale_ticks = scale_ticks.filter(function(d) {
+ return Math.abs(log10(d)) % 1 < 1e-6 || Math.abs(log10(d)) % 1 > 1 - 1e-6;
+ });
+ }
+
+ // filter out fraction ticks if our data is ints and if xmax > number of generated ticks
+ var number_of_ticks = scale_ticks.length;
+
+ // is our data object all ints?
+ var data_is_int = true;
+ args.data.forEach(function(d, i) {
+ d.forEach(function(d, i) {
+ if (d[accessor] % 1 !== 0) {
+ data_is_int = false;
+ return false;
+ }
+ });
+ });
+
+ if (data_is_int && number_of_ticks > max && args.format === 'count') {
+ // remove non-integer ticks
+ scale_ticks = scale_ticks.filter(function(d) {
+ return d % 1 === 0;
+ });
+ }
+
+ if (axis === 'x') {
+ args.processed.x_ticks = scale_ticks;
+ } else if (axis === 'y') {
+ args.processed.y_ticks = scale_ticks;
+ }
+}
+
+function raw_data_transformation(args) {
+ 'use strict';
+
+ // dupe our data so we can modify it without adverse effect
+ args.data = MG.clone(args.data);
+
+ // we need to account for a few data format cases:
+ // #0 {bar1:___, bar2:___} // single object (for, say, bar charts)
+ // #1 [{key:__, value:__}, ...] // unnested obj-arrays
+ // #2 [[{key:__, value:__}, ...], [{key:__, value:__}, ...]] // nested obj-arrays
+ // #3 [[4323, 2343],..] // unnested 2d array
+ // #4 [[[4323, 2343],..] , [[4323, 2343],..]] // nested 2d array
+ args.single_object = false; // for bar charts.
+ args.array_of_objects = false;
+ args.array_of_arrays = false;
+ args.nested_array_of_arrays = false;
+ args.nested_array_of_objects = false;
+
+ // is the data object a nested array?
+
+ if (is_array_of_arrays(args.data)) {
+ args.nested_array_of_objects = args.data.map(function(d) {
+ return is_array_of_objects_or_empty(d);
+ }); // Case #2
+ args.nested_array_of_arrays = args.data.map(function(d) {
+ return is_array_of_arrays(d);
+ }); // Case #4
+ } else {
+ args.array_of_objects = is_array_of_objects(args.data); // Case #1
+ args.array_of_arrays = is_array_of_arrays(args.data); // Case #3
+ }
+
+ if (args.chart_type === 'line') {
+ if (args.array_of_objects || args.array_of_arrays) {
+ args.data = [args.data];
+ }
+ } else {
+ if (!(mg_is_array(args.data[0]))) {
+ args.data = [args.data];
+ }
+ }
+ // if the y_accessor is an array, break it up and store the result in args.data
+ mg_process_multiple_x_accessors(args);
+ mg_process_multiple_y_accessors(args);
+
+ // if user supplies keyword in args.color, change to arg.colors.
+ // this is so that the API remains fairly sensible and legible.
+ if (args.color !== undefined) {
+ args.colors = args.color;
+ }
+
+ // if user has supplied args.colors, and that value is a string, turn it into an array.
+ if (args.colors !== null && typeof args.colors === 'string') {
+ args.colors = [args.colors];
+ }
+
+ // sort x-axis data
+ if (args.chart_type === 'line' && args.x_sort === true) {
+ for (var i = 0; i < args.data.length; i++) {
+ args.data[i].sort(function(a, b) {
+ return a[args.x_accessor] - b[args.x_accessor];
+ });
+ }
+ }
+
+ return this;
+}
+
+function mg_process_multiple_accessors(args, which_accessor) {
+ // turns an array of accessors into ...
+ if (mg_is_array(args[which_accessor])) {
+ args.data = args.data.map(function(_d) {
+ return args[which_accessor].map(function(ya) {
+ return _d.map(function(di) {
+ di = MG.clone(di);
+
+ if (di[ya] === undefined) {
+ return undefined;
+ }
+
+ di['multiline_' + which_accessor] = di[ya];
+ return di;
+ }).filter(function(di) {
+ return di !== undefined;
+ });
+ });
+ })[0];
+ args[which_accessor] = 'multiline_' + which_accessor;
+ }
+}
+
+function mg_process_multiple_x_accessors(args) {
+ mg_process_multiple_accessors(args, 'x_accessor');
+}
+
+function mg_process_multiple_y_accessors(args) {
+ mg_process_multiple_accessors(args, 'y_accessor');
+}
+
+MG.raw_data_transformation = raw_data_transformation;
+
+function process_line(args) {
+ 'use strict';
+
+ var time_frame;
+
+ // do we have a time-series?
+ var is_time_series = d3.sum(args.data.map(function(series) {
+ return series.length > 0 && mg_is_date(series[0][args.x_accessor]);
+ })) > 0;
+
+ // are we replacing missing y values with zeros?
+ if ((args.missing_is_zero || args.missing_is_hidden) && args.chart_type === 'line' && is_time_series) {
+ for (var i = 0; i < args.data.length; i++) {
+ // we need to have a dataset of length > 2, so if it's less than that, skip
+ if (args.data[i].length <= 1) {
+ continue;
+ }
+
+ var first = args.data[i][0];
+ var last = args.data[i][args.data[i].length - 1];
+
+ // initialize our new array for storing the processed data
+ var processed_data = [];
+
+ // we'll be starting from the day after our first date
+ var start_date = MG.clone(first[args.x_accessor]).setDate(first[args.x_accessor].getDate() + 1);
+
+ // if we've set a max_x, add data points up to there
+ var from = (args.min_x) ? args.min_x : start_date;
+ var upto = (args.max_x) ? args.max_x : last[args.x_accessor];
+
+ time_frame = mg_get_time_frame((upto - from) / 1000);
+
+ if (['four-days', 'many-days', 'many-months', 'years', 'default'].indexOf(time_frame) !== -1 && args.missing_is_hidden_accessor === null) {
+ for (var d = new Date(from); d <= upto; d.setDate(d.getDate() + 1)) {
+ var o = {};
+ d.setHours(0, 0, 0, 0);
+
+ // add the first date item, we'll be starting from the day after our first date
+ if (Date.parse(d) === Date.parse(new Date(start_date))) {
+ processed_data.push(MG.clone(args.data[i][0]));
+ }
+
+ // check to see if we already have this date in our data object
+ var existing_o = null;
+ args.data[i].forEach(function(val, i) {
+ if (Date.parse(val[args.x_accessor]) === Date.parse(new Date(d))) {
+ existing_o = val;
+
+ return false;
+ }
+ });
+
+ // if we don't have this date in our data object, add it and set it to zero
+ if (!existing_o) {
+ o[args.x_accessor] = new Date(d);
+ o[args.y_accessor] = 0;
+ o['_missing'] = true; //we want to distinguish between zero-value and missing observations
+ processed_data.push(o);
+ }
+
+ // if the data point has, say, a 'missing' attribute set or if its
+ // y-value is null identify it internally as missing
+ else if (existing_o[args.missing_is_hidden_accessor] || existing_o[args.y_accessor] === null) {
+ existing_o['_missing'] = true;
+ processed_data.push(existing_o);
+ }
+
+ //otherwise, use the existing object for that date
+ else {
+ processed_data.push(existing_o);
+ }
+ }
+ } else {
+ for (var j = 0; j < args.data[i].length; j += 1) {
+ var obj = MG.clone(args.data[i][j]);
+ obj['_missing'] = args.data[i][j][args.missing_is_hidden_accessor];
+ processed_data.push(obj);
+ }
+ }
+
+ // update our date object
+ args.data[i] = processed_data;
+ }
+ }
+
+ return this;
+}
+
+MG.process_line = process_line;
+
+function process_histogram(args) {
+ 'use strict';
+
+ // if args.binned == false, then we need to bin the data appropriately.
+ // if args.binned == true, then we need to make sure to compute the relevant computed data.
+ // the outcome of either of these should be something in args.computed_data.
+ // the histogram plotting function will be looking there for the data to plot.
+
+ // we need to compute an array of objects.
+ // each object has an x, y, and dx.
+
+ // histogram data is always single dimension
+ var our_data = args.data[0];
+
+ var extracted_data;
+ if (args.binned === false) {
+ // use d3's built-in layout.histogram functionality to compute what you need.
+
+ if (typeof(our_data[0]) === 'object') {
+ // we are dealing with an array of objects. Extract the data value of interest.
+ extracted_data = our_data
+ .map(function(d) {
+ return d[args.x_accessor];
+ });
+ } else if (typeof(our_data[0]) === 'number') {
+ // we are dealing with a simple array of numbers. No extraction needed.
+ extracted_data = our_data;
+ } else {
+ console.log('TypeError: expected an array of numbers, found ' + typeof(our_data[0]));
+ return;
+ }
+
+ var hist = d3.histogram();
+ if (args.bins) {
+ hist.thresholds(args.bins);
+ }
+
+ var bins = hist(extracted_data);
+ args.processed_data = bins.map(function(d) {
+ return { 'x': d.x0, 'y': d.length };
+ });
+ } else {
+ // here, we just need to reconstruct the array of objects
+ // take the x accessor and y accessor.
+ // pull the data as x and y. y is count.
+
+ args.processed_data = our_data.map(function(d) {
+ return { 'x': d[args.x_accessor], 'y': d[args.y_accessor] };
+ });
+
+ var this_pt;
+ var next_pt;
+
+ // we still need to compute the dx component for each data point
+ for (var i = 0; i < args.processed_data.length; i++) {
+ this_pt = args.processed_data[i];
+ if (i === args.processed_data.length - 1) {
+ this_pt.dx = args.processed_data[i - 1].dx;
+ } else {
+ next_pt = args.processed_data[i + 1];
+ this_pt.dx = next_pt.x - this_pt.x;
+ }
+ }
+ }
+
+ // capture the original data and accessors before replacing args.data
+ if (!args.processed) {
+ args.processed = {};
+ }
+ args.processed.original_data = args.data;
+ args.processed.original_x_accessor = args.x_accessor;
+ args.processed.original_y_accessor = args.y_accessor;
+
+ args.data = [args.processed_data];
+ args.x_accessor = args.processed_x_accessor;
+ args.y_accessor = args.processed_y_accessor;
+
+ return this;
+}
+
+MG.process_histogram = process_histogram;
+
+// for use with bar charts, etc.
+function process_categorical_variables(args) {
+ 'use strict';
+
+ var extracted_data, processed_data = {},
+ pd = [];
+ //var our_data = args.data[0];
+ var label_accessor = args.bar_orientation === 'vertical' ? args.x_accessor : args.y_accessor;
+ var data_accessor = args.bar_orientation === 'vertical' ? args.y_accessor : args.x_accessor;
+
+ return this;
+}
+
+MG.process_categorical_variables = process_categorical_variables;
+
+function process_point(args) {
+ 'use strict';
+
+ var data = args.data[0];
+ var x = data.map(function(d) {
+ return d[args.x_accessor];
+ });
+ var y = data.map(function(d) {
+ return d[args.y_accessor];
+ });
+
+ if (args.least_squares) {
+ args.ls_line = least_squares(x, y);
+ }
+
+ return this;
+}
+
+MG.process_point = process_point;
diff --git a/priv/static/js/metricsgraphics/misc/smoothers.js b/priv/static/js/metricsgraphics/misc/smoothers.js
new file mode 100644
index 0000000..aab8a00
--- /dev/null
+++ b/priv/static/js/metricsgraphics/misc/smoothers.js
@@ -0,0 +1,280 @@
+function add_ls(args) {
+ var svg = mg_get_svg_child_of(args.target);
+ var data = args.data[0];
+ var min_x = d3.min(data, function(d) {
+ return d[args.x_accessor]; });
+ var max_x = d3.max(data, function(d) {
+ return d[args.x_accessor]; });
+
+ d3.select(args.target).selectAll('.mg-least-squares-line').remove();
+
+ svg.append('svg:line')
+ .attr('x1', args.scales.X(min_x))
+ .attr('x2', args.scales.X(max_x))
+ .attr('y1', args.scales.Y(args.ls_line.fit(min_x)))
+ .attr('y2', args.scales.Y(args.ls_line.fit(max_x)))
+ .attr('class', 'mg-least-squares-line');
+}
+
+MG.add_ls = add_ls;
+
+function add_lowess(args) {
+ var svg = mg_get_svg_child_of(args.target);
+ var lowess = args.lowess_line;
+
+ var line = d3.svg.line()
+ .x(function(d) {
+ return args.scales.X(d.x); })
+ .y(function(d) {
+ return args.scales.Y(d.y); })
+ .interpolate(args.interpolate);
+
+ svg.append('path')
+ .attr('d', line(lowess))
+ .attr('class', 'mg-lowess-line');
+}
+
+MG.add_lowess = add_lowess;
+
+function lowess_robust(x, y, alpha, inc) {
+ // Used http://www.unc.edu/courses/2007spring/biol/145/001/docs/lectures/Oct27.html
+ // for the clear explanation of robust lowess.
+
+ // calculate the the first pass.
+ var _l;
+ var r = [];
+ var yhat = d3.mean(y);
+ var i;
+ for (i = 0; i < x.length; i += 1) { r.push(1); }
+ _l = _calculate_lowess_fit(x, y, alpha, inc, r);
+ var x_proto = _l.x;
+ var y_proto = _l.y;
+
+ // Now, take the fit, recalculate the weights, and re-run LOWESS using r*w instead of w.
+
+ for (i = 0; i < 100; i += 1) {
+ r = d3.zip(y_proto, y).map(function(yi) {
+ return Math.abs(yi[1] - yi[0]);
+ });
+
+ var q = d3.quantile(r.sort(), 0.5);
+
+ r = r.map(function(ri) {
+ return _bisquare_weight(ri / (6 * q));
+ });
+
+ _l = _calculate_lowess_fit(x, y, alpha, inc, r);
+ x_proto = _l.x;
+ y_proto = _l.y;
+ }
+
+ return d3.zip(x_proto, y_proto).map(function(d) {
+ var p = {};
+ p.x = d[0];
+ p.y = d[1];
+ return p;
+ });
+}
+
+MG.lowess_robust = lowess_robust;
+
+function lowess(x, y, alpha, inc) {
+ var r = [];
+ for (var i = 0; i < x.length; i += 1) { r.push(1); }
+ var _l = _calculate_lowess_fit(x, y, alpha, inc, r);
+}
+
+MG.lowess = lowess;
+
+function least_squares(x_, y_) {
+ var x, y, xi, yi,
+ _x = 0,
+ _y = 0,
+ _xy = 0,
+ _xx = 0;
+
+ var n = x_.length;
+ if (mg_is_date(x_[0])) {
+ x = x_.map(function(d) {
+ return d.getTime();
+ });
+ } else {
+ x = x_;
+ }
+
+ if (mg_is_date(y_[0])) {
+ y = y_.map(function(d) {
+ return d.getTime();
+ });
+ } else {
+ y = y_;
+ }
+
+ var xhat = d3.mean(x);
+ var yhat = d3.mean(y);
+ var numerator = 0,
+ denominator = 0;
+
+ for (var i = 0; i < x.length; i++) {
+ xi = x[i];
+ yi = y[i];
+ numerator += (xi - xhat) * (yi - yhat);
+ denominator += (xi - xhat) * (xi - xhat);
+ }
+
+ var beta = numerator / denominator;
+ var x0 = yhat - beta * xhat;
+
+ return {
+ x0: x0,
+ beta: beta,
+ fit: function(x) {
+ return x0 + x * beta;
+ }
+ };
+}
+
+MG.least_squares = least_squares;
+
+function _pow_weight(u, w) {
+ if (u >= 0 && u <= 1) {
+ return Math.pow(1 - Math.pow(u, w), w);
+ } else {
+ return 0;
+ }
+}
+
+function _bisquare_weight(u) {
+ return _pow_weight(u, 2);
+}
+
+function _tricube_weight(u) {
+ return _pow_weight(u, 3);
+}
+
+function _neighborhood_width(x0, xis) {
+ return Array.max(xis.map(function(xi) {
+ return Math.abs(x0 - xi);
+ }));
+}
+
+function _manhattan(x1, x2) {
+ return Math.abs(x1 - x2);
+}
+
+function _weighted_means(wxy) {
+ var wsum = d3.sum(wxy.map(function(wxyi) {
+ return wxyi.w; }));
+
+ return {
+ xbar: d3.sum(wxy.map(function(wxyi) {
+ return wxyi.w * wxyi.x;
+ })) / wsum,
+ ybar: d3.sum(wxy.map(function(wxyi) {
+ return wxyi.w * wxyi.y;
+ })) / wsum
+ };
+}
+
+function _weighted_beta(wxy, xbar, ybar) {
+ var num = d3.sum(wxy.map(function(wxyi) {
+ return Math.pow(wxyi.w, 2) * (wxyi.x - xbar) * (wxyi.y - ybar);
+ }));
+
+ var denom = d3.sum(wxy.map(function(wxyi) {
+ return Math.pow(wxyi.w, 2) * Math.pow(wxyi.x - xbar, 2);
+ }));
+
+ return num / denom;
+}
+
+function _weighted_least_squares(wxy) {
+ var ybar, xbar, beta_i, x0;
+
+ var _wm = _weighted_means(wxy);
+
+ xbar = _wm.xbar;
+ ybar = _wm.ybar;
+
+ var beta = _weighted_beta(wxy, xbar, ybar);
+
+ return {
+ beta: beta,
+ xbar: xbar,
+ ybar: ybar,
+ x0: ybar - beta * xbar
+
+ };
+}
+
+function _calculate_lowess_fit(x, y, alpha, inc, residuals) {
+ // alpha - smoothing factor. 0 < alpha < 1/
+ //
+ //
+ var k = Math.floor(x.length * alpha);
+
+ var sorted_x = x.slice();
+
+ sorted_x.sort(function(a, b) {
+ if (a < b) {
+ return -1; } else if (a > b) {
+ return 1; }
+
+ return 0;
+ });
+
+ var x_max = d3.quantile(sorted_x, 0.98);
+ var x_min = d3.quantile(sorted_x, 0.02);
+
+ var xy = d3.zip(x, y, residuals).sort();
+
+ var size = Math.abs(x_max - x_min) / inc;
+
+ var smallest = x_min;
+ var largest = x_max;
+ var x_proto = d3.range(smallest, largest, size);
+
+ var xi_neighbors;
+ var x_i, beta_i, x0_i, delta_i, xbar, ybar;
+
+ // for each prototype, find its fit.
+ var y_proto = [];
+
+ for (var i = 0; i < x_proto.length; i += 1) {
+ x_i = x_proto[i];
+
+ // get k closest neighbors.
+ xi_neighbors = xy.map(function(xyi) {
+ return [
+ Math.abs(xyi[0] - x_i),
+ xyi[0],
+ xyi[1],
+ xyi[2]
+ ];
+ }).sort().slice(0, k);
+
+ // Get the largest distance in the neighbor set.
+ delta_i = d3.max(xi_neighbors)[0];
+
+ // Prepare the weights for mean calculation and WLS.
+
+ xi_neighbors = xi_neighbors.map(function(wxy) {
+ return {
+ w: _tricube_weight(wxy[0] / delta_i) * wxy[3],
+ x: wxy[1],
+ y: wxy[2]
+ };
+ });
+
+ // Find the weighted least squares, obviously.
+ var _output = _weighted_least_squares(xi_neighbors);
+
+ x0_i = _output.x0;
+ beta_i = _output.beta;
+
+ //
+ y_proto.push(x0_i + beta_i * x_i);
+ }
+
+ return { x: x_proto, y: y_proto };
+}
diff --git a/priv/static/js/metricsgraphics/misc/transitions.js b/priv/static/js/metricsgraphics/misc/transitions.js
new file mode 100644
index 0000000..da1e503
--- /dev/null
+++ b/priv/static/js/metricsgraphics/misc/transitions.js
@@ -0,0 +1,31 @@
+// http://bl.ocks.org/mbostock/3916621
+function path_tween(d1, precision) {
+ return function() {
+ var path0 = this,
+ path1 = path0.cloneNode(),
+ n0 = path0.getTotalLength() || 0,
+ n1 = (path1.setAttribute("d", d1), path1).getTotalLength() || 0;
+
+ // Uniform sampling of distance based on specified precision.
+ var distances = [0],
+ i = 0,
+ dt = precision / Math.max(n0, n1);
+ while ((i += dt) < 1) distances.push(i);
+ distances.push(1);
+
+ // Compute point-interpolators at each distance.
+ var points = distances.map(function(t) {
+ var p0 = path0.getPointAtLength(t * n0),
+ p1 = path1.getPointAtLength(t * n1);
+ return d3.interpolate([p0.x, p0.y], [p1.x, p1.y]);
+ });
+
+ return function(t) {
+ return t < 1 ? "M" + points.map(function(p) {
+ return p(t);
+ }).join("L") : d1;
+ };
+ };
+}
+
+MG.path_tween = path_tween;
diff --git a/priv/static/js/metricsgraphics/misc/utility.js b/priv/static/js/metricsgraphics/misc/utility.js
new file mode 100644
index 0000000..f68327b
--- /dev/null
+++ b/priv/static/js/metricsgraphics/misc/utility.js
@@ -0,0 +1,619 @@
+//a set of helper functions, some that we've written, others that we've borrowed
+
+MG.convert = {};
+
+MG.convert.date = function(data, accessor, time_format) {
+ time_format = (typeof time_format === "undefined") ? '%Y-%m-%d' : time_format;
+ var parse_time = d3.timeParse(time_format);
+ data = data.map(function(d) {
+ d[accessor] = parse_time(d[accessor].trim());
+ return d;
+ });
+
+ return data;
+};
+
+MG.convert.number = function(data, accessor) {
+ data = data.map(function(d) {
+ d[accessor] = Number(d[accessor]);
+ return d;
+ });
+
+ return data;
+};
+
+MG.time_format = function(utc, specifier) {
+ return utc ? d3.utcFormat(specifier) : d3.timeFormat(specifier);
+};
+
+function mg_jquery_exists() {
+ if (typeof jQuery !== 'undefined' || typeof $ !== 'undefined') {
+ return true;
+ } else {
+ return false;
+ }
+}
+
+function mg_get_rollover_time_format(args) {
+ // if a rollover time format is defined, use that
+ if (args.rollover_time_format) {
+ return MG.time_format(args.utc_time, args.rollover_time_format);
+ }
+
+ switch (args.processed.x_time_frame) {
+ case 'millis':
+ return MG.time_format(args.utc_time, '%b %e, %Y %H:%M:%S.%L');
+ case 'seconds':
+ return MG.time_format(args.utc_time, '%b %e, %Y %H:%M:%S');
+ case 'less-than-a-day':
+ return MG.time_format(args.utc_time, '%b %e, %Y %I:%M%p');
+ case 'four-days':
+ return MG.time_format(args.utc_time, '%b %e, %Y %I:%M%p');
+ }
+
+ // default
+ return MG.time_format(args.utc_time, '%b %e, %Y');
+}
+
+function mg_data_in_plot_bounds(datum, args) {
+ return datum[args.x_accessor] >= args.processed.min_x &&
+ datum[args.x_accessor] <= args.processed.max_x &&
+ datum[args.y_accessor] >= args.processed.min_y &&
+ datum[args.y_accessor] <= args.processed.max_y;
+}
+
+function is_array(thing) {
+ return Object.prototype.toString.call(thing) === '[object Array]';
+}
+
+function is_function(thing) {
+ return Object.prototype.toString.call(thing) === '[object Function]';
+}
+
+function is_empty_array(thing) {
+ return is_array(thing) && thing.length === 0;
+}
+
+function is_object(thing) {
+ return Object.prototype.toString.call(thing) === '[object Object]';
+}
+
+function is_array_of_arrays(data) {
+ var all_elements = data.map(function(d) {
+ return is_array(d) === true && d.length > 0;
+ });
+
+ return d3.sum(all_elements) === data.length;
+}
+
+function is_array_of_objects(data) {
+ // is every element of data an object?
+ var all_elements = data.map(function(d) {
+ return is_object(d) === true;
+ });
+
+ return d3.sum(all_elements) === data.length;
+}
+
+function is_array_of_objects_or_empty(data) {
+ return is_empty_array(data) || is_array_of_objects(data);
+}
+
+function pluck(arr, accessor) {
+ return arr.map(function(d) {
+ return d[accessor]; });
+}
+
+function count_array_elements(arr) {
+ return arr.reduce(function(a, b) { a[b] = a[b] + 1 || 1;
+ return a; }, {});
+}
+
+function mg_get_bottom(args) {
+ return args.height - args.bottom;
+}
+
+function mg_get_plot_bottom(args) {
+ // returns the pixel location of the bottom side of the plot area.
+ return mg_get_bottom(args) - args.buffer;
+}
+
+function mg_get_top(args) {
+ return args.top;
+}
+
+function mg_get_plot_top(args) {
+ // returns the pixel location of the top side of the plot area.
+ return mg_get_top(args) + args.buffer;
+}
+
+function mg_get_left(args) {
+ return args.left;
+}
+
+function mg_get_plot_left(args) {
+ // returns the pixel location of the left side of the plot area.
+ return mg_get_left(args) + args.buffer;
+}
+
+function mg_get_right(args) {
+ return args.width - args.right;
+}
+
+function mg_get_plot_right(args) {
+ // returns the pixel location of the right side of the plot area.
+ return mg_get_right(args) - args.buffer;
+}
+
+//////// adding elements, removing elements /////////////
+
+function mg_exit_and_remove(elem) {
+ elem.exit().remove();
+}
+
+function mg_selectAll_and_remove(svg, cl) {
+ svg.selectAll(cl).remove();
+}
+
+function mg_add_g(svg, cl) {
+ return svg.append('g').classed(cl, true);
+}
+
+function mg_remove_element(svg, elem) {
+ svg.select(elem).remove();
+}
+
+//////// axis helper functions ////////////
+
+function mg_make_rug(args, rug_class) {
+ var svg = mg_get_svg_child_of(args.target);
+ var all_data = mg_flatten_array(args.data);
+ var rug = svg.selectAll('line.' + rug_class).data(all_data);
+
+ rug.enter()
+ .append('line')
+ .attr('class', rug_class)
+ .attr('opacity', 0.3);
+
+ //remove rug elements that are no longer in use
+ mg_exit_and_remove(rug);
+
+ //set coordinates of new rug elements
+ mg_exit_and_remove(rug);
+ return rug;
+}
+
+function mg_add_color_accessor_to_rug(rug, args, rug_mono_class) {
+ if (args.color_accessor) {
+ rug.attr('stroke', args.scalefns.colorf);
+ rug.classed(rug_mono_class, false);
+ } else {
+ rug.attr('stroke', null);
+ rug.classed(rug_mono_class, true);
+ }
+}
+
+function mg_rotate_labels(labels, rotation_degree) {
+ if (rotation_degree) {
+ labels.attr({
+ dy: 0,
+ transform: function() {
+ var elem = d3.select(this);
+ return 'rotate(' + rotation_degree + ' ' + elem.attr('x') + ',' + elem.attr('y') + ')';
+ }
+ });
+ }
+}
+
+//////////////////////////////////////////////////
+
+function mg_elements_are_overlapping(labels) {
+ labels = labels.node();
+ if (!labels) {
+ return false;
+ }
+
+ for (var i = 0; i < labels.length; i++) {
+ if (mg_is_horizontally_overlapping(labels[i], labels)) return true;
+ }
+
+ return false;
+}
+
+function mg_prevent_horizontal_overlap(labels, args) {
+ if (!labels || labels.length == 1) {
+ return;
+ }
+
+ //see if each of our labels overlaps any of the other labels
+ for (var i = 0; i < labels.length; i++) {
+ //if so, nudge it up a bit, if the label it intersects hasn't already been nudged
+ if (mg_is_horizontally_overlapping(labels[i], labels)) {
+ var node = d3.select(labels[i]);
+ var newY = +node.attr('y');
+ if (newY + 8 >= args.top) {
+ newY = args.top - 16;
+ }
+ node.attr('y', newY);
+ }
+ }
+}
+
+function mg_prevent_vertical_overlap(labels, args) {
+ if (!labels || labels.length == 1) {
+ return;
+ }
+
+ labels.sort(function(b, a) {
+ return d3.select(a).attr('y') - d3.select(b).attr('y');
+ });
+
+ labels.reverse();
+
+ var overlap_amount, label_i, label_j;
+
+ //see if each of our labels overlaps any of the other labels
+ for (var i = 0; i < labels.length; i++) {
+ //if so, nudge it up a bit, if the label it intersects hasn't already been nudged
+ label_i = d3.select(labels[i]).text();
+
+ for (var j = 0; j < labels.length; j++) {
+ label_j = d3.select(labels[j]).text();
+ overlap_amount = mg_is_vertically_overlapping(labels[i], labels[j]);
+
+ if (overlap_amount !== false && label_i !== label_j) {
+ var node = d3.select(labels[i]);
+ var newY = +node.attr('y');
+ newY = newY + overlap_amount;
+ node.attr('y', newY);
+ }
+ }
+ }
+}
+
+function mg_is_vertically_overlapping(element, sibling) {
+ var element_bbox = element.getBoundingClientRect();
+ var sibling_bbox = sibling.getBoundingClientRect();
+
+ if (element_bbox.top <= sibling_bbox.bottom && element_bbox.top >= sibling_bbox.top) {
+ return sibling_bbox.bottom - element_bbox.top;
+ }
+
+ return false;
+}
+
+function mg_is_horiz_overlap(element, sibling) {
+ var element_bbox = element.getBoundingClientRect();
+ var sibling_bbox = sibling.getBoundingClientRect();
+
+ if (element_bbox.right >= sibling_bbox.left || element_bbox.top >= sibling_bbox.top) {
+ return sibling_bbox.bottom - element_bbox.top;
+ }
+ return false;
+}
+
+function mg_is_horizontally_overlapping(element, labels) {
+ var element_bbox = element.getBoundingClientRect();
+
+ for (var i = 0; i < labels.length; i++) {
+ if (labels[i] == element) {
+ continue;
+ }
+
+ //check to see if this label overlaps with any of the other labels
+ var sibling_bbox = labels[i].getBoundingClientRect();
+ if (element_bbox.top === sibling_bbox.top &&
+ !(sibling_bbox.left > element_bbox.right || sibling_bbox.right < element_bbox.left)
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+function mg_infer_type(args, ns) {
+ // must return categorical or numerical.
+ var testPoint = mg_flatten_array(args.data);
+
+ testPoint = testPoint[0][args[ns + '_accessor']];
+ return typeof testPoint === 'string' ? 'categorical' : 'numerical';
+ }
+
+function mg_get_svg_child_of(selector_or_node) {
+ return d3.select(selector_or_node).select('svg');
+}
+
+function mg_flatten_array(arr) {
+ var flat_data = [];
+ return flat_data.concat.apply(flat_data, arr);
+}
+
+function mg_next_id() {
+ if (typeof MG._next_elem_id === 'undefined') {
+ MG._next_elem_id = 0;
+ }
+
+ return 'mg-' + (MG._next_elem_id++);
+}
+
+function mg_target_ref(target) {
+ if (typeof target === 'string') {
+ return mg_normalize(target);
+
+ } else if (target instanceof window.HTMLElement) {
+ var target_ref = target.getAttribute('data-mg-uid');
+ if (!target_ref) {
+ target_ref = mg_next_id();
+ target.setAttribute('data-mg-uid', target_ref);
+ }
+
+ return target_ref;
+
+ } else {
+ console.warn('The specified target should be a string or an HTMLElement.', target);
+ return mg_normalize(target);
+ }
+}
+
+function mg_normalize(string) {
+ return string
+ .replace(/[^a-zA-Z0-9 _-]+/g, '')
+ .replace(/ +?/g, '');
+}
+
+function get_pixel_dimension(target, dimension) {
+ return Number(d3.select(target).style(dimension).replace(/px/g, ''));
+}
+
+function get_width(target) {
+ return get_pixel_dimension(target, 'width');
+}
+
+function get_height(target) {
+ return get_pixel_dimension(target, 'height');
+}
+
+function isNumeric(n) {
+ return !isNaN(parseFloat(n)) && isFinite(n);
+}
+
+var each = function(obj, iterator, context) {
+ // yanked out of underscore
+ var breaker = {};
+ if (obj === null) return obj;
+ if (Array.prototype.forEach && obj.forEach === Array.prototype.forEach) {
+ obj.forEach(iterator, context);
+ } else if (obj.length === +obj.length) {
+ for (var i = 0, length = obj.length; i < length; i++) {
+ if (iterator.call(context, obj[i], i, obj) === breaker) return;
+ }
+ } else {
+ for (var k in obj) {
+ if (iterator.call(context, obj[k], k, obj) === breaker) return;
+ }
+ }
+
+ return obj;
+};
+
+function merge_with_defaults(obj) {
+ // taken from underscore
+ each(Array.prototype.slice.call(arguments, 1), function(source) {
+ if (source) {
+ for (var prop in source) {
+ if (obj[prop] === void 0) obj[prop] = source[prop];
+ }
+ }
+ });
+
+ return obj;
+}
+
+MG.merge_with_defaults = merge_with_defaults;
+
+function options_to_defaults(obj) {
+ return Object.keys(obj).reduce((r, k) => {
+ r[k] = obj[k][0];
+ return r;
+ }, {});
+}
+
+function compare_type(type, value) {
+ if (value == null) return true; // allow null or undefined
+ if (typeof type === 'string') {
+ if (type.substr(-2) === '[]') {
+ if (!is_array(value)) return false;
+ return value.every(i => compare_type(type.slice(0, -2), i));
+ }
+ return typeof value === type
+ || value === type
+ || type.length === 0
+ || type === 'array' && is_array(value);
+ }
+ if (typeof type === 'function') return value === type || value instanceof type;
+ return is_array(type) && !!~type.findIndex(i => compare_type(i, value));
+}
+
+function mg_validate_option(key, value) {
+ if (!is_array(MG.options[key])) return false; // non-existent option
+ const typeDef = MG.options[key][1];
+ if (!typeDef) return true; // not restricted type
+ return compare_type(typeDef, value);
+}
+
+function number_of_values(data, accessor, value) {
+ var values = data.filter(function(d) {
+ return d[accessor] === value;
+ });
+
+ return values.length;
+}
+
+function has_values_below(data, accessor, value) {
+ var values = data.filter(function(d) {
+ return d[accessor] <= value;
+ });
+
+ return values.length > 0;
+}
+
+function has_too_many_zeros(data, accessor, zero_count) {
+ return number_of_values(data, accessor, 0) >= zero_count;
+}
+
+function mg_is_date(obj) {
+ return Object.prototype.toString.call(obj) === '[object Date]';
+}
+
+function mg_is_object(obj) {
+ return Object.prototype.toString.call(obj) === '[object Object]';
+}
+
+function mg_is_array(obj) {
+ if (Array.isArray) {
+ return Array.isArray(obj);
+ }
+
+ return Object.prototype.toString.call(obj) === '[object Array]';
+}
+
+function mg_is_function(obj) {
+ return Object.prototype.toString.call(obj) === '[object Function]';
+}
+
+// deep copy
+// http://stackoverflow.com/questions/728360/most-elegant-way-to-clone-a-javascript-object
+MG.clone = function(obj) {
+ var copy;
+
+ // Handle the 3 simple types, and null or undefined
+ if (null === obj || "object" !== typeof obj) return obj;
+
+ // Handle Date
+ if (mg_is_date(obj)) {
+ copy = new Date();
+ copy.setTime(obj.getTime());
+ return copy;
+ }
+
+ // Handle Array
+ if (mg_is_array(obj)) {
+ copy = [];
+ for (var i = 0, len = obj.length; i < len; i++) {
+ copy[i] = MG.clone(obj[i]);
+ }
+ return copy;
+ }
+
+ // Handle Object
+ if (mg_is_object(obj)) {
+ copy = {};
+ for (var attr in obj) {
+ if (obj.hasOwnProperty(attr)) copy[attr] = MG.clone(obj[attr]);
+ }
+ return copy;
+ }
+
+ throw new Error("Unable to copy obj! Its type isn't supported.");
+};
+
+// give us the difference of two int arrays
+// http://radu.cotescu.com/javascript-diff-function/
+function arr_diff(a, b) {
+ var seen = [],
+ diff = [],
+ i;
+ for (i = 0; i < b.length; i++)
+ seen[b[i]] = true;
+ for (i = 0; i < a.length; i++)
+ if (!seen[a[i]])
+ diff.push(a[i]);
+ return diff;
+}
+
+MG.arr_diff = arr_diff;
+
+/**
+ Print warning message to the console when a feature has been scheduled for removal
+
+ @author Dan de Havilland (github.com/dandehavilland)
+ @date 2014-12
+*/
+function warn_deprecation(message, untilVersion) {
+ console.warn('Deprecation: ' + message + (untilVersion ? '. This feature will be removed in ' + untilVersion + '.' : ' the near future.'));
+ console.trace();
+}
+
+MG.warn_deprecation = warn_deprecation;
+
+/**
+ Truncate a string to fit within an SVG text node
+ CSS text-overlow doesn't apply to SVG <= 1.2
+
+ @author Dan de Havilland (github.com/dandehavilland)
+ @date 2014-12-02
+*/
+function truncate_text(textObj, textString, width) {
+ var bbox,
+ position = 0;
+
+ textObj.textContent = textString;
+ bbox = textObj.getBBox();
+
+ while (bbox.width > width) {
+ textObj.textContent = textString.slice(0, --position) + '...';
+ bbox = textObj.getBBox();
+
+ if (textObj.textContent === '...') {
+ break;
+ }
+ }
+}
+
+MG.truncate_text = truncate_text;
+
+/**
+ Wrap the contents of a text node to a specific width
+
+ Adapted from bl.ocks.org/mbostock/7555321
+
+ @author Mike Bostock
+ @author Dan de Havilland
+ @date 2015-01-14
+*/
+function wrap_text(text, width, token, tspanAttrs) {
+ text.each(function() {
+ var text = d3.select(this),
+ words = text.text().split(token || /\s+/).reverse(),
+ word,
+ line = [],
+ lineNumber = 0,
+ lineHeight = 1.1, // ems
+ y = text.attr("y"),
+ dy = 0,
+ tspan = text.text(null)
+ .append("tspan")
+ .attr("x", 0)
+ .attr("y", dy + "em")
+ .attr(tspanAttrs || {});
+
+ while (!!(word = words.pop())) {
+ line.push(word);
+ tspan.text(line.join(" "));
+ if (width === null || tspan.node().getComputedTextLength() > width) {
+ line.pop();
+ tspan.text(line.join(" "));
+ line = [word];
+ tspan = text
+ .append("tspan")
+ .attr("x", 0)
+ .attr("y", ++lineNumber * lineHeight + dy + "em")
+ .attr(tspanAttrs || {})
+ .text(word);
+ }
+ }
+ });
+}
+
+MG.wrap_text = wrap_text;