summaryrefslogtreecommitdiff
path: root/priv/static/js/metricsgraphics/charts/point.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--priv/static/js/metricsgraphics/charts/point.js383
1 files changed, 383 insertions, 0 deletions
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);
+}