diff options
Diffstat (limited to '')
-rw-r--r-- | priv/static/js/metricsgraphics/charts/point.js | 383 |
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); +} |