summaryrefslogtreecommitdiff
path: root/priv/static/js/metricsgraphics/charts
diff options
context:
space:
mode:
Diffstat (limited to 'priv/static/js/metricsgraphics/charts')
-rw-r--r--priv/static/js/metricsgraphics/charts/bar.js792
-rw-r--r--priv/static/js/metricsgraphics/charts/histogram.js222
-rw-r--r--priv/static/js/metricsgraphics/charts/line.js922
-rw-r--r--priv/static/js/metricsgraphics/charts/missing.js144
-rw-r--r--priv/static/js/metricsgraphics/charts/point.js383
-rw-r--r--priv/static/js/metricsgraphics/charts/table.js220
6 files changed, 2683 insertions, 0 deletions
diff --git a/priv/static/js/metricsgraphics/charts/bar.js b/priv/static/js/metricsgraphics/charts/bar.js
new file mode 100644
index 0000000..b39d59f
--- /dev/null
+++ b/priv/static/js/metricsgraphics/charts/bar.js
@@ -0,0 +1,792 @@
+{
+ // TODO add styles to stylesheet instead
+ function scaffold({target, width, height, top, left, right, buffer}) {
+ const svg = mg_get_svg_child_of(target);
+ // main margins
+ svg.append('line')
+ .attr('x1', 0)
+ .attr('x2', width)
+ .attr('y1', top)
+ .attr('y2', top)
+ .attr('stroke', 'black');
+ svg.append('line')
+ .attr('x1', 0)
+ .attr('x2', width)
+ .attr('y1', height - bottom)
+ .attr('y2', height - bottom)
+ .attr('stroke', 'black');
+
+ svg.append('line')
+ .attr('x1', left)
+ .attr('x2', left)
+ .attr('y1', 0)
+ .attr('y2', height)
+ .attr('stroke', 'black');
+
+ svg.append('line')
+ .attr('x1', width - right)
+ .attr('x2', width - right)
+ .attr('y1', 0)
+ .attr('y2', height)
+ .attr('stroke', 'black');
+
+ // plot area margins
+ svg.append('line')
+ .attr('x1', 0)
+ .attr('x2', width)
+ .attr('y1', height - bottom - buffer)
+ .attr('y2', height - bottom - buffer)
+ .attr('stroke', 'gray');
+
+ svg.append('line')
+ .attr('x1', 0)
+ .attr('x2', width)
+ .attr('y1', top + buffer)
+ .attr('y2', top + buffer)
+ .attr('stroke', 'gray');
+
+ svg.append('line')
+ .attr('x1', left + buffer)
+ .attr('x2', left + buffer)
+ .attr('y1', 0)
+ .attr('y2', args.height)
+ .attr('stroke', 'gray');
+ svg.append('line')
+ .attr('x1', width - right - buffer)
+ .attr('x2', width - right - buffer)
+ .attr('y1', 0)
+ .attr('y2', height)
+ .attr('stroke', 'gray');
+ }
+
+ // barchart re-write.
+ function mg_targeted_legend({legend_target, orientation, scales}) {
+ let labels;
+ const plot = '';
+ if (legend_target) {
+
+ const div = d3.select(legend_target).append('div').classed('mg-bar-target-legend', true);
+
+ if (orientation == 'horizontal') labels = scales.Y.domain();
+ else labels = scales.X.domain();
+
+ labels.forEach(label => {
+ const outer_span = div.append('span').classed('mg-bar-target-element', true);
+ outer_span.append('span')
+ .classed('mg-bar-target-legend-shape', true)
+ .style('color', scales.COLOR(label))
+ .text('\u25FC ');
+ outer_span.append('span')
+ .classed('mg-bar-target-legend-text', true)
+ .text(label);
+ });
+ }
+ }
+
+ function legend_on_graph(svg, args) {
+ // draw each element at the top right
+ // get labels
+
+ let labels;
+ if (args.orientation=='horizontal') labels = args.scales.Y.domain();
+ else labels = args.scales.X.domain();
+
+ let lineCount = 0;
+ const lineHeight = 1.1;
+ const g = svg.append('g').classed("mg-bar-legend", true);
+ const textContainer = g.append('text');
+
+ textContainer
+ .selectAll('*')
+ .remove();
+ textContainer
+ .attr('width', args.right)
+ .attr('height', 100)
+ .attr('text-anchor', 'start');
+
+ labels.forEach(label => {
+ const sub_container = textContainer.append('tspan')
+ .attr('x', mg_get_plot_right(args))
+ .attr('y', args.height / 2)
+ .attr('dy', `${lineCount * lineHeight}em`);
+ sub_container.append('tspan')
+ .text('\u25a0 ')
+ .attr('fill', args.scales.COLOR(label))
+ .attr('font-size', 20);
+ sub_container.append('tspan')
+ .text(label)
+ .attr('font-weight', 300)
+ .attr('font-size', 10);
+ lineCount++;
+ });
+
+ // d.values.forEach(function (datum) {
+ // formatted_y = mg_format_y_rollover(args, num, datum);
+
+ // if (args.y_rollover_format !== null) {
+ // formatted_y = number_rollover_format(args.y_rollover_format, datum, args.y_accessor);
+ // } else {
+ // formatted_y = args.yax_units + num(datum[args.y_accessor]);
+ // }
+
+ // sub_container = textContainer.append('tspan').attr('x', 0).attr('y', (lineCount * lineHeight) + 'em');
+ // formatted_y = mg_format_y_rollover(args, num, datum);
+ // mouseover_tspan(sub_container, '\u2014 ')
+ // .color(args, datum);
+ // mouseover_tspan(sub_container, formatted_x + ' ' + formatted_y);
+
+ // lineCount++;
+ // });
+ }
+
+ function barChart(args) {
+ this.args = args;
+
+ this.init = (args) => {
+ this.args = args;
+ args.x_axis_type = mg_infer_type(args, 'x');
+ args.y_axis_type = mg_infer_type(args, 'y');
+
+ // this is specific to how rects work in svg, let's keep track of the bar orientation to
+ // plot appropriately.
+ if (args.x_axis_type == 'categorical') {
+ args.orientation = 'vertical';
+ } else if (args.y_axis_type == 'categorical') {
+ args.orientation = 'horizontal';
+ } else if (args.x_axis_type != 'categorical' && args.y_axis_type != 'categorical') {
+ // histogram.
+ args.orientation = 'vertical';
+ }
+
+ raw_data_transformation(args);
+
+ process_point(args);
+ init(args);
+
+ let xMaker;
+ let yMaker;
+
+ if (args.x_axis_type === 'categorical') {
+ xMaker = MG.scale_factory(args)
+ .namespace('x')
+ .categoricalDomainFromData()
+ .categoricalRangeBands([0, args.xgroup_height], args.xgroup_accessor === null);
+
+ if (args.xgroup_accessor) {
+ new MG.scale_factory(args)
+ .namespace('xgroup')
+ .categoricalDomainFromData()
+ .categoricalRangeBands('bottom');
+
+ } else {
+ args.scales.XGROUP = d => mg_get_plot_left(args);
+ args.scalefns.xgroupf = d => mg_get_plot_left(args);
+ }
+
+ args.scalefns.xoutf = d => args.scalefns.xf(d) + args.scalefns.xgroupf(d);
+ } else {
+ xMaker = MG.scale_factory(args)
+ .namespace('x')
+ .inflateDomain(true)
+ .zeroBottom(args.y_axis_type === 'categorical')
+ .numericalDomainFromData((args.baselines || []).map(d => d[args.x_accessor]))
+ .numericalRange('bottom');
+
+ args.scalefns.xoutf = args.scalefns.xf;
+ }
+
+ // y-scale generation. This needs to get simplified.
+ if (args.y_axis_type === 'categorical') {
+ yMaker = MG.scale_factory(args)
+ .namespace('y')
+ .zeroBottom(true)
+ .categoricalDomainFromData()
+ .categoricalRangeBands([0, args.ygroup_height], true);
+
+ if (args.ygroup_accessor) {
+
+ new MG.scale_factory(args)
+ .namespace('ygroup')
+ .categoricalDomainFromData()
+ .categoricalRangeBands('left');
+
+ } else {
+ args.scales.YGROUP = () => mg_get_plot_top(args);
+ args.scalefns.ygroupf = d => mg_get_plot_top(args);
+
+ }
+ args.scalefns.youtf = d => args.scalefns.yf(d) + args.scalefns.ygroupf(d);
+
+ } else {
+ const baselines = (args.baselines || []).map(d => d[args.y_accessor]);
+
+ yMaker = MG.scale_factory(args)
+ .namespace('y')
+ .inflateDomain(true)
+ .zeroBottom(args.x_axis_type === 'categorical')
+ .numericalDomainFromData(baselines)
+ .numericalRange('left');
+
+ args.scalefns.youtf = d => args.scalefns.yf(d);
+ }
+
+ if (args.ygroup_accessor !== null) {
+ args.ycolor_accessor = args.y_accessor;
+ MG.scale_factory(args)
+ .namespace('ycolor')
+ .scaleName('color')
+ .categoricalDomainFromData()
+ .categoricalColorRange();
+ }
+
+ if (args.xgroup_accessor !== null) {
+ args.xcolor_accessor = args.x_accessor;
+ MG.scale_factory(args)
+ .namespace('xcolor')
+ .scaleName('color')
+ .categoricalDomainFromData()
+ .categoricalColorRange();
+ }
+
+ // if (args.ygroup_accessor !== null) {
+ // MG.scale_factory(args)
+ // .namespace('ygroup')
+ // .categoricalDomainFromData()
+ // .categoricalColorRange();
+ // }
+
+ new MG.axis_factory(args)
+ .namespace('x')
+ .type(args.x_axis_type)
+ .zeroLine(args.y_axis_type === 'categorical')
+ .position(args.x_axis_position)
+ .draw();
+
+ new MG.axis_factory(args)
+ .namespace('y')
+ .type(args.y_axis_type)
+ .zeroLine(args.x_axis_type === 'categorical')
+ .position(args.y_axis_position)
+ .draw();
+
+ //mg_categorical_group_color_scale(args);
+
+ this.mainPlot();
+ this.markers();
+ this.rollover();
+ this.windowListeners();
+ //scaffold(args)
+
+ return this;
+ };
+
+ this.mainPlot = () => {
+ const svg = mg_get_svg_child_of(args.target);
+ const data = args.data[0];
+ let barplot = svg.select('g.mg-barplot');
+ const fresh_render = barplot.empty();
+
+ let bars, predictor_bars, pp, pp0, baseline_marks;
+
+ const perform_load_animation = fresh_render && args.animate_on_load;
+ const should_transition = perform_load_animation || args.transition_on_update;
+ const transition_duration = args.transition_duration || 1000;
+
+ // draw the plot on first render
+ if (fresh_render) {
+ barplot = svg.append('g')
+ .classed('mg-barplot', true);
+ }
+
+ bars = barplot.selectAll('.mg-bar')
+ .data(data)
+ .enter()
+ .append('rect')
+ .classed('mg-bar', true)
+ .classed('default-bar', args.scales.hasOwnProperty('COLOR') ? false : true);
+
+ // TODO - reimplement
+
+ // reference_accessor {}
+
+ // if (args.predictor_accessor) {
+ // predictor_bars = barplot.selectAll('.mg-bar-prediction')
+ // .data(data.filter(function(d) {
+ // return d.hasOwnProperty(args.predictor_accessor) }));
+
+ // predictor_bars.exit().remove();
+
+ // predictor_bars.enter().append('rect')
+ // .classed('mg-bar-prediction', true);
+ // }
+
+ // if (args.baseline_accessor) {
+ // baseline_marks = barplot.selectAll('.mg-bar-baseline')
+ // .data(data.filter(function(d) {
+ // return d.hasOwnProperty(args.baseline_accessor) }));
+
+ // baseline_marks.exit().remove();
+
+ // baseline_marks.enter().append('line')
+ // .classed('mg-bar-baseline', true);
+ // }
+
+ let appropriate_size;
+
+ // setup transitions
+ // if (should_transition) {
+ // bars = bars.transition()
+ // .duration(transition_duration);
+
+ // if (predictor_bars) {
+ // predictor_bars = predictor_bars.transition()
+ // .duration(transition_duration);
+ // }
+
+ // if (baseline_marks) {
+ // baseline_marks = baseline_marks.transition()
+ // .duration(transition_duration);
+ // }
+ // }
+
+ //appropriate_size = args.scales.Y_ingroup.rangeBand()/1.5;
+ let length, width, length_type, width_type, length_coord, width_coord,
+ length_scalefn, width_scalefn, length_scale, width_scale,
+ length_accessor, width_accessor, length_coord_map, width_coord_map,
+ length_map, width_map;
+
+ let reference_length_map, reference_length_coord_fn;
+
+ if (args.orientation == 'vertical') {
+ length = 'height';
+ width = 'width';
+ length_type = args.y_axis_type;
+ width_type = args.x_axis_type;
+ length_coord = 'y';
+ width_coord = 'x';
+ length_scalefn = length_type == 'categorical' ? args.scalefns.youtf : args.scalefns.yf;
+ width_scalefn = width_type == 'categorical' ? args.scalefns.xoutf : args.scalefns.xf;
+ length_scale = args.scales.Y;
+ width_scale = args.scales.X;
+ length_accessor = args.y_accessor;
+ width_accessor = args.x_accessor;
+
+ length_coord_map = d => {
+ let l;
+ l = length_scalefn(d);
+ if (d[length_accessor] < 0) {
+ l = length_scale(0);
+ }
+ return l;
+ };
+
+ length_map = d => Math.abs(length_scalefn(d) - length_scale(0));
+
+ reference_length_map = d => Math.abs(length_scale(d[args.reference_accessor]) - length_scale(0));
+
+ reference_length_coord_fn = d => length_scale(d[args.reference_accessor]);
+ }
+
+ if (args.orientation == 'horizontal') {
+ length = 'width';
+ width = 'height';
+ length_type = args.x_axis_type;
+ width_type = args.y_axis_type;
+ length_coord = 'x';
+ width_coord = 'y';
+ length_scalefn = length_type == 'categorical' ? args.scalefns.xoutf : args.scalefns.xf;
+ width_scalefn = width_type == 'categorical' ? args.scalefns.youtf : args.scalefns.yf;
+ length_scale = args.scales.X;
+ width_scale = args.scales.Y;
+ length_accessor = args.x_accessor;
+ width_accessor = args.y_accessor;
+
+ length_coord_map = d => {
+ let l;
+ l = length_scale(0);
+ return l;
+ };
+
+ length_map = d => Math.abs(length_scalefn(d) - length_scale(0));
+
+ reference_length_map = d => Math.abs(length_scale(d[args.reference_accessor]) - length_scale(0));
+
+ reference_length_coord_fn = d => length_scale(0);
+ }
+
+ // if (perform_load_animation) {
+ // bars.attr(length, 0);
+
+ // if (predictor_bars) {
+ // predictor_bars.attr(length, 0);
+ // }
+
+ // // if (baseline_marks) {
+ // // baseline_marks.attr({
+ // // x1: args.scales.X(0),
+ // // x2: args.scales.X(0)
+ // // });
+ // // }
+ // }
+
+ bars.attr(length_coord, length_coord_map);
+
+ // bars.attr(length_coord, 40)
+ //bars.attr(width_coord, 70)
+
+ bars.attr(width_coord, d => {
+ let w;
+ if (width_type == 'categorical') {
+ w = width_scalefn(d);
+ } else {
+ w = width_scale(0);
+ if (d[width_accessor] < 0) {
+ w = width_scalefn(d);
+ }
+ }
+ w = w - args.bar_thickness/2;
+ return w;
+ });
+
+ if (args.scales.COLOR) {
+ bars.attr('fill', args.scalefns.colorf);
+ }
+
+ bars
+ .attr(length, length_map)
+ .attr(width, d => args.bar_thickness);
+
+ if (args.reference_accessor !== null) {
+ const reference_data = data.filter(d => d.hasOwnProperty(args.reference_accessor));
+ const reference_bars = barplot.selectAll('.mg-categorical-reference')
+ .data(reference_data)
+ .enter()
+ .append('rect');
+
+ reference_bars
+ .attr(length_coord, reference_length_coord_fn)
+ .attr(width_coord, d => width_scalefn(d) - args.reference_thickness/2)
+ .attr(length, reference_length_map)
+ .attr(width, args.reference_thickness);
+ }
+
+ if (args.comparison_accessor !== null) {
+ let comparison_thickness = null;
+ if (args.comparison_thickness === null) {
+ comparison_thickness = args.bar_thickness/2;
+ } else {
+ comparison_thickness = args.comparison_thickness;
+ }
+
+ const comparison_data = data.filter(d => d.hasOwnProperty(args.comparison_accessor));
+ const comparison_marks = barplot.selectAll('.mg-categorical-comparison')
+ .data(comparison_data)
+ .enter()
+ .append('line');
+
+ comparison_marks
+ .attr(`${length_coord}1`, d => length_scale(d[args.comparison_accessor]))
+ .attr(`${length_coord}2`, d => length_scale(d[args.comparison_accessor]))
+ .attr(`${width_coord}1`, d => width_scalefn(d) - comparison_thickness/2)
+ .attr(`${width_coord}2`, d => width_scalefn(d) + comparison_thickness/2)
+ .attr('stroke', 'black')
+ .attr('stroke-width', args.comparison_width);
+ }
+
+ //bars.attr(width_coord, );
+ // bars.attr('width', 50);
+ // bars.attr('height', 50);
+ // bars.attr('y', function(d){
+ // var y = args.scales.Y(0);
+ // if (d[args.y_accessor] < 0) {
+ // y = args.scalefns.yf(d);
+ // }
+ // return y;
+ // });
+
+ // bars.attr('x', function(d){
+ // return 40;
+ // })
+
+ // bars.attr('width', function(d){
+ // return 100;
+ // });
+
+ // bars.attr('height', 100);
+
+ // bars.attr('fill', 'black');
+ // bars.attr('x', function(d) {
+ // var x = args.scales.X(0);
+ // if (d[args.x_accessor] < 0) {
+ // x = args.scalefns.xf(d);
+ // }
+ // return x;
+ // })
+ // TODO - reimplement.
+ // if (args.predictor_accessor) {
+ // predictor_bars
+ // .attr('x', args.scales.X(0))
+ // .attr('y', function(d) {
+ // return args.scalefns.ygroupf(d) + args.scalefns.yf(d) + args.scales.Y.rangeBand() * (7 / 16) // + pp0 * appropriate_size/(pp*2) + appropriate_size / 2;
+ // })
+ // .attr('height', args.scales.Y.rangeBand() / 8) //appropriate_size / pp)
+ // .attr('width', function(d) {
+ // return args.scales.X(d[args.predictor_accessor]) - args.scales.X(0);
+ // });
+ // }
+
+ // TODO - reimplement.
+ // if (args.baseline_accessor) {
+
+ // baseline_marks
+ // .attr('x1', function(d) {
+ // return args.scales.X(d[args.baseline_accessor]); })
+ // .attr('x2', function(d) {
+ // return args.scales.X(d[args.baseline_accessor]); })
+ // .attr('y1', function(d) {
+ // return args.scalefns.ygroupf(d) + args.scalefns.yf(d) + args.scales.Y.rangeBand() / 4
+ // })
+ // .attr('y2', function(d) {
+ // return args.scalefns.ygroupf(d) + args.scalefns.yf(d) + args.scales.Y.rangeBand() * 3 / 4
+ // });
+ // }
+ if (args.legend || (args.color_accessor !== null && args.ygroup_accessor !== args.color_accessor)) {
+ if (!args.legend_target) legend_on_graph(svg, args);
+ else mg_targeted_legend(args);
+ }
+ return this;
+ };
+
+ this.markers = () => {
+ markers(args);
+ return this;
+ };
+
+ this.rollover = () => {
+ const svg = mg_get_svg_child_of(args.target);
+ let g;
+
+ if (svg.selectAll('.mg-active-datapoint-container').nodes().length === 0) {
+ mg_add_g(svg, 'mg-active-datapoint-container');
+ }
+
+ //remove the old rollovers if they already exist
+ svg.selectAll('.mg-rollover-rect').remove();
+ svg.selectAll('.mg-active-datapoint').remove();
+
+ // get orientation
+ let length, width, length_type, width_type, length_coord, width_coord,
+ length_scalefn, width_scalefn, length_scale, width_scale,
+ length_accessor, width_accessor;
+
+ let length_coord_map, width_coord_map, length_map, width_map;
+
+ if (args.orientation == 'vertical') {
+ length = 'height';
+ width = 'width';
+ length_type = args.y_axis_type;
+ width_type = args.x_axis_type;
+ length_coord = 'y';
+ width_coord = 'x';
+ length_scalefn = length_type == 'categorical' ? args.scalefns.youtf : args.scalefns.yf;
+ width_scalefn = width_type == 'categorical' ? args.scalefns.xoutf : args.scalefns.xf;
+ length_scale = args.scales.Y;
+ width_scale = args.scales.X;
+ length_accessor = args.y_accessor;
+ width_accessor = args.x_accessor;
+
+ length_coord_map = d => mg_get_plot_top(args);
+
+ length_map = d => args.height -args.top-args.bottom-args.buffer*2;
+ }
+
+ if (args.orientation == 'horizontal') {
+ length = 'width';
+ width = 'height';
+ length_type = args.x_axis_type;
+ width_type = args.y_axis_type;
+ length_coord = 'x';
+ width_coord = 'y';
+ length_scalefn = length_type == 'categorical' ? args.scalefns.xoutf : args.scalefns.xf;
+ width_scalefn = width_type == 'categorical' ? args.scalefns.youtf : args.scalefns.yf;
+ length_scale = args.scales.X;
+ width_scale = args.scales.Y;
+ length_accessor = args.x_accessor;
+ width_accessor = args.y_accessor;
+
+ length_coord_map = d => {
+ let l;
+ l = length_scale(0);
+ return l;
+ };
+
+ length_map = d => args.width -args.left-args.right-args.buffer*2;
+ }
+
+ //rollover text
+ let rollover_x, rollover_anchor;
+ if (args.rollover_align === 'right') {
+ rollover_x = args.width - args.right;
+ rollover_anchor = 'end';
+ } else if (args.rollover_align === 'left') {
+ rollover_x = args.left;
+ rollover_anchor = 'start';
+ } else {
+ rollover_x = (args.width - args.left - args.right) / 2 + args.left;
+ rollover_anchor = 'middle';
+ }
+
+ svg.append('text')
+ .attr('class', 'mg-active-datapoint')
+ .attr('xml:space', 'preserve')
+ .attr('x', rollover_x)
+ .attr('y', args.top * 0.75)
+ .attr('dy', '.35em')
+ .attr('text-anchor', rollover_anchor);
+
+ g = svg.append('g')
+ .attr('class', 'mg-rollover-rect');
+
+ //draw rollover bars
+ const bars = g.selectAll(".mg-bar-rollover")
+ .data(args.data[0]).enter()
+ .append("rect")
+ .attr('class', 'mg-bar-rollover');
+
+ bars.attr('opacity', 0)
+ .attr(length_coord, length_coord_map)
+ .attr(width_coord, d => {
+ let w;
+ if (width_type == 'categorical') {
+ w = width_scalefn(d);
+ } else {
+ w = width_scale(0);
+ if (d[width_accessor] < 0) {
+ w = width_scalefn(d);
+ }
+ }
+ w = w - args.bar_thickness/2;
+ return w;
+ });
+
+ bars.attr(length, length_map);
+ bars.attr(width, d => args.bar_thickness);
+
+ bars
+ .on('mouseover', this.rolloverOn(args))
+ .on('mouseout', this.rolloverOff(args))
+ .on('mousemove', this.rolloverMove(args));
+
+ return this;
+ };
+
+ this.rolloverOn = (args) => {
+ const svg = mg_get_svg_child_of(args.target);
+ const label_accessor = this.is_vertical ? args.x_accessor : args.y_accessor;
+ const data_accessor = this.is_vertical ? args.y_accessor : args.x_accessor;
+ const label_units = this.is_vertical ? args.yax_units : args.xax_units;
+
+ return (d, i) => {
+
+ const fmt = MG.time_format(args.utc_time, '%b %e, %Y');
+ const num = format_rollover_number(args);
+
+ //highlight active bar
+ const bar = svg.selectAll('g.mg-barplot .mg-bar')
+ .filter((d, j) => j === i).classed('active', true);
+
+ if (args.scales.hasOwnProperty('COLOR')) {
+ bar.attr('fill', d3.rgb(args.scalefns.colorf(d)).darker());
+ } else {
+ bar.classed('default-active', true);
+ }
+
+ //update rollover text
+ if (args.show_rollover_text) {
+ const mouseover = mg_mouseover_text(args, { svg });
+ let row = mouseover.mouseover_row();
+
+ if (args.ygroup_accessor) row.text(`${d[args.ygroup_accessor]} `).bold();
+
+ row.text(mg_format_x_mouseover(args, d));
+ row.text(`${args.y_accessor}: ${d[args.y_accessor]}`);
+ if (args.predictor_accessor || args.baseline_accessor) {
+ row = mouseover.mouseover_row();
+
+ if (args.predictor_accessor) row.text(mg_format_data_for_mouseover(args, d, null, args.predictor_accessor, false));
+ if (args.baseline_accessor) row.text(mg_format_data_for_mouseover(args, d, null, args.baseline_accessor, false));
+ }
+ }
+ if (args.mouseover) {
+ args.mouseover(d, i);
+ }
+ };
+ };
+
+ this.rolloverOff = (args) => {
+ const svg = mg_get_svg_child_of(args.target);
+
+ return (d, i) => {
+ //reset active bar
+ const bar = svg.selectAll('g.mg-barplot .mg-bar.active').classed('active', false);
+
+ if (args.scales.hasOwnProperty('COLOR')) {
+ bar.attr('fill', args.scalefns.colorf(d));
+ } else {
+ bar.classed('default-active', false);
+ }
+
+ //reset active data point text
+ svg.select('.mg-active-datapoint')
+ .text('');
+
+ mg_clear_mouseover_container(svg);
+
+ if (args.mouseout) {
+ args.mouseout(d, i);
+ }
+ };
+ };
+
+ this.rolloverMove = (args) => (d, i) => {
+ if (args.mousemove) {
+ args.mousemove(d, i);
+ }
+ };
+
+ this.windowListeners = () => {
+ mg_window_listeners(this.args);
+ return this;
+ };
+
+ this.init(args);
+ }
+
+ const options = {
+ buffer: [16, 'number'],
+ y_accessor: ['factor', 'string'],
+ x_accessor: ['value', 'string'],
+ reference_accessor: [null, 'string'],
+ comparison_accessor: [null, 'string'],
+ secondary_label_accessor: [null, 'string'],
+ color_accessor: [null, 'string'],
+ color_type: ['category', ['number', 'category']],
+ color_domain: [null, 'number[]'],
+ reference_thickness: [1, 'number'],
+ comparison_width: [3, 'number'],
+ comparison_thickness: [null, 'number'],
+ legend: [false, 'boolean'],
+ legend_target: [null, 'string'],
+ mouseover_align: ['right', ['right', 'left']],
+ baseline_accessor: [null, 'string'],
+ predictor_accessor: [null, 'string'],
+ predictor_proportion: [5, 'number'],
+ show_bar_zero: [true, 'boolean'],
+ binned: [true, 'boolean'],
+ truncate_x_labels: [true, 'boolean'],
+ truncate_y_labels: [true, 'boolean']
+ };
+
+ MG.register('bar', barChart, options);
+
+}
diff --git a/priv/static/js/metricsgraphics/charts/histogram.js b/priv/static/js/metricsgraphics/charts/histogram.js
new file mode 100644
index 0000000..3fca8e3
--- /dev/null
+++ b/priv/static/js/metricsgraphics/charts/histogram.js
@@ -0,0 +1,222 @@
+{
+ function histogram(args) {
+ this.init = (args) => {
+ this.args = args;
+
+ raw_data_transformation(args);
+ process_histogram(args);
+ init(args);
+
+ new MG.scale_factory(args)
+ .namespace('x')
+ .numericalDomainFromData()
+ .numericalRange('bottom');
+
+ const baselines = (args.baselines || []).map(d => d[args.y_accessor]);
+
+ new MG.scale_factory(args)
+ .namespace('y')
+ .zeroBottom(true)
+ .inflateDomain(true)
+ .numericalDomainFromData(baselines)
+ .numericalRange('left');
+
+ x_axis(args);
+ y_axis(args);
+
+ this.mainPlot();
+ this.markers();
+ this.rollover();
+ this.windowListeners();
+
+ return this;
+ };
+
+ this.mainPlot = () => {
+ const svg = mg_get_svg_child_of(args.target);
+
+ //remove the old histogram, add new one
+ svg.selectAll('.mg-histogram').remove();
+
+ const g = svg.append('g')
+ .attr('class', 'mg-histogram');
+
+ const bar = g.selectAll('.mg-bar')
+ .data(args.data[0])
+ .enter().append('g')
+ .attr('class', 'mg-bar')
+ .attr('transform', d => `translate(${args.scales.X(d[args.x_accessor]).toFixed(2)},${args.scales.Y(d[args.y_accessor]).toFixed(2)})`);
+
+ //draw bars
+ bar.append('rect')
+ .attr('x', 1)
+ .attr('width', (d, i) => {
+ if (args.data[0].length === 1) {
+ return (args.scalefns.xf(args.data[0][0]) - args.bar_margin).toFixed(0);
+ } else if (i !== args.data[0].length - 1) {
+ return (args.scalefns.xf(args.data[0][i + 1]) - args.scalefns.xf(d)).toFixed(0);
+ } else {
+ return (args.scalefns.xf(args.data[0][1]) - args.scalefns.xf(args.data[0][0])).toFixed(0);
+ }
+ })
+ .attr('height', d => {
+ if (d[args.y_accessor] === 0) {
+ return 0;
+ }
+
+ return (args.height - args.bottom - args.buffer - args.scales.Y(d[args.y_accessor])).toFixed(2);
+ });
+
+ return this;
+ };
+
+ this.markers = () => {
+ markers(args);
+ return this;
+ };
+
+ this.rollover = () => {
+ const svg = mg_get_svg_child_of(args.target);
+
+ if (svg.selectAll('.mg-active-datapoint-container').nodes().length === 0) {
+ mg_add_g(svg, 'mg-active-datapoint-container');
+ }
+
+ //remove the old rollovers if they already exist
+ svg.selectAll('.mg-rollover-rect').remove();
+ svg.selectAll('.mg-active-datapoint').remove();
+
+ const g = svg.append('g')
+ .attr('class', 'mg-rollover-rect');
+
+ //draw rollover bars
+ const bar = g.selectAll('.mg-bar')
+ .data(args.data[0])
+ .enter().append('g')
+ .attr('class', (d, i) => {
+ if (args.linked) {
+ return `mg-rollover-rects roll_${i}`;
+ } else {
+ return 'mg-rollover-rects';
+ }
+ })
+ .attr('transform', d => `translate(${args.scales.X(d[args.x_accessor])},${0})`);
+
+ bar.append('rect')
+ .attr('x', 1)
+ .attr('y', args.buffer + (args.title ? args.title_y_position : 0))
+ .attr('width', (d, i) => {
+ //if data set is of length 1
+ if (args.data[0].length === 1) {
+ return (args.scalefns.xf(args.data[0][0]) - args.bar_margin).toFixed(0);
+ } else if (i !== args.data[0].length - 1) {
+ return (args.scalefns.xf(args.data[0][i + 1]) - args.scalefns.xf(d)).toFixed(0);
+ } else {
+ return (args.scalefns.xf(args.data[0][1]) - args.scalefns.xf(args.data[0][0])).toFixed(0);
+ }
+ })
+ .attr('height', d => args.height)
+ .attr('opacity', 0)
+ .on('mouseover', this.rolloverOn(args))
+ .on('mouseout', this.rolloverOff(args))
+ .on('mousemove', this.rolloverMove(args));
+
+ return this;
+ };
+
+ this.rolloverOn = (args) => {
+ const svg = mg_get_svg_child_of(args.target);
+
+ return (d, i) => {
+ svg.selectAll('text')
+ .filter((g, j) => d === g)
+ .attr('opacity', 0.3);
+
+ const fmt = args.processed.xax_format || MG.time_format(args.utc_time, '%b %e, %Y');
+ const num = format_rollover_number(args);
+
+ svg.selectAll('.mg-bar rect')
+ .filter((d, j) => j === i)
+ .classed('active', true);
+
+ //trigger mouseover on all matching bars
+ if (args.linked && !MG.globals.link) {
+ MG.globals.link = true;
+
+ //trigger mouseover on matching bars in .linked charts
+ d3.selectAll(`.mg-rollover-rects.roll_${i} rect`)
+ .each(function(d) { //use existing i
+ d3.select(this).on('mouseover')(d, i);
+ });
+ }
+
+ //update rollover text
+ if (args.show_rollover_text) {
+ const mo = mg_mouseover_text(args, { svg });
+ const row = mo.mouseover_row();
+ row.text('\u259F ').elem
+ .classed('hist-symbol', true);
+
+ row.text(mg_format_x_mouseover(args, d)); // x
+ row.text(mg_format_y_mouseover(args, d, args.time_series === false));
+ }
+
+ if (args.mouseover) {
+ mg_setup_mouseover_container(svg, args);
+ args.mouseover(d, i);
+ }
+ };
+ };
+
+ this.rolloverOff = (args) => {
+ const svg = mg_get_svg_child_of(args.target);
+
+ return (d, i) => {
+ if (args.linked && MG.globals.link) {
+ MG.globals.link = false;
+
+ //trigger mouseout on matching bars in .linked charts
+ d3.selectAll(`.mg-rollover-rects.roll_${i} rect`)
+ .each(function(d) { //use existing i
+ d3.select(this).on('mouseout')(d, i);
+ });
+ }
+
+ //reset active bar
+ svg.selectAll('.mg-bar rect')
+ .classed('active', false);
+
+ //reset active data point text
+ mg_clear_mouseover_container(svg);
+
+ if (args.mouseout) {
+ args.mouseout(d, i);
+ }
+ };
+ };
+
+ this.rolloverMove = (args) => (d, i) => {
+ if (args.mousemove) {
+ args.mousemove(d, i);
+ }
+ };
+
+ this.windowListeners = () => {
+ mg_window_listeners(this.args);
+ return this;
+ };
+
+ this.init(args);
+ }
+
+ const options = {
+ bar_margin: [1, "number"], // the margin between bars
+ binned: [false, "boolean"], // determines whether the data is already binned
+ bins: [null, ['number', 'number[]', 'function']], // the number of bins to use. type: {null, number | thresholds | threshold_function}
+ processed_x_accessor: ['x', 'string'],
+ processed_y_accessor: ['y', 'string'],
+ processed_dx_accessor: ['dx', 'string']
+ };
+
+ MG.register('histogram', histogram, options);
+}
diff --git a/priv/static/js/metricsgraphics/charts/line.js b/priv/static/js/metricsgraphics/charts/line.js
new file mode 100644
index 0000000..f441af3
--- /dev/null
+++ b/priv/static/js/metricsgraphics/charts/line.js
@@ -0,0 +1,922 @@
+{
+ function mg_line_color_text(elem, line_id, {color, colors}) {
+ elem.classed('mg-hover-line-color', color === null)
+ .classed(`mg-hover-line${line_id}-color`, colors === null)
+ .attr('fill', colors === null ? '' : colors[line_id - 1]);
+ }
+
+ function mg_line_graph_generators(args, plot, svg) {
+ mg_add_line_generator(args, plot);
+ mg_add_area_generator(args, plot);
+ mg_add_flat_line_generator(args, plot);
+ mg_add_confidence_band_generator(args, plot, svg);
+ }
+
+ function mg_add_confidence_band_generator(args, plot, svg) {
+ plot.existing_band = svg.selectAll('.mg-confidence-band').nodes();
+ if (args.show_confidence_band) {
+ plot.confidence_area = d3.area()
+ .defined(plot.line.defined())
+ .x(args.scalefns.xf)
+ .y0(d => {
+ const l = args.show_confidence_band[0];
+ if (d[l] != undefined) {
+ return args.scales.Y(d[l]);
+ } else {
+ return args.scales.Y(d[args.y_accessor]);
+ }
+ })
+ .y1(d => {
+ const u = args.show_confidence_band[1];
+ if (d[u] != undefined) {
+ return args.scales.Y(d[u]);
+ } else {
+ return args.scales.Y(d[args.y_accessor]);
+ }
+ })
+ .curve(args.interpolate);
+ }
+ }
+
+ function mg_add_area_generator({scalefns, scales, interpolate, flip_area_under_y_value}, plot) {
+
+ const areaBaselineValue = (Number.isFinite(flip_area_under_y_value)) ? scales.Y(flip_area_under_y_value) : scales.Y.range()[0];
+
+ plot.area = d3.area()
+ .defined(plot.line.defined())
+ .x(scalefns.xf)
+ .y0(() => {
+ return areaBaselineValue;
+ })
+ .y1(scalefns.yf)
+ .curve(interpolate);
+ }
+
+ function mg_add_flat_line_generator({y_accessor, scalefns, scales, interpolate}, plot) {
+ plot.flat_line = d3.line()
+ .defined(d => (d['_missing'] === undefined || d['_missing'] !== true) && d[y_accessor] !== null)
+ .x(scalefns.xf)
+ .y(() => scales.Y(plot.data_median))
+ .curve(interpolate);
+ }
+
+ function mg_add_line_generator({scalefns, interpolate, missing_is_zero, y_accessor}, plot) {
+ plot.line = d3.line()
+ .x(scalefns.xf)
+ .y(scalefns.yf)
+ .curve(interpolate);
+
+ // if missing_is_zero is not set, then hide data points that fall in missing
+ // data ranges or that have been explicitly identified as missing in the
+ // data source.
+ if (!missing_is_zero) {
+ // a line is defined if the _missing attrib is not set to true
+ // and the y-accessor is not null
+ plot.line = plot.line.defined(d => (d['_missing'] === undefined || d['_missing'] !== true) && d[y_accessor] !== null);
+ }
+ }
+
+ function mg_add_confidence_band(
+ {show_confidence_band, transition_on_update, data, target},
+ plot,
+ svg,
+ which_line
+ ) {
+ if (show_confidence_band) {
+ let confidenceBand;
+ if (svg.select(`.mg-confidence-band-${which_line}`).empty()) {
+ svg.append('path')
+ .attr('class', `mg-confidence-band mg-confidence-band-${which_line}`);
+ }
+
+ // transition this line's confidence band
+ confidenceBand = svg.select(`.mg-confidence-band-${which_line}`);
+
+ confidenceBand
+ .transition()
+ .duration(() => (transition_on_update) ? 1000 : 0)
+ .attr('d', plot.confidence_area(data[which_line - 1]))
+ .attr('clip-path', `url(#mg-plot-window-${mg_target_ref(target)})`);
+ }
+ }
+
+ function mg_add_area({data, target, colors}, plot, svg, which_line, line_id) {
+ const areas = svg.selectAll(`.mg-main-area.mg-area${line_id}`);
+ if (plot.display_area) {
+ // if area already exists, transition it
+ if (!areas.empty()) {
+ svg.node().appendChild(areas.node());
+
+ areas.transition()
+ .duration(plot.update_transition_duration)
+ .attr('d', plot.area(data[which_line]))
+ .attr('clip-path', `url(#mg-plot-window-${mg_target_ref(target)})`);
+ } else { // otherwise, add the area
+ svg.append('path')
+ .classed('mg-main-area', true)
+ .classed(`mg-area${line_id}`, true)
+ .classed('mg-area-color', colors === null)
+ .classed(`mg-area${line_id}-color`, colors === null)
+ .attr('d', plot.area(data[which_line]))
+ .attr('fill', colors === null ? '' : colors[line_id - 1])
+ .attr('clip-path', `url(#mg-plot-window-${mg_target_ref(target)})`);
+ }
+ } else if (!areas.empty()) {
+ areas.remove();
+ }
+ }
+
+ function mg_default_color_for_path(this_path, line_id) {
+ this_path.classed('mg-line-color', true)
+ .classed(`mg-line${line_id}-color`, true);
+ }
+
+ function mg_color_line({colors}, this_path, which_line, line_id) {
+ if (colors) {
+ // for now, if args.colors is not an array, then keep moving as if nothing happened.
+ // if args.colors is not long enough, default to the usual line_id color.
+ if (colors.constructor === Array) {
+ this_path.attr('stroke', colors[which_line]);
+ if (colors.length < which_line + 1) {
+ // Go with default coloring.
+ // this_path.classed('mg-line' + (line_id) + '-color', true);
+ mg_default_color_for_path(this_path, line_id);
+ }
+ } else {
+ // this_path.classed('mg-line' + (line_id) + '-color', true);
+ mg_default_color_for_path(this_path, line_id);
+ }
+ } else {
+ // this is the typical workflow
+ // this_path.classed('mg-line' + (line_id) + '-color', true);
+ mg_default_color_for_path(this_path, line_id);
+ }
+ }
+
+ function mg_add_line_element({animate_on_load, data, y_accessor, target}, plot, this_path, which_line) {
+ if (animate_on_load) {
+ plot.data_median = d3.median(data[which_line], d => d[y_accessor]);
+ this_path.attr('d', plot.flat_line(data[which_line]))
+ .transition()
+ .duration(1000)
+ .attr('d', plot.line(data[which_line]))
+ .attr('clip-path', `url(#mg-plot-window-${mg_target_ref(target)})`);
+ } else { // or just add the line
+ this_path.attr('d', plot.line(data[which_line]))
+ .attr('clip-path', `url(#mg-plot-window-${mg_target_ref(target)})`);
+ }
+ }
+
+ function mg_add_line(args, plot, svg, existing_line, which_line, line_id) {
+ if (!existing_line.empty()) {
+ svg.node().appendChild(existing_line.node());
+
+ const lineTransition = existing_line.transition()
+ .duration(plot.update_transition_duration);
+
+ if (!plot.display_area && args.transition_on_update && !args.missing_is_hidden) {
+ lineTransition.attrTween('d', path_tween(plot.line(args.data[which_line]), 4));
+ } else {
+ lineTransition.attr('d', plot.line(args.data[which_line]));
+ }
+ } else { // otherwise...
+ // if we're animating on load, animate the line from its median value
+ const this_path = svg.append('path')
+ .attr('class', `mg-main-line mg-line${line_id}`);
+
+ mg_color_line(args, this_path, which_line, line_id);
+ mg_add_line_element(args, plot, this_path, which_line);
+ }
+ }
+
+ function mg_add_legend_element(args, plot, which_line, line_id) {
+ let this_legend;
+ if (args.legend) {
+ if (is_array(args.legend)) {
+ this_legend = args.legend[which_line];
+ } else if (is_function(args.legend)) {
+ this_legend = args.legend(args.data[which_line]);
+ }
+
+ if (args.legend_target) {
+ if (args.colors && args.colors.constructor === Array) {
+ plot.legend_text = `<span style='color:${args.colors[which_line]}'>&mdash; ${this_legend}&nbsp; </span>${plot.legend_text}`;
+ } else {
+ plot.legend_text = `<span class='mg-line${line_id}-legend-color'>&mdash; ${this_legend}&nbsp; </span>${plot.legend_text}`;
+ }
+ } else {
+ let anchor_point, anchor_orientation, dx;
+
+ if (args.y_axis_position === 'left') {
+ anchor_point = args.data[which_line][args.data[which_line].length - 1];
+ anchor_orientation = 'start';
+ dx = args.buffer;
+ } else {
+ anchor_point = args.data[which_line][0];
+ anchor_orientation = 'end';
+ dx = -args.buffer;
+ }
+ const legend_text = plot.legend_group.append('svg:text')
+ .attr('x', args.scalefns.xf(anchor_point))
+ .attr('dx', dx)
+ .attr('y', args.scalefns.yf(anchor_point))
+ .attr('dy', '.35em')
+ .attr('font-size', 10)
+ .attr('text-anchor', anchor_orientation)
+ .attr('font-weight', '300')
+ .text(this_legend);
+
+ if (args.colors && args.colors.constructor === Array) {
+ if (args.colors.length < which_line + 1) {
+ legend_text.classed(`mg-line${line_id}-legend-color`, true);
+ } else {
+ legend_text.attr('fill', args.colors[which_line]);
+ }
+ } else {
+ legend_text.classed('mg-line-legend-color', true)
+ .classed(`mg-line${line_id}-legend-color`, true);
+ }
+
+ mg_prevent_vertical_overlap(plot.legend_group.selectAll('.mg-line-legend text').nodes(), args);
+ }
+ }
+ }
+
+ function mg_plot_legend_if_legend_target(target, legend) {
+ if (target) d3.select(target).html(legend);
+ }
+
+ function mg_add_legend_group({legend}, plot, svg) {
+ if (legend) plot.legend_group = mg_add_g(svg, 'mg-line-legend');
+ }
+
+ function mg_remove_existing_line_rollover_elements(svg) {
+ // remove the old rollovers if they already exist
+ mg_selectAll_and_remove(svg, '.mg-rollover-rect');
+ mg_selectAll_and_remove(svg, '.mg-voronoi');
+
+ // remove the old rollover text and circle if they already exist
+ mg_selectAll_and_remove(svg, '.mg-active-datapoint');
+ mg_selectAll_and_remove(svg, '.mg-line-rollover-circle');
+ //mg_selectAll_and_remove(svg, '.mg-active-datapoint-container');
+ }
+
+ function mg_add_rollover_circle({data, colors}, svg) {
+ // append circle
+ const circle = svg.selectAll('.mg-line-rollover-circle')
+ .data(data)
+ .enter().append('circle')
+ .attr('cx', 0)
+ .attr('cy', 0)
+ .attr('r', 0);
+
+ if (colors && colors.constructor === Array) {
+ circle
+ .attr('class', ({__line_id__}) => `mg-line${__line_id__}`)
+ .attr('fill', (d, i) => colors[i])
+ .attr('stroke', (d, i) => colors[i]);
+ } else {
+ circle.attr('class', ({__line_id__}, i) => [
+ `mg-line${__line_id__}`,
+ `mg-line${__line_id__}-color`,
+ `mg-area${__line_id__}-color`
+ ].join(' '));
+ }
+ circle.classed('mg-line-rollover-circle', true);
+ }
+
+ function mg_set_unique_line_id_for_each_series({data, custom_line_color_map}) {
+ // update our data by setting a unique line id for each series
+ // increment from 1... unless we have a custom increment series
+
+ for (let i = 0; i < data.length; i++) {
+ data[i].forEach(datum => {
+ datum.__index__ = i + 1;
+ datum.__line_id__ = (custom_line_color_map.length > 0) ? custom_line_color_map[i] : i + 1;
+ });
+ }
+ }
+
+ function mg_nest_data_for_voronoi({data}) {
+ return d3.merge(data);
+ }
+
+ function mg_line_class_string(args) {
+ return d => {
+ let class_string;
+
+ if (args.linked) {
+ const v = d[args.x_accessor];
+ const formatter = MG.time_format(args.utc_time, args.linked_format);
+
+ // only format when x-axis is date
+ const id = (typeof v === 'number') ? (d.__line_id__ - 1) : formatter(v);
+ class_string = `roll_${id} mg-line${d.__line_id__}`;
+
+ if (args.color === null) {
+ class_string += ` mg-line${d.__line_id__}-color`;
+ }
+ return class_string;
+
+ } else {
+ class_string = `mg-line${d.__line_id__}`;
+ if (args.color === null) class_string += ` mg-line${d.__line_id__}-color`;
+ return class_string;
+ }
+ };
+ }
+
+ function mg_add_voronoi_rollover(args, svg, rollover_on, rollover_off, rollover_move, rollover_click) {
+ const voronoi = d3.voronoi()
+ .x(d => args.scales.X(d[args.x_accessor]).toFixed(2))
+ .y(d => args.scales.Y(d[args.y_accessor]).toFixed(2))
+ .extent([
+ [args.buffer, args.buffer + (args.title ? args.title_y_position : 0)],
+ [args.width - args.buffer, args.height - args.buffer]
+ ]);
+
+ const g = mg_add_g(svg, 'mg-voronoi');
+ g.selectAll('path')
+ .data(voronoi.polygons(mg_nest_data_for_voronoi(args)))
+ .enter()
+ .append('path')
+ .filter(d => d !== undefined && d.length > 0)
+ .attr('d', d => d == null ? null : `M${d.join('L')}Z`)
+ .datum(d => d == null ? null : d.data) // because of d3.voronoi, reassign d
+ .attr('class', mg_line_class_string(args))
+ .on('click', rollover_click)
+ .on('mouseover', rollover_on)
+ .on('mouseout', rollover_off)
+ .on('mousemove', rollover_move);
+
+ mg_configure_voronoi_rollover(args, svg);
+ }
+
+ function nest_data_for_aggregate_rollover({x_accessor, data, x_sort}) {
+ const data_nested = d3.nest()
+ .key(d => d[x_accessor])
+ .entries(d3.merge(data));
+ data_nested.forEach(entry => {
+ const datum = entry.values[0];
+ entry.key = datum[x_accessor];
+ });
+
+ if (x_sort) {
+ return data_nested.sort((a, b) => new Date(a.key) - new Date(b.key));
+ } else {
+ return data_nested;
+ }
+ }
+
+ function mg_add_aggregate_rollover(args, svg, rollover_on, rollover_off, rollover_move, rollover_click) {
+ // Undo the keys getting coerced to strings, by setting the keys from the values
+ // This is necessary for when we have X axis keys that are things like
+ const data_nested = nest_data_for_aggregate_rollover(args);
+
+ const xf = data_nested.map(({key}) => args.scales.X(key));
+
+ const g = svg.append('g')
+ .attr('class', 'mg-rollover-rect');
+
+ g.selectAll('.mg-rollover-rects')
+ .data(data_nested).enter()
+ .append('rect')
+ .attr('x', (d, i) => {
+ if (xf.length === 1) return mg_get_plot_left(args);
+ else if (i === 0) return xf[i].toFixed(2);
+ else return ((xf[i - 1] + xf[i]) / 2).toFixed(2);
+ })
+ .attr('y', args.top)
+ .attr('width', (d, i) => {
+ if (xf.length === 1) return mg_get_plot_right(args);
+ else if (i === 0) return ((xf[i + 1] - xf[i]) / 2).toFixed(2);
+ else if (i === xf.length - 1) return ((xf[i] - xf[i - 1]) / 2).toFixed(2);
+ else return ((xf[i + 1] - xf[i - 1]) / 2).toFixed(2);
+ })
+ .attr('class', ({values}) => {
+ let line_classes = values.map(({__line_id__}) => {
+ let lc = mg_line_class(__line_id__);
+ if (args.colors === null) lc += ` ${mg_line_color_class(__line_id__)}`;
+ return lc;
+ }).join(' ');
+ if (args.linked && values.length > 0) {
+ line_classes += ` ${mg_rollover_id_class(mg_rollover_format_id(values[0], args))}`;
+ }
+
+ return line_classes;
+ })
+ .attr('height', args.height - args.bottom - args.top - args.buffer)
+ .attr('opacity', 0)
+ .on('click', rollover_click)
+ .on('mouseover', rollover_on)
+ .on('mouseout', rollover_off)
+ .on('mousemove', rollover_move);
+
+ mg_configure_aggregate_rollover(args, svg);
+ }
+
+ function mg_configure_singleton_rollover({data}, svg) {
+ svg.select('.mg-rollover-rect rect')
+ .on('mouseover')(data[0][0], 0);
+ }
+
+ function mg_configure_voronoi_rollover({data, custom_line_color_map}, svg) {
+ for (let i = 0; i < data.length; i++) {
+ let j = i + 1;
+
+ if (custom_line_color_map.length > 0 &&
+ custom_line_color_map[i] !== undefined) {
+ j = custom_line_color_map[i];
+ }
+
+ if (data[i].length === 1 && !svg.selectAll(`.mg-voronoi .mg-line${j}`).empty()) {
+ svg.selectAll(`.mg-voronoi .mg-line${j}`)
+ .on('mouseover')(data[i][0], 0);
+
+ svg.selectAll(`.mg-voronoi .mg-line${j}`)
+ .on('mouseout')(data[i][0], 0);
+ }
+ }
+ }
+
+ function mg_line_class(line_id) {
+ return `mg-line${line_id}`;
+ }
+
+ function mg_line_color_class(line_id) {
+ return `mg-line${line_id}-color`;
+ }
+
+ function mg_rollover_id_class(id) {
+ return `roll_${id}`;
+ }
+
+ function mg_rollover_format_id(d, {x_accessor, utc_time, linked_format}) {
+ const v = d[x_accessor];
+ const formatter = MG.time_format(utc_time, linked_format);
+ // only format when x-axis is date
+ return (typeof v === 'number') ? v.toString().replace('.', '_') : formatter(v);
+ }
+
+ function mg_add_single_line_rollover(args, svg, rollover_on, rollover_off, rollover_move, rollover_click) {
+ // set to 1 unless we have a custom increment series
+ let line_id = 1;
+ if (args.custom_line_color_map.length > 0) {
+ line_id = args.custom_line_color_map[0];
+ }
+
+ const g = svg.append('g')
+ .attr('class', 'mg-rollover-rect');
+
+ const xf = args.data[0].map(args.scalefns.xf);
+
+ g.selectAll('.mg-rollover-rects')
+ .data(args.data[0]).enter()
+ .append('rect')
+ .attr('class', (d, i) => {
+ let cl = `${mg_line_color_class(line_id)} ${mg_line_class(d.__line_id__)}`;
+ if (args.linked) cl += `${cl} ${mg_rollover_id_class(mg_rollover_format_id(d, args))}`;
+ return cl;
+ })
+ .attr('x', (d, i) => {
+ // if data set is of length 1
+ if (xf.length === 1) return mg_get_plot_left(args);
+ else if (i === 0) return xf[i].toFixed(2);
+ else return ((xf[i - 1] + xf[i]) / 2).toFixed(2);
+ })
+ .attr('y', (d, i) => (args.data.length > 1) ? args.scalefns.yf(d) - 6 // multi-line chart sensitivity
+ : args.top)
+ .attr('width', (d, i) => {
+ // if data set is of length 1
+ if (xf.length === 1) return mg_get_plot_right(args);
+ else if (i === 0) return ((xf[i + 1] - xf[i]) / 2).toFixed(2);
+ else if (i === xf.length - 1) return ((xf[i] - xf[i - 1]) / 2).toFixed(2);
+ else return ((xf[i + 1] - xf[i - 1]) / 2).toFixed(2);
+ })
+ .attr('height', (d, i) => (args.data.length > 1) ? 12 // multi-line chart sensitivity
+ : args.height - args.bottom - args.top - args.buffer)
+ .attr('opacity', 0)
+ .on('click', rollover_click)
+ .on('mouseover', rollover_on)
+ .on('mouseout', rollover_off)
+ .on('mousemove', rollover_move);
+
+ if (mg_is_singleton(args)) {
+ mg_configure_singleton_rollover(args, svg);
+ }
+ }
+
+ function mg_configure_aggregate_rollover({data}, svg) {
+ const rect = svg.selectAll('.mg-rollover-rect rect');
+ const rect_first = rect.nodes()[0][0] || rect.nodes()[0];
+ if (data.filter(({length}) => length === 1).length > 0) {
+ rect.on('mouseover')(rect_first.__data__, 0);
+ }
+ }
+
+ function mg_is_standard_multiline({data, aggregate_rollover}) {
+ return data.length > 1 && !aggregate_rollover;
+ }
+
+ function mg_is_aggregated_rollover({data, aggregate_rollover}) {
+ return data.length > 1 && aggregate_rollover;
+ }
+
+ function mg_is_singleton({data}) {
+ return data.length === 1 && data[0].length === 1;
+ }
+
+ function mg_draw_all_line_elements(args, plot, svg) {
+ mg_remove_dangling_bands(plot, svg);
+
+ // If option activated, remove existing active points if exists
+ if (args.active_point_on_lines) {
+ svg.selectAll('circle.mg-shown-active-point').remove();
+ }
+
+ for (let i = args.data.length - 1; i >= 0; i--) {
+ const this_data = args.data[i];
+
+ // passing the data for the current line
+ MG.call_hook('line.before_each_series', [this_data, args]);
+
+ // override increment if we have a custom increment series
+ let line_id = i + 1;
+ if (args.custom_line_color_map.length > 0) {
+ line_id = args.custom_line_color_map[i];
+ }
+
+ args.data[i].__line_id__ = line_id;
+
+ // If option activated, add active points for each lines
+ if (args.active_point_on_lines) {
+ svg.selectAll('circle-' + line_id)
+ .data(args.data[i])
+ .enter()
+ .filter((d) => {
+ return d[args.active_point_accessor];
+ })
+ .append('circle')
+ .attr('class', 'mg-area' + (line_id) + '-color mg-shown-active-point')
+ .attr('cx', args.scalefns.xf)
+ .attr('cy', args.scalefns.yf)
+ .attr('r', () => {
+ return args.active_point_size;
+ });
+ }
+
+ const existing_line = svg.select(`path.mg-main-line.mg-line${line_id}`);
+ if (this_data.length === 0) {
+ existing_line.remove();
+ continue;
+ }
+
+ mg_add_confidence_band(args, plot, svg, line_id);
+
+ if (Array.isArray(args.area)) {
+ if (args.area[line_id - 1]) {
+ mg_add_area(args, plot, svg, i, line_id);
+ }
+ } else {
+ mg_add_area(args, plot, svg, i, line_id);
+ }
+
+ mg_add_line(args, plot, svg, existing_line, i, line_id);
+ mg_add_legend_element(args, plot, i, line_id);
+
+ // passing the data for the current line
+ MG.call_hook('line.after_each_series', [this_data, existing_line, args]);
+ }
+ }
+
+ function mg_remove_dangling_bands({existing_band}, svg) {
+ if (existing_band[0] && existing_band[0].length > svg.selectAll('.mg-main-line').node().length) {
+ svg.selectAll('.mg-confidence-band').remove();
+ }
+ }
+
+ function mg_line_main_plot(args) {
+ const plot = {};
+ const svg = mg_get_svg_child_of(args.target);
+
+ // remove any old legends if they exist
+ mg_selectAll_and_remove(svg, '.mg-line-legend');
+ mg_add_legend_group(args, plot, svg);
+
+ plot.data_median = 0;
+ plot.update_transition_duration = (args.transition_on_update) ? 1000 : 0;
+ plot.display_area = (args.area && !args.use_data_y_min && args.data.length <= 1 && args.aggregate_rollover === false) || (Array.isArray(args.area) && args.area.length > 0);
+ plot.legend_text = '';
+ mg_line_graph_generators(args, plot, svg);
+ plot.existing_band = svg.selectAll('.mg-confidence-band').nodes();
+
+ // should we continue with the default line render? A `line.all_series` hook should return false to prevent the default.
+ const continueWithDefault = MG.call_hook('line.before_all_series', [args]);
+ if (continueWithDefault !== false) {
+ mg_draw_all_line_elements(args, plot, svg);
+ }
+
+ mg_plot_legend_if_legend_target(args.legend_target, plot.legend_text);
+ }
+
+ function mg_line_rollover_setup(args, graph) {
+ const svg = mg_get_svg_child_of(args.target);
+
+ if (args.showActivePoint && svg.selectAll('.mg-active-datapoint-container').nodes().length === 0) {
+ mg_add_g(svg, 'mg-active-datapoint-container');
+ }
+
+ mg_remove_existing_line_rollover_elements(svg);
+ mg_add_rollover_circle(args, svg);
+ mg_set_unique_line_id_for_each_series(args);
+
+ if (mg_is_standard_multiline(args)) {
+ mg_add_voronoi_rollover(args, svg, graph.rolloverOn(args), graph.rolloverOff(args), graph.rolloverMove(args), graph.rolloverClick(args));
+ } else if (mg_is_aggregated_rollover(args)) {
+ mg_add_aggregate_rollover(args, svg, graph.rolloverOn(args), graph.rolloverOff(args), graph.rolloverMove(args), graph.rolloverClick(args));
+ } else {
+ mg_add_single_line_rollover(args, svg, graph.rolloverOn(args), graph.rolloverOff(args), graph.rolloverMove(args), graph.rolloverClick(args));
+ }
+ }
+
+ function mg_update_rollover_circle(args, svg, d) {
+ if (args.aggregate_rollover && args.data.length > 1) {
+ // hide the circles in case a non-contiguous series is present
+ svg.selectAll('circle.mg-line-rollover-circle')
+ .style('opacity', 0);
+
+ d.values.forEach((datum, index, list) => {
+ if (args.missing_is_hidden && list[index]['_missing']) {
+ return;
+ }
+
+ if (mg_data_in_plot_bounds(datum, args)) mg_update_aggregate_rollover_circle(args, svg, datum);
+ });
+ } else if ((args.missing_is_hidden && d['_missing']) || d[args.y_accessor] === null) {
+ // disable rollovers for hidden parts of the line
+ // recall that hidden parts are missing data ranges and possibly also
+ // data points that have been explicitly identified as missing
+ return;
+ } else {
+ // show circle on mouse-overed rect
+ if (mg_data_in_plot_bounds(d, args)) {
+ mg_update_generic_rollover_circle(args, svg, d);
+ }
+ }
+ }
+
+ function mg_update_aggregate_rollover_circle({scales, x_accessor, y_accessor, point_size}, svg, datum) {
+ svg.select(`circle.mg-line-rollover-circle.mg-line${datum.__line_id__}`)
+ .attr('cx', scales.X(datum[x_accessor]).toFixed(2))
+ .attr('cy', scales.Y(datum[y_accessor]).toFixed(2))
+ .attr('r', point_size)
+ .style('opacity', 1);
+ }
+
+ function mg_update_generic_rollover_circle({scales, x_accessor, y_accessor, point_size}, svg, d) {
+ svg.selectAll(`circle.mg-line-rollover-circle.mg-line${d.__line_id__}`)
+ .classed('mg-line-rollover-circle', true)
+ .attr('cx', () => scales.X(d[x_accessor]).toFixed(2))
+ .attr('cy', () => scales.Y(d[y_accessor]).toFixed(2))
+ .attr('r', point_size)
+ .style('opacity', 1);
+ }
+
+ function mg_trigger_linked_mouseovers(args, d, i) {
+ if (args.linked && !MG.globals.link) {
+ MG.globals.link = true;
+ if (!args.aggregate_rollover || d[args.y_accessor] !== undefined || (d.values && d.values.length > 0)) {
+ const datum = d.values ? d.values[0] : d;
+ const id = mg_rollover_format_id(datum, args);
+ // trigger mouseover on matching line in .linked charts
+ d3.selectAll(`.${mg_line_class(datum.__line_id__)}.${mg_rollover_id_class(id)}`)
+ .each(function(d) {
+ d3.select(this)
+ .on('mouseover')(d, i);
+ });
+ }
+ }
+ }
+
+ function mg_trigger_linked_mouseouts({linked, utc_time, linked_format, x_accessor}, d, i) {
+ if (linked && MG.globals.link) {
+ MG.globals.link = false;
+
+ const formatter = MG.time_format(utc_time, linked_format);
+ const datums = d.values ? d.values : [d];
+ datums.forEach(datum => {
+ const v = datum[x_accessor];
+ const id = (typeof v === 'number') ? i : formatter(v);
+
+ // trigger mouseout on matching line in .linked charts
+ d3.selectAll(`.roll_${id}`)
+ .each(function(d) {
+ d3.select(this)
+ .on('mouseout')(d);
+ });
+ });
+ }
+ }
+
+ function mg_remove_active_data_points_for_aggregate_rollover(args, svg) {
+ svg.selectAll('circle.mg-line-rollover-circle').filter(({length}) => length > 1)
+ .style('opacity', 0);
+ }
+
+ function mg_remove_active_data_points_for_generic_rollover({custom_line_color_map, data}, svg, line_id) {
+ svg.selectAll(`circle.mg-line-rollover-circle.mg-line${line_id}`)
+ .style('opacity', () => {
+ let id = line_id - 1;
+ if (custom_line_color_map.length > 0 &&
+ custom_line_color_map.indexOf(line_id) !== undefined
+ ) {
+ id = custom_line_color_map.indexOf(line_id);
+ }
+
+ if (data[id].length === 1) {
+ return 1;
+ } else {
+ return 0;
+ }
+ });
+ }
+
+ function mg_remove_active_text(svg) {
+ svg.select('.mg-active-datapoint').text('');
+ }
+
+ function lineChart(args) {
+ this.init = function(args) {
+ this.args = args;
+
+ if (!args.data || args.data.length === 0) {
+ args.internal_error = 'No data was supplied';
+ internal_error(args);
+ return this;
+ } else {
+ args.internal_error = undefined;
+ }
+
+ raw_data_transformation(args);
+ process_line(args);
+
+ MG.call_hook('line.before_destroy', this);
+
+ init(args);
+
+ // TODO incorporate markers into calculation of x scales
+ new MG.scale_factory(args)
+ .namespace('x')
+ .numericalDomainFromData()
+ .numericalRange('bottom');
+
+ const baselines = (args.baselines || []).map(d => d[args.y_accessor]);
+
+ new MG.scale_factory(args)
+ .namespace('y')
+ .zeroBottom(true)
+ .inflateDomain(true)
+ .numericalDomainFromData(baselines)
+ .numericalRange('left');
+
+ if (args.x_axis) {
+ new MG.axis_factory(args)
+ .namespace('x')
+ .type('numerical')
+ .position(args.x_axis_position)
+ .rug(x_rug(args))
+ .label(mg_add_x_label)
+ .draw();
+ }
+
+ if (args.y_axis) {
+ new MG.axis_factory(args)
+ .namespace('y')
+ .type('numerical')
+ .position(args.y_axis_position)
+ .rug(y_rug(args))
+ .label(mg_add_y_label)
+ .draw();
+ }
+
+ this.markers();
+ this.mainPlot();
+ this.rollover();
+ this.windowListeners();
+ if (args.brush) MG.add_brush_function(args);
+ MG.call_hook('line.after_init', this);
+
+ return this;
+ };
+
+ this.mainPlot = function() {
+ mg_line_main_plot(args);
+ return this;
+ };
+
+ this.markers = function() {
+ markers(args);
+ return this;
+ };
+
+ this.rollover = function() {
+ mg_line_rollover_setup(args, this);
+ MG.call_hook('line.after_rollover', args);
+
+ return this;
+ };
+
+ this.rolloverClick = args => (d, i) => {
+ if (args.click) {
+ args.click(d, i);
+ }
+ };
+
+ this.rolloverOn = args => {
+ const svg = mg_get_svg_child_of(args.target);
+
+ return (d, i) => {
+ mg_update_rollover_circle(args, svg, d);
+ mg_trigger_linked_mouseovers(args, d, i);
+
+ svg.selectAll('text')
+ .filter((g, j) => d === g)
+ .attr('opacity', 0.3);
+
+ // update rollover text except for missing data points
+ if (args.show_rollover_text &&
+ !((args.missing_is_hidden && d['_missing']) || d[args.y_accessor] === null)
+ ) {
+ const mouseover = mg_mouseover_text(args, { svg });
+ let row = mouseover.mouseover_row();
+ if (args.aggregate_rollover) {
+ row.text((args.aggregate_rollover && args.data.length > 1
+ ? mg_format_x_aggregate_mouseover
+ : mg_format_x_mouseover)(args, d));
+ }
+
+ const pts = args.aggregate_rollover && args.data.length > 1
+ ? d.values
+ : [d];
+
+ pts.forEach(di => {
+ if (args.aggregate_rollover) {
+ row = mouseover.mouseover_row();
+ }
+
+ if (args.legend) {
+ mg_line_color_text(row.text(`${args.legend[di.__index__ - 1]} `).bold(), di.__line_id__, args);
+ }
+
+ mg_line_color_text(row.text('\u2014 ').elem, di.__line_id__, args);
+ if (!args.aggregate_rollover) {
+ row.text(mg_format_x_mouseover(args, di));
+ }
+
+ row.text(mg_format_y_mouseover(args, di, args.time_series === false));
+ });
+ }
+
+ if (args.mouseover) {
+ args.mouseover(d, i);
+ }
+ };
+ };
+
+ this.rolloverOff = args => {
+ const svg = mg_get_svg_child_of(args.target);
+
+ return (d, i) => {
+ mg_trigger_linked_mouseouts(args, d, i);
+ if (args.aggregate_rollover) {
+ mg_remove_active_data_points_for_aggregate_rollover(args, svg);
+ } else {
+ mg_remove_active_data_points_for_generic_rollover(args, svg, d.__line_id__);
+ }
+
+ if (args.data[0].length > 1) {
+ mg_clear_mouseover_container(svg);
+ }
+
+ if (args.mouseout) {
+ args.mouseout(d, i);
+ }
+ };
+ };
+
+ this.rolloverMove = args => (d, i) => {
+ if (args.mousemove) {
+ args.mousemove(d, i);
+ }
+ };
+
+ this.windowListeners = function() {
+ mg_window_listeners(this.args);
+ return this;
+ };
+
+ this.init(args);
+ }
+
+ MG.register('line', lineChart);
+}
diff --git a/priv/static/js/metricsgraphics/charts/missing.js b/priv/static/js/metricsgraphics/charts/missing.js
new file mode 100644
index 0000000..330d5c0
--- /dev/null
+++ b/priv/static/js/metricsgraphics/charts/missing.js
@@ -0,0 +1,144 @@
+{
+ function mg_missing_add_text(svg, {missing_text, width, height}) {
+ svg.selectAll('.mg-missing-text').data([missing_text])
+ .enter().append('text')
+ .attr('class', 'mg-missing-text')
+ .attr('x', width / 2)
+ .attr('y', height / 2)
+ .attr('dy', '.50em')
+ .attr('text-anchor', 'middle')
+ .text(missing_text);
+ }
+
+ function mg_missing_x_scale(args) {
+ args.scales.X = d3.scaleLinear()
+ .domain([0, args.data.length])
+ .range([mg_get_plot_left(args), mg_get_plot_right(args)]);
+ args.scalefns.yf = ({y}) => args.scales.Y(y);
+ }
+
+ function mg_missing_y_scale(args) {
+ args.scales.Y = d3.scaleLinear()
+ .domain([-2, 2])
+ .range([args.height - args.bottom - args.buffer * 2, args.top]);
+ args.scalefns.xf = ({x}) => args.scales.X(x);
+ }
+
+ function mg_make_fake_data(args) {
+ const data = [];
+ for (let x = 1; x <= 50; x++) {
+ data.push({ x, y: Math.random() - (x * 0.03) });
+ }
+ args.data = data;
+ }
+
+ function mg_add_missing_background_rect(g, {title, buffer, title_y_position, width, height}) {
+ g.append('svg:rect')
+ .classed('mg-missing-background', true)
+ .attr('x', buffer)
+ .attr('y', buffer + (title ? title_y_position : 0) * 2)
+ .attr('width', width - buffer * 2)
+ .attr('height', height - buffer * 2 - (title ? title_y_position : 0) * 2)
+ .attr('rx', 15)
+ .attr('ry', 15);
+ }
+
+ function mg_missing_add_line(g, {scalefns, interpolate, data}) {
+ const line = d3.line()
+ .x(scalefns.xf)
+ .y(scalefns.yf)
+ .curve(interpolate);
+
+ g.append('path')
+ .attr('class', 'mg-main-line mg-line1-color')
+ .attr('d', line(data));
+ }
+
+ function mg_missing_add_area(g, {scalefns, scales, interpolate, data}) {
+ const area = d3.area()
+ .x(scalefns.xf)
+ .y0(scales.Y.range()[0])
+ .y1(scalefns.yf)
+ .curve(interpolate);
+
+ g.append('path')
+ .attr('class', 'mg-main-area mg-area1-color')
+ .attr('d', area(data));
+ }
+
+ function mg_remove_all_children({target}) {
+ d3.select(target).selectAll('svg *').remove();
+ }
+
+ function mg_missing_remove_legend({legend_target}) {
+ if (legend_target) {
+ d3.select(legend_target).html('');
+ }
+ }
+
+ function missingData(args) {
+ this.init = (args) => {
+ this.args = args;
+
+ mg_init_compute_width(args);
+ mg_init_compute_height(args);
+
+ // create svg if one doesn't exist
+
+ const container = d3.select(args.target);
+ mg_raise_container_error(container, args);
+ let svg = container.selectAll('svg');
+ mg_remove_svg_if_chart_type_has_changed(svg, args);
+ svg = mg_add_svg_if_it_doesnt_exist(svg, args);
+ mg_adjust_width_and_height_if_changed(svg, args);
+ mg_set_viewbox_for_scaling(svg, args);
+ mg_remove_all_children(args);
+
+ svg.classed('mg-missing', true);
+ mg_missing_remove_legend(args);
+
+ chart_title(args);
+
+ // are we adding a background placeholder
+ if (args.show_missing_background) {
+ mg_make_fake_data(args);
+ mg_missing_x_scale(args);
+ mg_missing_y_scale(args);
+ const g = mg_add_g(svg, 'mg-missing-pane');
+
+ mg_add_missing_background_rect(g, args);
+ mg_missing_add_line(g, args);
+ mg_missing_add_area(g, args);
+ }
+
+ mg_missing_add_text(svg, args);
+
+ this.windowListeners();
+
+ return this;
+ };
+
+ this.windowListeners = () => {
+ mg_window_listeners(this.args);
+ return this;
+ };
+
+ this.init(args);
+ }
+
+ const defaults = {
+ top: [40, 'number'], // the size of the top margin
+ bottom: [30, 'number'], // the size of the bottom margin
+ right: [10, 'number'], // size of the right margin
+ left: [0, 'number'], // size of the left margin
+ buffer: [8, 'number'], // the buffer between the actual chart area and the margins
+ legend_target: ['', 'string'],
+ width: [350, 'number'],
+ height: [220, 'number'],
+ missing_text: ['Data currently missing or unavailable', 'string'],
+ show_tooltips: [true, 'boolean'],
+ show_missing_background: [true, 'boolean']
+ };
+
+ MG.register('missing-data', missingData, defaults);
+}
diff --git a/priv/static/js/metricsgraphics/charts/point.js b/priv/static/js/metricsgraphics/charts/point.js
new file mode 100644
index 0000000..b511f19
--- /dev/null
+++ b/priv/static/js/metricsgraphics/charts/point.js
@@ -0,0 +1,383 @@
+function point_mouseover(args, svg, d) {
+ const mouseover = mg_mouseover_text(args, { svg });
+ const row = mouseover.mouseover_row();
+
+ if (args.color_accessor !== null && args.color_type === 'category') {
+ const label = d[args.color_accessor];
+ row.text(`${label} `).bold().attr('fill', args.scalefns.colorf(d));
+ }
+
+ mg_color_point_mouseover(args, row.text('\u25CF ').elem, d); // point shape
+
+ row.text(mg_format_x_mouseover(args, d)); // x
+ row.text(mg_format_y_mouseover(args, d, args.time_series === false));
+}
+
+function mg_color_point_mouseover({color_accessor, scalefns}, elem, d) {
+ if (color_accessor !== null) {
+ elem.attr('fill', scalefns.colorf(d));
+ elem.attr('stroke', scalefns.colorf(d));
+ } else {
+ elem.classed('mg-points-mono', true);
+ }
+}
+
+
+{
+ function mg_filter_out_plot_bounds(data, args) {
+ // max_x, min_x, max_y, min_y;
+ const x = args.x_accessor;
+ const y = args.y_accessor;
+ const new_data = data.filter(d => (args.min_x === null || d[x] >= args.min_x) &&
+ (args.max_x === null || d[x] <= args.max_x) &&
+ (args.min_y === null || d[y] >= args.min_y) &&
+ (args.max_y === null || d[y] <= args.max_y));
+ return new_data;
+ }
+
+ function pointChart(args) {
+ this.init = function(args) {
+ this.args = args;
+
+ // infer y_axis and x_axis type;
+ args.x_axis_type = mg_infer_type(args, 'x');
+ args.y_axis_type = mg_infer_type(args, 'y');
+
+ raw_data_transformation(args);
+
+ process_point(args);
+ init(args);
+
+ let xMaker, yMaker;
+
+ if (args.x_axis_type === 'categorical') {
+ xMaker = MG.scale_factory(args)
+ .namespace('x')
+ .categoricalDomainFromData()
+ .categoricalRangeBands([0, args.xgroup_height], args.xgroup_accessor === null);
+
+ if (args.xgroup_accessor) {
+ new MG.scale_factory(args)
+ .namespace('xgroup')
+ .categoricalDomainFromData()
+ .categoricalRangeBands('bottom');
+
+ } else {
+ args.scales.XGROUP = () => mg_get_plot_left(args);
+ args.scalefns.xgroupf = () => mg_get_plot_left(args);
+ }
+
+ args.scalefns.xoutf = d => args.scalefns.xf(d) + args.scalefns.xgroupf(d);
+ } else {
+ xMaker = MG.scale_factory(args)
+ .namespace('x')
+ .inflateDomain(true)
+ .zeroBottom(args.y_axis_type === 'categorical')
+ .numericalDomainFromData((args.baselines || []).map(d => d[args.x_accessor]))
+ .numericalRange('bottom');
+
+ args.scalefns.xoutf = args.scalefns.xf;
+ }
+
+ // y-scale generation. This needs to get simplified.
+ if (args.y_axis_type === 'categorical') {
+ yMaker = MG.scale_factory(args)
+ .namespace('y')
+ .zeroBottom(true)
+ .categoricalDomainFromData()
+ .categoricalRangeBands([0, args.ygroup_height], true);
+
+ if (args.ygroup_accessor) {
+
+ new MG.scale_factory(args)
+ .namespace('ygroup')
+ .categoricalDomainFromData()
+ .categoricalRangeBands('left');
+
+ } else {
+ args.scales.YGROUP = () => mg_get_plot_top(args);
+ args.scalefns.ygroupf = () => mg_get_plot_top(args);
+
+ }
+ args.scalefns.youtf = d => args.scalefns.yf(d) + args.scalefns.ygroupf(d);
+
+ } else {
+ const baselines = (args.baselines || []).map(d => d[args.y_accessor]);
+ yMaker = MG.scale_factory(args)
+ .namespace('y')
+ .inflateDomain(true)
+ .zeroBottom(args.x_axis_type === 'categorical')
+ .numericalDomainFromData(baselines)
+ .numericalRange('left');
+
+ args.scalefns.youtf = d => args.scalefns.yf(d);
+ }
+
+ /////// COLOR accessor
+ if (args.color_accessor !== null) {
+ const colorScale = MG.scale_factory(args).namespace('color');
+ if (args.color_type === 'number') {
+ // do the color scale.
+ // etiher get color range, or what.
+ colorScale
+ .numericalDomainFromData(mg_get_color_domain(args))
+ .numericalRange(mg_get_color_range(args))
+ .clamp(true);
+ } else {
+ if (args.color_domain) {
+ colorScale
+ .categoricalDomain(args.color_domain)
+ .categoricalRange(args.color_range);
+ } else {
+ colorScale
+ .categoricalDomainFromData()
+ .categoricalColorRange();
+ }
+ }
+ }
+
+ if (args.size_accessor) {
+ new MG.scale_factory(args).namespace('size')
+ .numericalDomainFromData()
+ .numericalRange(mg_get_size_range(args))
+ .clamp(true);
+ }
+
+ new MG.axis_factory(args)
+ .namespace('x')
+ .type(args.x_axis_type)
+ .zeroLine(args.y_axis_type === 'categorical')
+ .position(args.x_axis_position)
+ .rug(x_rug(args))
+ .label(mg_add_x_label)
+ .draw();
+
+ new MG.axis_factory(args)
+ .namespace('y')
+ .type(args.y_axis_type)
+ .zeroLine(args.x_axis_type === 'categorical')
+ .position(args.y_axis_position)
+ .rug(y_rug(args))
+ .label(mg_add_y_label)
+ .draw();
+
+ this.mainPlot();
+ this.markers();
+ this.rollover();
+ this.windowListeners();
+ if (args.brush) MG.add_brush_function(args);
+ return this;
+ };
+
+ this.markers = function() {
+ markers(args);
+ if (args.least_squares) {
+ add_ls(args);
+ }
+
+ return this;
+ };
+
+ this.mainPlot = function() {
+ const svg = mg_get_svg_child_of(args.target);
+
+ const data = mg_filter_out_plot_bounds(args.data[0], args);
+ //remove the old points, add new one
+ svg.selectAll('.mg-points').remove();
+
+ const g = svg.append('g')
+ .classed('mg-points', true);
+
+ const pts = g.selectAll('circle')
+ .data(data)
+ .enter().append('circle')
+ .attr('class', (d, i) => `path-${i}`)
+ .attr('cx', args.scalefns.xoutf)
+ .attr('cy', d => args.scalefns.youtf(d));
+
+ let highlights;
+ svg.selectAll('.mg-highlight').remove();
+ if (args.highlight && mg_is_function(args.highlight)) {
+ highlights = svg.append('g')
+ .classed('mg-highlight', true)
+ .selectAll('circle')
+ .data(data.filter(args.highlight))
+ .enter().append('circle')
+ .attr('cx', args.scalefns.xoutf)
+ .attr('cy', d => args.scalefns.youtf(d));
+ }
+
+ const elements = [pts].concat(highlights ? [highlights] : []);
+ //are we coloring our points, or just using the default color?
+ if (args.color_accessor !== null) {
+ elements.forEach(e => e.attr('fill', args.scalefns.colorf).attr('stroke', args.scalefns.colorf));
+ } else {
+ elements.forEach(e => e.classed('mg-points-mono', true));
+ }
+
+ pts.attr('r', (args.size_accessor !== null) ? args.scalefns.sizef : args.point_size);
+ if (highlights) {
+ highlights.attr('r', (args.size_accessor !== null) ? (d, i) => args.scalefns.sizef(d, i) + 2 : args.point_size + 2);
+ }
+
+ return this;
+ };
+
+ this.rollover = function() {
+ const svg = mg_get_svg_child_of(args.target);
+
+ if (svg.selectAll('.mg-active-datapoint-container').nodes().length === 0) {
+ mg_add_g(svg, 'mg-active-datapoint-container');
+ }
+
+ //remove the old rollovers if they already exist
+ svg.selectAll('.mg-voronoi').remove();
+
+ //add rollover paths
+ const voronoi = d3.voronoi()
+ .x(args.scalefns.xoutf)
+ .y(args.scalefns.youtf)
+ .extent([
+ [args.buffer, args.buffer + (args.title ? args.title_y_position : 0)],
+ [args.width - args.buffer, args.height - args.buffer]
+ ]);
+
+ const paths = svg.append('g')
+ .attr('class', 'mg-voronoi');
+
+ paths.selectAll('path')
+ .data(voronoi.polygons(mg_filter_out_plot_bounds(args.data[0], args)))
+ .enter().append('path')
+ .attr('d', d => d == null ? null : `M${d.join(',')}Z`)
+ .attr('class', (d, i) => `path-${i}`)
+ .style('fill-opacity', 0)
+ .on('click', this.rolloverClick(args))
+ .on('mouseover', this.rolloverOn(args))
+ .on('mouseout', this.rolloverOff(args))
+ .on('mousemove', this.rolloverMove(args));
+
+ if (args.data[0].length === 1) {
+ point_mouseover(args, svg, args.data[0][0]);
+ }
+
+ return this;
+ };
+
+ this.rolloverClick = args => {
+ return (d, i) => {
+ if (args.click) {
+ args.click(d, i);
+ }
+ };
+ };
+
+ this.rolloverOn = args => {
+ const svg = mg_get_svg_child_of(args.target);
+
+ return (d, i) => {
+ svg.selectAll('.mg-points circle')
+ .classed('selected', false);
+
+ //highlight active point
+ const pts = svg.selectAll(`.mg-points circle.path-${i}`)
+ .classed('selected', true);
+
+ if (args.size_accessor) {
+ pts.attr('r', di => args.scalefns.sizef(di) + args.active_point_size_increase);
+ } else {
+ pts.attr('r', args.point_size + args.active_point_size_increase);
+ }
+
+ //trigger mouseover on all points for this class name in .linked charts
+ if (args.linked && !MG.globals.link) {
+ MG.globals.link = true;
+
+ //trigger mouseover on matching point in .linked charts
+ d3.selectAll(`.mg-voronoi .path-${i}`)
+ .each(() => {
+ d3.select(this).on('mouseover')(d, i);
+ });
+ }
+
+ if (args.show_rollover_text) {
+ point_mouseover(args, svg, d.data);
+ }
+
+ if (args.mouseover) {
+ args.mouseover(d, i);
+ }
+ };
+ };
+
+ this.rolloverOff = args => {
+ const svg = mg_get_svg_child_of(args.target);
+
+ return (d, i) => {
+ if (args.linked && MG.globals.link) {
+ MG.globals.link = false;
+
+ d3.selectAll(`.mg-voronoi .path-${i}`)
+ .each(() => {
+ d3.select(this).on('mouseout')(d, i);
+ });
+ }
+
+ //reset active point
+ const pts = svg.selectAll('.mg-points circle')
+ .classed('unselected', false)
+ .classed('selected', false);
+
+ if (args.size_accessor) {
+ pts.attr('r', args.scalefns.sizef);
+ } else {
+ pts.attr('r', args.point_size);
+ }
+
+ //reset active data point text
+ if (args.data[0].length > 1) mg_clear_mouseover_container(svg);
+
+ if (args.mouseout) {
+ args.mouseout(d, i);
+ }
+ };
+ };
+
+ this.rolloverMove = args => (d, i) => {
+ if (args.mousemove) {
+ args.mousemove(d, i);
+ }
+ };
+
+ this.update = function(args) {
+ return this;
+ };
+
+ this.windowListeners = function() {
+ mg_window_listeners(this.args);
+ return this;
+ };
+
+ this.init(args);
+ }
+
+ const options = {
+ color_accessor: [null, 'string'], // the data element to use to map points to colors
+ color_range: [null, 'array'], // the range used to color different groups of points
+ color_type: ['number', ['number', 'category']], // specifies whether the color scale is quantitative or qualitative
+ point_size: [2.5, 'number'], // the radius of the dots in the scatterplot
+ size_accessor: [null, 'string'], // should point sizes be mapped to data
+ size_range: [null, 'array'], // the range of point sizes
+ lowess: [false, 'boolean'], // specifies whether to show a lowess line of best-fit
+ least_squares: [false, 'boolean'], // specifies whether to show a least-squares line of best-fit
+ y_categorical_show_guides: [true, 'boolean'],
+ x_categorical_show_guides: [true, 'boolean'],
+ buffer: [16, 'string'],
+ label_accessor: [null, 'boolean'],
+ size_domain: [null, 'array'],
+ color_domain: [null, 'array'],
+ active_point_size_increase: [1, 'number'],
+ highlight: [null, 'function'] // if this callback function returns true, the selected point will be highlighted
+ };
+
+ MG.register('point', pointChart, options);
+}
diff --git a/priv/static/js/metricsgraphics/charts/table.js b/priv/static/js/metricsgraphics/charts/table.js
new file mode 100644
index 0000000..3081d8c
--- /dev/null
+++ b/priv/static/js/metricsgraphics/charts/table.js
@@ -0,0 +1,220 @@
+/*
+Data Tables
+
+Along with histograms, bars, lines, and scatters, a simple data table can take you far.
+We often just want to look at numbers, organized as a table, where columns are variables,
+and rows are data points. Sometimes we want a cell to have a small graphic as the main
+column element, in which case we want small multiples. sometimes we want to
+
+var table = New data_table(data)
+ .target('div#data-table')
+ .title({accessor: 'point_name', align: 'left'})
+ .description({accessor: 'description'})
+ .number({accessor: ''})
+
+*/
+
+MG.data_table = function(args) {
+ 'use strict';
+ this.args = args;
+ this.args.standard_col = { width: 150, font_size: 12, font_weight: 'normal' };
+ this.args.columns = [];
+ this.formatting_options = [
+ ['color', 'color'],
+ ['font-weight', 'font_weight'],
+ ['font-style', 'font_style'],
+ ['font-size', 'font_size']
+ ];
+
+ this._strip_punctuation = function(s) {
+ var punctuationless = s.replace(/[^a-zA-Z0-9 _]+/g, '');
+ var finalString = punctuationless.replace(/ +?/g, '');
+ return finalString;
+ };
+
+ this._format_element = function(element, value, args) {
+ this.formatting_options.forEach(function(fo) {
+ var attr = fo[0];
+ var key = fo[1];
+ if (args[key]) element.style(attr,
+ typeof args[key] === 'string' ||
+ typeof args[key] === 'number' ?
+ args[key] : args[key](value));
+ });
+ };
+
+ this._add_column = function(_args, arg_type) {
+ var standard_column = this.args.standard_col;
+ var args = merge_with_defaults(MG.clone(_args), MG.clone(standard_column));
+ args.type = arg_type;
+ this.args.columns.push(args);
+ };
+
+ this.target = function() {
+ var target = arguments[0];
+ this.args.target = target;
+ return this;
+ };
+
+ this.title = function() {
+ this._add_column(arguments[0], 'title');
+ return this;
+ };
+
+ this.text = function() {
+ this._add_column(arguments[0], 'text');
+ return this;
+ };
+
+ this.bullet = function() {
+ /*
+ text label
+ main value
+ comparative measure
+ any number of ranges
+
+ additional args:
+ no title
+ xmin, xmax
+ format: percentage
+ xax_formatter
+ */
+ return this;
+ };
+
+ this.sparkline = function() {
+ return this;
+ };
+
+ this.number = function() {
+ this._add_column(arguments[0], 'number');
+ return this;
+ };
+
+ this.display = function() {
+ var args = this.args;
+
+ chart_title(args);
+
+ var target = args.target;
+ var table = d3.select(target).append('table').classed('mg-data-table', true);
+ var colgroup = table.append('colgroup');
+ var thead = table.append('thead');
+ var tbody = table.append('tbody');
+ var this_column;
+ var this_title;
+
+ var tr, th, td_accessor, td_type, td_value, th_text, td_text, td;
+ var col;
+ var h;
+
+ tr = thead.append('tr');
+
+ for (h = 0; h < args.columns.length; h++) {
+ var this_col = args.columns[h];
+ td_type = this_col.type;
+ th_text = this_col.label;
+ th_text = th_text === undefined ? '' : th_text;
+ th = tr.append('th')
+ .style('width', this_col.width)
+ .style('text-align', td_type === 'title' ? 'left' : 'right')
+ .text(th_text);
+
+ if (args.show_tooltips && this_col.description && mg_jquery_exists()) {
+ th.append('i')
+ .classed('fa', true)
+ .classed('fa-question-circle', true)
+ .classed('fa-inverse', true);
+
+ $(th.node()).popover({
+ html: true,
+ animation: false,
+ content: this_col.description,
+ trigger: 'hover',
+ placement: 'top',
+ container: $(th.node())
+ });
+ }
+ }
+
+ for (h = 0; h < args.columns.length; h++) {
+ col = colgroup.append('col');
+ if (args.columns[h].type === 'number') {
+ col.attr('align', 'char').attr('char', '.');
+ }
+ }
+
+ for (var i = 0; i < args.data.length; i++) {
+ tr = tbody.append('tr');
+ for (var j = 0; j < args.columns.length; j++) {
+ this_column = args.columns[j];
+ td_accessor = this_column.accessor;
+ td_value = td_text = args.data[i][td_accessor];
+ td_type = this_column.type;
+
+ if (td_type === 'number') {
+ //td_text may need to be rounded
+ if (this_column.hasOwnProperty('round') && !this_column.hasOwnProperty('format')) {
+ // round according to the number value in this_column.round
+ td_text = d3.format('0,.' + this_column.round + 'f')(td_text);
+ }
+
+ if (this_column.hasOwnProperty('value_formatter')) {
+ // provide a function that formats the text according to the function this_column.format.
+ td_text = this_column.value_formatter(td_text);
+ }
+
+ if (this_column.hasOwnProperty('format')) {
+ // this is a shorthand for percentage formatting, and others if need be.
+ // supported: 'percentage', 'count', 'temperature'
+
+ if (this_column.round) {
+ td_text = Math.round(td_text, this_column.round);
+ }
+
+ var this_format = this_column.format;
+ var formatter;
+
+ if (this_format === 'percentage') formatter = d3.format('.0%');
+ if (this_format === 'count') formatter = d3.format(',.0f');
+ if (this_format === 'temperature') formatter = function(t) {
+ return t + '°'; };
+
+ td_text = formatter(td_text);
+ }
+
+ if (this_column.hasOwnProperty('currency')) {
+ // this is another shorthand for formatting according to a currency amount, which gets appended to front of number
+ td_text = this_column.currency + td_text;
+ }
+ }
+
+ td = tr.append('td')
+ .classed('table-' + td_type, true)
+ .classed('table-' + td_type + '-' + this._strip_punctuation(td_accessor), true)
+ .attr('data-value', td_value)
+ .style('width', this_column.width)
+ .style('text-align', td_type === 'title' || td_type === 'text' ? 'left' : 'right');
+
+ this._format_element(td, td_value, this_column);
+
+ if (td_type === 'title') {
+ this_title = td.append('div').text(td_text);
+ this._format_element(this_title, td_text, this_column);
+
+ if (args.columns[j].hasOwnProperty('secondary_accessor')) {
+ td.append('div')
+ .text(args.data[i][args.columns[j].secondary_accessor])
+ .classed("secondary-title", true);
+ }
+ } else {
+ td.text(td_text);
+ }
+ }
+ }
+
+ return this;
+ };
+
+ return this;
+};