diff options
Diffstat (limited to 'priv/static/js/metricsgraphics/charts/line.js')
-rw-r--r-- | priv/static/js/metricsgraphics/charts/line.js | 922 |
1 files changed, 922 insertions, 0 deletions
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]}'>— ${this_legend} </span>${plot.legend_text}`; + } else { + plot.legend_text = `<span class='mg-line${line_id}-legend-color'>— ${this_legend} </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); +} |