diff options
author | href <href@random.sh> | 2021-09-01 10:30:18 +0200 |
---|---|---|
committer | href <href@random.sh> | 2021-09-01 10:30:18 +0200 |
commit | 75687711f35355bc30e4829439384aab28fcac6d (patch) | |
tree | 8f3256f472893c39720a684d390e890a152f7303 /priv/static/js/metricsgraphics/misc | |
parent | link: 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.js | 16 | ||||
-rw-r--r-- | priv/static/js/metricsgraphics/misc/formatters.js | 147 | ||||
-rw-r--r-- | priv/static/js/metricsgraphics/misc/markup.js | 66 | ||||
-rw-r--r-- | priv/static/js/metricsgraphics/misc/process.js | 368 | ||||
-rw-r--r-- | priv/static/js/metricsgraphics/misc/smoothers.js | 280 | ||||
-rw-r--r-- | priv/static/js/metricsgraphics/misc/transitions.js | 31 | ||||
-rw-r--r-- | priv/static/js/metricsgraphics/misc/utility.js | 619 |
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; |