(function(root, factory) { if (typeof define === 'function' && define.amd) { define(['d3'], factory); } else if (typeof exports === 'object') { module.exports = factory(require('d3')); } else { root.MG = factory(root.d3); } }(this, function(d3) { 'use strict'; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; (typeof window === 'undefined' ? global : window).MG = { version: '2.11' }; //a set of helper functions, some that we've written, others that we've borrowed MG.convert = {}; MG.convert.date = function (data, accessor, time_format) { time_format = typeof time_format === "undefined" ? '%Y-%m-%d' : time_format; var parse_time = d3.timeParse(time_format); data = data.map(function (d) { d[accessor] = parse_time(d[accessor].trim()); return d; }); return data; }; MG.convert.number = function (data, accessor) { data = data.map(function (d) { d[accessor] = Number(d[accessor]); return d; }); return data; }; MG.time_format = function (utc, specifier) { return utc ? d3.utcFormat(specifier) : d3.timeFormat(specifier); }; function mg_jquery_exists() { if (typeof jQuery !== 'undefined' || typeof $ !== 'undefined') { return true; } else { return false; } } function mg_get_rollover_time_format(args) { // if a rollover time format is defined, use that if (args.rollover_time_format) { return MG.time_format(args.utc_time, args.rollover_time_format); } switch (args.processed.x_time_frame) { case 'millis': return MG.time_format(args.utc_time, '%b %e, %Y %H:%M:%S.%L'); case 'seconds': return MG.time_format(args.utc_time, '%b %e, %Y %H:%M:%S'); case 'less-than-a-day': return MG.time_format(args.utc_time, '%b %e, %Y %I:%M%p'); case 'four-days': return MG.time_format(args.utc_time, '%b %e, %Y %I:%M%p'); } // default return MG.time_format(args.utc_time, '%b %e, %Y'); } function mg_data_in_plot_bounds(datum, args) { return datum[args.x_accessor] >= args.processed.min_x && datum[args.x_accessor] <= args.processed.max_x && datum[args.y_accessor] >= args.processed.min_y && datum[args.y_accessor] <= args.processed.max_y; } function is_array(thing) { return Object.prototype.toString.call(thing) === '[object Array]'; } function is_function(thing) { return Object.prototype.toString.call(thing) === '[object Function]'; } function is_empty_array(thing) { return is_array(thing) && thing.length === 0; } function is_object(thing) { return Object.prototype.toString.call(thing) === '[object Object]'; } function is_array_of_arrays(data) { var all_elements = data.map(function (d) { return is_array(d) === true && d.length > 0; }); return d3.sum(all_elements) === data.length; } function is_array_of_objects(data) { // is every element of data an object? var all_elements = data.map(function (d) { return is_object(d) === true; }); return d3.sum(all_elements) === data.length; } function is_array_of_objects_or_empty(data) { return is_empty_array(data) || is_array_of_objects(data); } function pluck(arr, accessor) { return arr.map(function (d) { return d[accessor]; }); } function count_array_elements(arr) { return arr.reduce(function (a, b) { a[b] = a[b] + 1 || 1; return a; }, {}); } function mg_get_bottom(args) { return args.height - args.bottom; } function mg_get_plot_bottom(args) { // returns the pixel location of the bottom side of the plot area. return mg_get_bottom(args) - args.buffer; } function mg_get_top(args) { return args.top; } function mg_get_plot_top(args) { // returns the pixel location of the top side of the plot area. return mg_get_top(args) + args.buffer; } function mg_get_left(args) { return args.left; } function mg_get_plot_left(args) { // returns the pixel location of the left side of the plot area. return mg_get_left(args) + args.buffer; } function mg_get_right(args) { return args.width - args.right; } function mg_get_plot_right(args) { // returns the pixel location of the right side of the plot area. return mg_get_right(args) - args.buffer; } //////// adding elements, removing elements ///////////// function mg_exit_and_remove(elem) { elem.exit().remove(); } function mg_selectAll_and_remove(svg, cl) { svg.selectAll(cl).remove(); } function mg_add_g(svg, cl) { return svg.append('g').classed(cl, true); } function mg_remove_element(svg, elem) { svg.select(elem).remove(); } //////// axis helper functions //////////// function mg_make_rug(args, rug_class) { var svg = mg_get_svg_child_of(args.target); var all_data = mg_flatten_array(args.data); var rug = svg.selectAll('line.' + rug_class).data(all_data); rug.enter().append('line').attr('class', rug_class).attr('opacity', 0.3); //remove rug elements that are no longer in use mg_exit_and_remove(rug); //set coordinates of new rug elements mg_exit_and_remove(rug); return rug; } function mg_add_color_accessor_to_rug(rug, args, rug_mono_class) { if (args.color_accessor) { rug.attr('stroke', args.scalefns.colorf); rug.classed(rug_mono_class, false); } else { rug.attr('stroke', null); rug.classed(rug_mono_class, true); } } function mg_rotate_labels(labels, rotation_degree) { if (rotation_degree) { labels.attr({ dy: 0, transform: function transform() { var elem = d3.select(this); return 'rotate(' + rotation_degree + ' ' + elem.attr('x') + ',' + elem.attr('y') + ')'; } }); } } ////////////////////////////////////////////////// function mg_elements_are_overlapping(labels) { labels = labels.node(); if (!labels) { return false; } for (var i = 0; i < labels.length; i++) { if (mg_is_horizontally_overlapping(labels[i], labels)) return true; } return false; } function mg_prevent_horizontal_overlap(labels, args) { if (!labels || labels.length == 1) { return; } //see if each of our labels overlaps any of the other labels for (var i = 0; i < labels.length; i++) { //if so, nudge it up a bit, if the label it intersects hasn't already been nudged if (mg_is_horizontally_overlapping(labels[i], labels)) { var node = d3.select(labels[i]); var newY = +node.attr('y'); if (newY + 8 >= args.top) { newY = args.top - 16; } node.attr('y', newY); } } } function mg_prevent_vertical_overlap(labels, args) { if (!labels || labels.length == 1) { return; } labels.sort(function (b, a) { return d3.select(a).attr('y') - d3.select(b).attr('y'); }); labels.reverse(); var overlap_amount, label_i, label_j; //see if each of our labels overlaps any of the other labels for (var i = 0; i < labels.length; i++) { //if so, nudge it up a bit, if the label it intersects hasn't already been nudged label_i = d3.select(labels[i]).text(); for (var j = 0; j < labels.length; j++) { label_j = d3.select(labels[j]).text(); overlap_amount = mg_is_vertically_overlapping(labels[i], labels[j]); if (overlap_amount !== false && label_i !== label_j) { var node = d3.select(labels[i]); var newY = +node.attr('y'); newY = newY + overlap_amount; node.attr('y', newY); } } } } function mg_is_vertically_overlapping(element, sibling) { var element_bbox = element.getBoundingClientRect(); var sibling_bbox = sibling.getBoundingClientRect(); if (element_bbox.top <= sibling_bbox.bottom && element_bbox.top >= sibling_bbox.top) { return sibling_bbox.bottom - element_bbox.top; } return false; } function mg_is_horiz_overlap(element, sibling) { var element_bbox = element.getBoundingClientRect(); var sibling_bbox = sibling.getBoundingClientRect(); if (element_bbox.right >= sibling_bbox.left || element_bbox.top >= sibling_bbox.top) { return sibling_bbox.bottom - element_bbox.top; } return false; } function mg_is_horizontally_overlapping(element, labels) { var element_bbox = element.getBoundingClientRect(); for (var i = 0; i < labels.length; i++) { if (labels[i] == element) { continue; } //check to see if this label overlaps with any of the other labels var sibling_bbox = labels[i].getBoundingClientRect(); if (element_bbox.top === sibling_bbox.top && !(sibling_bbox.left > element_bbox.right || sibling_bbox.right < element_bbox.left)) { return true; } } return false; } function mg_infer_type(args, ns) { // must return categorical or numerical. var testPoint = mg_flatten_array(args.data); testPoint = testPoint[0][args[ns + '_accessor']]; return typeof testPoint === 'string' ? 'categorical' : 'numerical'; } function mg_get_svg_child_of(selector_or_node) { return d3.select(selector_or_node).select('svg'); } function mg_flatten_array(arr) { var flat_data = []; return flat_data.concat.apply(flat_data, arr); } function mg_next_id() { if (typeof MG._next_elem_id === 'undefined') { MG._next_elem_id = 0; } return 'mg-' + MG._next_elem_id++; } function mg_target_ref(target) { if (typeof target === 'string') { return mg_normalize(target); } else if (target instanceof window.HTMLElement) { var target_ref = target.getAttribute('data-mg-uid'); if (!target_ref) { target_ref = mg_next_id(); target.setAttribute('data-mg-uid', target_ref); } return target_ref; } else { console.warn('The specified target should be a string or an HTMLElement.', target); return mg_normalize(target); } } function mg_normalize(string) { return string.replace(/[^a-zA-Z0-9 _-]+/g, '').replace(/ +?/g, ''); } function get_pixel_dimension(target, dimension) { return Number(d3.select(target).style(dimension).replace(/px/g, '')); } function get_width(target) { return get_pixel_dimension(target, 'width'); } function get_height(target) { return get_pixel_dimension(target, 'height'); } function isNumeric(n) { return !isNaN(parseFloat(n)) && isFinite(n); } var each = function each(obj, iterator, context) { // yanked out of underscore var breaker = {}; if (obj === null) return obj; if (Array.prototype.forEach && obj.forEach === Array.prototype.forEach) { obj.forEach(iterator, context); } else if (obj.length === +obj.length) { for (var i = 0, length = obj.length; i < length; i++) { if (iterator.call(context, obj[i], i, obj) === breaker) return; } } else { for (var k in obj) { if (iterator.call(context, obj[k], k, obj) === breaker) return; } } return obj; }; function merge_with_defaults(obj) { // taken from underscore each(Array.prototype.slice.call(arguments, 1), function (source) { if (source) { for (var prop in source) { if (obj[prop] === void 0) obj[prop] = source[prop]; } } }); return obj; } MG.merge_with_defaults = merge_with_defaults; function number_of_values(data, accessor, value) { var values = data.filter(function (d) { return d[accessor] === value; }); return values.length; } function has_values_below(data, accessor, value) { var values = data.filter(function (d) { return d[accessor] <= value; }); return values.length > 0; } function has_too_many_zeros(data, accessor, zero_count) { return number_of_values(data, accessor, 0) >= zero_count; } function mg_is_date(obj) { return Object.prototype.toString.call(obj) === '[object Date]'; } function mg_is_object(obj) { return Object.prototype.toString.call(obj) === '[object Object]'; } function mg_is_array(obj) { if (Array.isArray) { return Array.isArray(obj); } return Object.prototype.toString.call(obj) === '[object Array]'; } function mg_is_function(obj) { return Object.prototype.toString.call(obj) === '[object Function]'; } // deep copy // http://stackoverflow.com/questions/728360/most-elegant-way-to-clone-a-javascript-object MG.clone = function (obj) { var copy; // Handle the 3 simple types, and null or undefined if (null === obj || "object" !== (typeof obj === 'undefined' ? 'undefined' : _typeof(obj))) return obj; // Handle Date if (mg_is_date(obj)) { copy = new Date(); copy.setTime(obj.getTime()); return copy; } // Handle Array if (mg_is_array(obj)) { copy = []; for (var i = 0, len = obj.length; i < len; i++) { copy[i] = MG.clone(obj[i]); } return copy; } // Handle Object if (mg_is_object(obj)) { copy = {}; for (var attr in obj) { if (obj.hasOwnProperty(attr)) copy[attr] = MG.clone(obj[attr]); } return copy; } throw new Error("Unable to copy obj! Its type isn't supported."); }; // give us the difference of two int arrays // http://radu.cotescu.com/javascript-diff-function/ function arr_diff(a, b) { var seen = [], diff = [], i; for (i = 0; i < b.length; i++) { seen[b[i]] = true; }for (i = 0; i < a.length; i++) { if (!seen[a[i]]) diff.push(a[i]); }return diff; } MG.arr_diff = arr_diff; /** Print warning message to the console when a feature has been scheduled for removal @author Dan de Havilland (github.com/dandehavilland) @date 2014-12 */ function warn_deprecation(message, untilVersion) { console.warn('Deprecation: ' + message + (untilVersion ? '. This feature will be removed in ' + untilVersion + '.' : ' the near future.')); console.trace(); } MG.warn_deprecation = warn_deprecation; /** Truncate a string to fit within an SVG text node CSS text-overlow doesn't apply to SVG <= 1.2 @author Dan de Havilland (github.com/dandehavilland) @date 2014-12-02 */ function truncate_text(textObj, textString, width) { var bbox, position = 0; textObj.textContent = textString; bbox = textObj.getBBox(); while (bbox.width > width) { textObj.textContent = textString.slice(0, --position) + '...'; bbox = textObj.getBBox(); if (textObj.textContent === '...') { break; } } } MG.truncate_text = truncate_text; /** Wrap the contents of a text node to a specific width Adapted from bl.ocks.org/mbostock/7555321 @author Mike Bostock @author Dan de Havilland @date 2015-01-14 */ function wrap_text(text, width, token, tspanAttrs) { text.each(function () { var text = d3.select(this), words = text.text().split(token || /\s+/).reverse(), word, line = [], lineNumber = 0, lineHeight = 1.1, // ems y = text.attr("y"), dy = 0, tspan = text.text(null).append("tspan").attr("x", 0).attr("y", dy + "em").attr(tspanAttrs || {}); while (!!(word = words.pop())) { line.push(word); tspan.text(line.join(" ")); if (width === null || tspan.node().getComputedTextLength() > width) { line.pop(); tspan.text(line.join(" ")); line = [word]; tspan = text.append("tspan").attr("x", 0).attr("y", ++lineNumber * lineHeight + dy + "em").attr(tspanAttrs || {}).text(word); } } }); } MG.wrap_text = wrap_text; function register(chartType, descriptor, defaults) { MG.charts[chartType] = { descriptor: descriptor, defaults: defaults || {} }; } MG.register = register; /** Record of all registered hooks. For internal use only. */ MG._hooks = {}; /** Add a hook callthrough to the stack. Hooks are executed in the order that they were registered. */ MG.add_hook = function (name, func, context) { var hooks; if (!MG._hooks[name]) { MG._hooks[name] = []; } hooks = MG._hooks[name]; var already_registered = hooks.filter(function (hook) { return hook.func === func; }).length > 0; if (already_registered) { throw 'That function is already registered.'; } hooks.push({ func: func, context: context }); }; /** Execute registered hooks. Optional arguments */ MG.call_hook = function (name) { var hooks = MG._hooks[name], result = [].slice.apply(arguments, [1]), processed; if (hooks) { hooks.forEach(function (hook) { if (hook.func) { var params = processed || result; if (params && params.constructor !== Array) { params = [params]; } params = [].concat.apply([], params); processed = hook.func.apply(hook.context, params); } }); } return processed || result; }; MG.globals = {}; MG.deprecations = { rollover_callback: { replacement: 'mouseover', version: '2.0' }, rollout_callback: { replacement: 'mouseout', version: '2.0' }, x_rollover_format: { replacement: 'x_mouseover', version: '2.10' }, y_rollover_format: { replacement: 'y_mouseover', version: '2.10' }, show_years: { replacement: 'show_secondary_x_label', version: '2.1' }, xax_start_at_min: { replacement: 'axes_not_compact', version: '2.7' }, interpolate_tension: { replacement: 'interpolate', version: '2.10' } }; MG.globals.link = false; MG.globals.version = "1.1"; MG.charts = {}; MG.data_graphic = function (args) { 'use strict'; var defaults = { missing_is_zero: false, // if true, missing values will be treated as zeros missing_is_hidden: false, // if true, missing values will appear as broken segments missing_is_hidden_accessor: null, // the accessor that determines the boolean value for missing data points legend: '', // an array identifying the labels for a chart's lines legend_target: '', // if set, the specified element is populated with a legend error: '', // if set, a graph will show an error icon and log the error to the console animate_on_load: false, // animate lines on load top: 65, // the size of the top margin title_y_position: 10, // how many pixels from the top edge (0) should we show the title at center_title_full_width: false, // center the title over the full graph (i.e. ignore left and right margins) bottom: 45, // the size of the bottom margin right: 10, // size of the right margin left: 50, // size of the left margin buffer: 8, // the buffer between the actual chart area and the margins width: 350, // the width of the entire graphic height: 220, // the height of the entire graphic full_width: false, // sets the graphic width to be the width of the parent element and resizes dynamically full_height: false, // sets the graphic width to be the width of the parent element and resizes dynamically small_height_threshold: 120, // the height threshold for when smaller text appears small_width_threshold: 160, // the width threshold for when smaller text appears xax_count: 6, // number of x axis ticks xax_tick_length: 5, // x axis tick length axes_not_compact: true, yax_count: 3, // number of y axis ticks yax_tick_length: 5, // y axis tick length x_extended_ticks: false, // extends x axis ticks across chart - useful for tall charts y_extended_ticks: false, // extends y axis ticks across chart - useful for long charts y_scale_type: 'linear', max_x: null, max_y: null, min_x: null, min_y: null, // if set, y axis starts at an arbitrary value min_y_from_data: false, // if set, y axis will start at minimum value rather than at 0 point_size: 2.5, // the size of the dot that appears on a line on mouse-over active_point_on_lines: false, // if set, active dot on lines will be displayed. active_point_accessor: 'active', // data accessor value to determine if a point is active or not active_point_size: 2, // the size of the dot that appears on a line when points_always_visible: false, // whether to always display data points and not just on hover x_accessor: 'date', xax_units: '', x_label: '', x_sort: true, x_axis: true, y_axis: true, x_axis_position: 'bottom', y_axis_position: 'left', x_axis_type: null, // TO BE INTRODUCED IN 2.10 y_axis_type: null, // TO BE INTRODUCED IN 2.10 ygroup_accessor: null, xgroup_accessor: null, y_padding_percentage: 0.05, // for categorical scales y_outer_padding_percentage: 0.1, // for categorical scales ygroup_padding_percentage: 0.25, // for categorical scales ygroup_outer_padding_percentage: 0, // for categorical scales x_padding_percentage: 0.05, // for categorical scales x_outer_padding_percentage: 0.1, // for categorical scales xgroup_padding_percentage: 0.25, // for categorical scales xgroup_outer_padding_percentage: 0, // for categorical scales y_categorical_show_guides: false, x_categorical_show_guide: false, rotate_x_labels: 0, rotate_y_labels: 0, y_accessor: 'value', y_label: '', yax_units: '', yax_units_append: false, x_rug: false, y_rug: false, mouseover_align: 'right', // implemented in point.js x_mouseover: null, y_mouseover: null, transition_on_update: true, mouseover: null, click: null, show_rollover_text: true, show_confidence_band: null, // given [l, u] shows a confidence at each point from l to u xax_format: null, // xax_format is a function that formats the labels for the x axis. area: true, // Can be also an array to select lines having areas (e.g. [1, 3]) flip_area_under_y_value: null, // Specify a Y baseline number value to flip area under it. chart_type: 'line', data: [], decimals: 2, // the number of decimals in any rollover format: 'count', // format = {count, percentage} inflator: 10 / 9, // for setting y axis max linked: false, // links together all other graphs with linked:true, so rollovers in one trigger rollovers in the others linked_format: '%Y-%m-%d', // What granularity to link on for graphs. Default is at day list: false, baselines: null, // sets the baseline lines markers: null, // sets the marker lines scalefns: {}, scales: {}, utc_time: false, european_clock: false, show_year_markers: false, show_secondary_x_label: true, target: '#viz', interpolate: d3.curveCatmullRom.alpha(0), // interpolation method to use when rendering lines; increase tension if your data is irregular and you notice artifacts custom_line_color_map: [], // allows arbitrary mapping of lines to colors, e.g. [2,3] will map line 1 to color 2 and line 2 to color 3 colors: null, // UNIMPLEMENTED - allows direct color mapping to line colors. Will eventually require max_data_size: null, // explicitly specify the the max number of line series, for use with custom_line_color_map aggregate_rollover: false, // links the lines in a multi-line chart show_tooltips: true, // if enabled, a chart's description will appear in a tooltip (requires jquery) showActivePoint: true, // If enabled show active data point information in chart brush: null, // add brushing function for this chart. could be set as 'xy', 'x', 'y' to restrict axis zoom_target: null, // zooming target of brushing function. if not set the default is to zoom the current chart brushing_selection_changed: null // callback function on brushing. the first parameter are the arguments that correspond to this chart, the second parameter is the range of the selection }; MG.call_hook('global.defaults', defaults); if (!args) { args = {}; } var selected_chart = MG.charts[args.chart_type || defaults.chart_type]; merge_with_defaults(args, selected_chart.defaults, defaults); if (args.list) { args.x_accessor = 0; args.y_accessor = 1; } // check for deprecated parameters for (var key in MG.deprecations) { if (args.hasOwnProperty(key)) { var deprecation = MG.deprecations[key], message = 'Use of `args.' + key + '` has been deprecated', replacement = deprecation.replacement, version; // transparently alias the deprecated if (replacement) { if (args[replacement]) { message += '. The replacement - `args.' + replacement + '` - has already been defined. This definition will be discarded.'; } else { args[replacement] = args[key]; } } if (deprecation.warned) { continue; } deprecation.warned = true; if (replacement) { message += ' in favor of `args.' + replacement + '`'; } warn_deprecation(message, deprecation.version); } } MG.call_hook('global.before_init', args); new selected_chart.descriptor(args); return args.data; }; if (mg_jquery_exists()) { /* ======================================================================== * Bootstrap: tooltip.js v3.3.5 * http://getbootstrap.com/javascript/#tooltip * Inspired by the original jQuery.tipsy by Jason Frame * ======================================================================== * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // TOOLTIP PUBLIC CLASS DEFINITION // =============================== var Tooltip = function Tooltip(element, options) { this.type = null; this.options = null; this.enabled = null; this.timeout = null; this.hoverState = null; this.$element = null; this.inState = null; this.init('tooltip', element, options); }; Tooltip.VERSION = '3.3.5'; Tooltip.TRANSITION_DURATION = 150; Tooltip.DEFAULTS = { animation: true, placement: 'top', selector: false, template: '', trigger: 'hover focus', title: '', delay: 0, html: false, container: false, viewport: { selector: 'body', padding: 0 } }; Tooltip.prototype.init = function (type, element, options) { this.enabled = true; this.type = type; this.$element = $(element); this.options = this.getOptions(options); this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : this.options.viewport.selector || this.options.viewport); this.inState = { click: false, hover: false, focus: false }; if (this.$element[0] instanceof document.constructor && !this.options.selector) { throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!'); } var triggers = this.options.trigger.split(' '); for (var i = triggers.length; i--;) { var trigger = triggers[i]; if (trigger == 'click') { this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)); } else if (trigger != 'manual') { var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin'; var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout'; this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)); this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)); } } this.options.selector ? this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' }) : this.fixTitle(); }; Tooltip.prototype.getDefaults = function () { return Tooltip.DEFAULTS; }; Tooltip.prototype.getOptions = function (options) { options = $.extend({}, this.getDefaults(), this.$element.data(), options); if (options.delay && typeof options.delay == 'number') { options.delay = { show: options.delay, hide: options.delay }; } return options; }; Tooltip.prototype.getDelegateOptions = function () { var options = {}; var defaults = this.getDefaults(); this._options && $.each(this._options, function (key, value) { if (defaults[key] != value) options[key] = value; }); return options; }; Tooltip.prototype.enter = function (obj) { var self = obj instanceof this.constructor ? obj : $(obj.currentTarget).data('bs.' + this.type); if (!self) { self = new this.constructor(obj.currentTarget, this.getDelegateOptions()); $(obj.currentTarget).data('bs.' + this.type, self); } if (obj instanceof $.Event) { self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true; } if (self.tip().hasClass('in') || self.hoverState == 'in') { self.hoverState = 'in'; return; } clearTimeout(self.timeout); self.hoverState = 'in'; if (!self.options.delay || !self.options.delay.show) return self.show(); self.timeout = setTimeout(function () { if (self.hoverState == 'in') self.show(); }, self.options.delay.show); }; Tooltip.prototype.isInStateTrue = function () { for (var key in this.inState) { if (this.inState[key]) return true; } return false; }; Tooltip.prototype.leave = function (obj) { var self = obj instanceof this.constructor ? obj : $(obj.currentTarget).data('bs.' + this.type); if (!self) { self = new this.constructor(obj.currentTarget, this.getDelegateOptions()); $(obj.currentTarget).data('bs.' + this.type, self); } if (obj instanceof $.Event) { self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false; } if (self.isInStateTrue()) return; clearTimeout(self.timeout); self.hoverState = 'out'; if (!self.options.delay || !self.options.delay.hide) return self.hide(); self.timeout = setTimeout(function () { if (self.hoverState == 'out') self.hide(); }, self.options.delay.hide); }; Tooltip.prototype.show = function () { var e = $.Event('show.bs.' + this.type); if (this.hasContent() && this.enabled) { this.$element.trigger(e); var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0]); if (e.isDefaultPrevented() || !inDom) return; var that = this; var $tip = this.tip(); var tipId = this.getUID(this.type); this.setContent(); $tip.attr('id', tipId); this.$element.attr('aria-describedby', tipId); if (this.options.animation) $tip.addClass('fade'); var placement = typeof this.options.placement == 'function' ? this.options.placement.call(this, $tip[0], this.$element[0]) : this.options.placement; var autoToken = /\s?auto?\s?/i; var autoPlace = autoToken.test(placement); if (autoPlace) placement = placement.replace(autoToken, '') || 'top'; $tip.detach().css({ top: 0, left: 0, display: 'block' }).addClass(placement).data('bs.' + this.type, this); this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element); this.$element.trigger('inserted.bs.' + this.type); var pos = this.getPosition(); var actualWidth = $tip[0].offsetWidth; var actualHeight = $tip[0].offsetHeight; if (autoPlace) { var orgPlacement = placement; var viewportDim = this.getPosition(this.$viewport); placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' : placement == 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' : placement == 'right' && pos.right + actualWidth > viewportDim.width ? 'left' : placement == 'left' && pos.left - actualWidth < viewportDim.left ? 'right' : placement; $tip.removeClass(orgPlacement).addClass(placement); } var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight); this.applyPlacement(calculatedOffset, placement); var complete = function complete() { var prevHoverState = that.hoverState; that.$element.trigger('shown.bs.' + that.type); that.hoverState = null; if (prevHoverState == 'out') that.leave(that); }; $.support.transition && this.$tip.hasClass('fade') ? $tip.one('bsTransitionEnd', complete).emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : complete(); } }; Tooltip.prototype.applyPlacement = function (offset, placement) { var $tip = this.tip(); var width = $tip[0].offsetWidth; var height = $tip[0].offsetHeight; // manually read margins because getBoundingClientRect includes difference var marginTop = parseInt($tip.css('margin-top'), 10); var marginLeft = parseInt($tip.css('margin-left'), 10); // we must check for NaN for ie 8/9 if (isNaN(marginTop)) marginTop = 0; if (isNaN(marginLeft)) marginLeft = 0; offset.top += marginTop; offset.left += marginLeft; // $.fn.offset doesn't round pixel values // so we use setOffset directly with our own function B-0 $.offset.setOffset($tip[0], $.extend({ using: function using(props) { $tip.css({ top: Math.round(props.top), left: Math.round(props.left) }); } }, offset), 0); $tip.addClass('in'); // check to see if placing tip in new offset caused the tip to resize itself var actualWidth = $tip[0].offsetWidth; var actualHeight = $tip[0].offsetHeight; if (placement == 'top' && actualHeight != height) { offset.top = offset.top + height - actualHeight; } var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight); if (delta.left) offset.left += delta.left;else offset.top += delta.top; var isVertical = /top|bottom/.test(placement); var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight; var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight'; $tip.offset(offset); this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical); }; Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) { this.arrow().css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%').css(isVertical ? 'top' : 'left', ''); }; Tooltip.prototype.setContent = function () { var $tip = this.tip(); var title = this.getTitle(); $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title); $tip.removeClass('fade in top bottom left right'); }; Tooltip.prototype.hide = function (callback) { var that = this; var $tip = $(this.$tip); var e = $.Event('hide.bs.' + this.type); function complete() { if (that.hoverState != 'in') $tip.detach(); that.$element.removeAttr('aria-describedby').trigger('hidden.bs.' + that.type); callback && callback(); } this.$element.trigger(e); if (e.isDefaultPrevented()) return; $tip.removeClass('in'); $.support.transition && $tip.hasClass('fade') ? $tip.one('bsTransitionEnd', complete).emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : complete(); this.hoverState = null; return this; }; Tooltip.prototype.fixTitle = function () { var $e = this.$element; if ($e.attr('title') || typeof $e.attr('data-original-title') != 'string') { $e.attr('data-original-title', $e.attr('title') || '').attr('title', ''); } }; Tooltip.prototype.hasContent = function () { return this.getTitle(); }; Tooltip.prototype.getPosition = function ($element) { $element = $element || this.$element; var el = $element[0]; var isBody = el.tagName == 'BODY'; var elRect = el.getBoundingClientRect(); if (elRect.width == null) { // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093 elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top }); } var elOffset = isBody ? { top: 0, left: 0 } : $element.offset(); var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() }; var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null; return $.extend({}, elRect, scroll, outerDims, elOffset); }; Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : /* placement == 'right' */{ top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width }; }; Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) { var delta = { top: 0, left: 0 }; if (!this.$viewport) return delta; var viewportPadding = this.options.viewport && this.options.viewport.padding || 0; var viewportDimensions = this.getPosition(this.$viewport); if (/right|left/.test(placement)) { var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll; var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight; if (topEdgeOffset < viewportDimensions.top) { // top overflow delta.top = viewportDimensions.top - topEdgeOffset; } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset; } } else { var leftEdgeOffset = pos.left - viewportPadding; var rightEdgeOffset = pos.left + viewportPadding + actualWidth; if (leftEdgeOffset < viewportDimensions.left) { // left overflow delta.left = viewportDimensions.left - leftEdgeOffset; } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset; } } return delta; }; Tooltip.prototype.getTitle = function () { var title; var $e = this.$element; var o = this.options; title = $e.attr('data-original-title') || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title); return title; }; Tooltip.prototype.getUID = function (prefix) { do { prefix += ~~(Math.random() * 1000000); } while (document.getElementById(prefix)); return prefix; }; Tooltip.prototype.tip = function () { if (!this.$tip) { this.$tip = $(this.options.template); if (this.$tip.length != 1) { throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!'); } } return this.$tip; }; Tooltip.prototype.arrow = function () { return this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow'); }; Tooltip.prototype.enable = function () { this.enabled = true; }; Tooltip.prototype.disable = function () { this.enabled = false; }; Tooltip.prototype.toggleEnabled = function () { this.enabled = !this.enabled; }; Tooltip.prototype.toggle = function (e) { var self = this; if (e) { self = $(e.currentTarget).data('bs.' + this.type); if (!self) { self = new this.constructor(e.currentTarget, this.getDelegateOptions()); $(e.currentTarget).data('bs.' + this.type, self); } } if (e) { self.inState.click = !self.inState.click; if (self.isInStateTrue()) self.enter(self);else self.leave(self); } else { self.tip().hasClass('in') ? self.leave(self) : self.enter(self); } }; Tooltip.prototype.destroy = function () { var that = this; clearTimeout(this.timeout); this.hide(function () { that.$element.off('.' + that.type).removeData('bs.' + that.type); if (that.$tip) { that.$tip.detach(); } that.$tip = null; that.$arrow = null; that.$viewport = null; }); }; // TOOLTIP PLUGIN DEFINITION // ========================= function Plugin(option) { return this.each(function () { var $this = $(this); var data = $this.data('bs.tooltip'); var options = (typeof option === 'undefined' ? 'undefined' : _typeof(option)) == 'object' && option; if (!data && /destroy|hide/.test(option)) return; if (!data) $this.data('bs.tooltip', data = new Tooltip(this, options)); if (typeof option == 'string') data[option](); }); } var old = $.fn.tooltip; $.fn.tooltip = Plugin; $.fn.tooltip.Constructor = Tooltip; // TOOLTIP NO CONFLICT // =================== $.fn.tooltip.noConflict = function () { $.fn.tooltip = old; return this; }; }(jQuery); /* ======================================================================== * Bootstrap: popover.js v3.3.5 * http://getbootstrap.com/javascript/#popovers * ======================================================================== * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // POPOVER PUBLIC CLASS DEFINITION // =============================== var Popover = function Popover(element, options) { this.init('popover', element, options); }; if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js'); Popover.VERSION = '3.3.5'; Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, { placement: 'right', trigger: 'click', content: '', template: '' }); // NOTE: POPOVER EXTENDS tooltip.js // ================================ Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype); Popover.prototype.constructor = Popover; Popover.prototype.getDefaults = function () { return Popover.DEFAULTS; }; Popover.prototype.setContent = function () { var $tip = this.tip(); var title = this.getTitle(); var content = this.getContent(); $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title); $tip.find('.popover-content').children().detach().end()[// we use append for html objects to maintain js events this.options.html ? typeof content == 'string' ? 'html' : 'append' : 'text'](content); $tip.removeClass('fade top bottom left right in'); // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do // this manually by checking the contents. if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide(); }; Popover.prototype.hasContent = function () { return this.getTitle() || this.getContent(); }; Popover.prototype.getContent = function () { var $e = this.$element; var o = this.options; return $e.attr('data-content') || (typeof o.content == 'function' ? o.content.call($e[0]) : o.content); }; Popover.prototype.arrow = function () { return this.$arrow = this.$arrow || this.tip().find('.arrow'); }; // POPOVER PLUGIN DEFINITION // ========================= function Plugin(option) { return this.each(function () { var $this = $(this); var data = $this.data('bs.popover'); var options = (typeof option === 'undefined' ? 'undefined' : _typeof(option)) == 'object' && option; if (!data && /destroy|hide/.test(option)) return; if (!data) $this.data('bs.popover', data = new Popover(this, options)); if (typeof option == 'string') data[option](); }); } var old = $.fn.popover; $.fn.popover = Plugin; $.fn.popover.Constructor = Popover; // POPOVER NO CONFLICT // =================== $.fn.popover.noConflict = function () { $.fn.popover = old; return this; }; }(jQuery); } function chart_title(args) { 'use strict'; var svg = mg_get_svg_child_of(args.target); //remove the current title if it exists svg.select('.mg-header').remove(); if (args.target && args.title) { var chartTitle = svg.insert('text').attr('class', 'mg-header').attr('x', args.center_title_full_width ? args.width / 2 : (args.width + args.left - args.right) / 2).attr('y', args.title_y_position).attr('text-anchor', 'middle').attr('dy', '0.55em'); //show the title chartTitle.append('tspan').attr('class', 'mg-chart-title').text(args.title); //show and activate the description icon if we have a description if (args.show_tooltips && args.description && mg_jquery_exists()) { chartTitle.append('tspan').attr('class', 'mg-chart-description').attr('dx', '0.3em').text('\uF059'); //now that the title is an svg text element, we'll have to trigger //mouseenter, mouseleave events manually for the popover to work properly var $chartTitle = $(chartTitle.node()); $chartTitle.popover({ html: true, animation: false, placement: 'top', content: args.description, container: args.target, trigger: 'manual', template: '

' }).on('mouseenter', function () { d3.selectAll(args.target).selectAll('.mg-popover').remove(); $(this).popover('show'); $(d3.select(args.target).select('.popover').node()).on('mouseleave', function () { $chartTitle.popover('hide'); }); }).on('mouseleave', function () { setTimeout(function () { if (!$('.popover:hover').length) { $chartTitle.popover('hide'); } }, 120); }); } else if (args.show_tooltips && args.description && typeof $ === 'undefined') { args.error = 'In order to enable tooltips, please make sure you include jQuery.'; } } if (args.error) { error(args); } } MG.chart_title = chart_title; function mg_add_scale_function(args, scalefcn_name, scale, accessor, inflation) { args.scalefns[scalefcn_name] = function (di) { if (inflation === undefined) return args.scales[scale](di[accessor]);else return args.scales[scale](di[accessor]) + inflation; }; } function mg_position(str, args) { if (str === 'bottom' || str === 'top') { return [mg_get_plot_left(args), mg_get_plot_right(args)]; } if (str === 'left' || str === 'right') { return [mg_get_plot_bottom(args), args.top]; } } function mg_cat_position(str, args) { if (str === 'bottom' || str === 'top') { return [mg_get_plot_left(args), mg_get_plot_right(args)]; } if (str === 'left' || str === 'right') { return [mg_get_plot_bottom(args), mg_get_plot_top(args)]; } } function MGScale(args) { // big wrapper around d3 scale that automatically formats & calculates scale bounds // according to the data, and handles other niceties. var scaleArgs = {}; scaleArgs.use_inflator = false; scaleArgs.zero_bottom = false; scaleArgs.scaleType = 'numerical'; this.namespace = function (_namespace) { scaleArgs.namespace = _namespace; scaleArgs.namespace_accessor_name = scaleArgs.namespace + '_accessor'; scaleArgs.scale_name = scaleArgs.namespace.toUpperCase(); scaleArgs.scalefn_name = scaleArgs.namespace + 'f'; return this; }; this.scaleName = function (scaleName) { scaleArgs.scale_name = scaleName.toUpperCase(); scaleArgs.scalefn_name = scaleName + 'f'; return this; }; this.inflateDomain = function (tf) { scaleArgs.use_inflator = tf; return this; }; this.zeroBottom = function (tf) { scaleArgs.zero_bottom = tf; return this; }; ///////////////////////////////////////////////////////////////////////////////////////////////////////// /// all scale domains are either numerical (number, date, etc.) or categorical (factor, label, etc) ///// ///////////////////////////////////////////////////////////////////////////////////////////////////////// // these functions automatically create the d3 scale function and place the domain. this.numericalDomainFromData = function () { var other_flat_data_arrays = []; if (arguments.length > 0) { other_flat_data_arrays = arguments; } // pull out a non-empty array in args.data. var illustrative_data; for (var i = 0; i < args.data.length; i++) { if (args.data[i].length > 0) { illustrative_data = args.data[i]; } } scaleArgs.is_time_series = mg_is_date(illustrative_data[0][args[scaleArgs.namespace_accessor_name]]) ? true : false; mg_add_scale_function(args, scaleArgs.scalefn_name, scaleArgs.scale_name, args[scaleArgs.namespace_accessor_name]); mg_min_max_numerical(args, scaleArgs, other_flat_data_arrays, scaleArgs.use_inflator); var time_scale = args.utc_time ? d3.scaleUtc() : d3.scaleTime(); args.scales[scaleArgs.scale_name] = scaleArgs.is_time_series ? time_scale : mg_is_function(args[scaleArgs.namespace + '_scale_type']) ? args.y_scale_type() : args[scaleArgs.namespace + '_scale_type'] === 'log' ? d3.scaleLog() : d3.scaleLinear(); args.scales[scaleArgs.scale_name].domain([args.processed['min_' + scaleArgs.namespace], args.processed['max_' + scaleArgs.namespace]]); scaleArgs.scaleType = 'numerical'; return this; }; this.categoricalDomain = function (domain) { args.scales[scaleArgs.scale_name] = d3.scaleOrdinal().domain(domain); mg_add_scale_function(args, scaleArgs.scalefn_name, scaleArgs.scale_name, args[scaleArgs.namespace_accessor_name]); return this; }; this.categoricalDomainFromData = function () { // make args.categorical_variables. // lets make the categorical variables. var all_data = mg_flatten_array(args.data); //d3.set(data.map(function(d){return d[args.group_accessor]})).values() scaleArgs.categoricalVariables = d3.set(all_data.map(function (d) { return d[args[scaleArgs.namespace_accessor_name]]; })).values(); args.scales[scaleArgs.scale_name] = d3.scaleBand().domain(scaleArgs.categoricalVariables); scaleArgs.scaleType = 'categorical'; return this; }; ///////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////// all scale ranges are either positional (for axes, etc) or arbitrary (colors, size, etc) ////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////// this.numericalRange = function (range) { if (typeof range === 'string') { args.scales[scaleArgs.scale_name].range(mg_position(range, args)); } else { args.scales[scaleArgs.scale_name].range(range); } return this; }; this.categoricalRangeBands = function (range, halfway) { if (halfway === undefined) halfway = false; var namespace = scaleArgs.namespace; var paddingPercentage = args[namespace + '_padding_percentage']; var outerPaddingPercentage = args[namespace + '_outer_padding_percentage']; if (typeof range === 'string') { // if string, it's a location. Place it accordingly. args.scales[scaleArgs.scale_name].range(mg_position(range, args)).paddingInner(paddingPercentage).paddingOuter(outerPaddingPercentage); } else { args.scales[scaleArgs.scale_name].range(range).paddingInner(paddingPercentage).paddingOuter(outerPaddingPercentage); } mg_add_scale_function(args, scaleArgs.scalefn_name, scaleArgs.scale_name, args[scaleArgs.namespace_accessor_name], halfway ? args.scales[scaleArgs.scale_name].bandwidth() / 2 : 0); return this; }; this.categoricalRange = function (range) { args.scales[scaleArgs.scale_name].range(range); mg_add_scale_function(args, scaleArgs.scalefn_name, scaleArgs.scale_name, args[scaleArgs.namespace_accessor_name]); return this; }; this.categoricalColorRange = function () { args.scales[scaleArgs.scale_name] = args.scales[scaleArgs.scale_name].domain().length > 10 ? d3.scaleOrdinal(d3.schemeCategory20) : d3.scaleOrdinal(d3.schemeCategory10); args.scales[scaleArgs.scale_name].domain(scaleArgs.categoricalVariables); mg_add_scale_function(args, scaleArgs.scalefn_name, scaleArgs.scale_name, args[scaleArgs.namespace_accessor_name]); return this; }; this.clamp = function (yn) { args.scales[scaleArgs.scale_name].clamp(yn); return this; }; return this; } MG.scale_factory = MGScale; /////////////////////////////// x, x_accessor, markers, baselines, etc. function mg_min_max_numerical(args, scaleArgs, additional_data_arrays) { // A BIT OF EXPLANATION ABOUT THIS FUNCTION // This function pulls out all the accessor values in all the arrays in args.data. // We also have this additional argument, additional_data_arrays, which is an array of arrays of raw data values. // These values also get concatenated to the data pulled from args.data, and the extents are calculate from that. // They are optional. // // This may seem arbitrary, but it gives us a lot of flexibility. For instance, if we're calculating // the min and max for the y axis of a line chart, we're going to want to also factor in baselines (horizontal lines // that might potentially be outside of the y value bounds). The easiest way to do this is in the line.js code // & scale creation to just flatten the args.baselines array, pull out hte values, and feed it in // so it appears in additional_data_arrays. var namespace = scaleArgs.namespace; var namespace_accessor_name = scaleArgs.namespace_accessor_name; var use_inflator = scaleArgs.use_inflator; var zero_bottom = scaleArgs.zero_bottom; var accessor = args[namespace_accessor_name]; // add together all relevant data arrays. var all_data = mg_flatten_array(args.data).map(function (dp) { return dp[accessor]; }).concat(mg_flatten_array(additional_data_arrays)); // do processing for log if (args[namespace + '_scale_type'] === 'log') { all_data = all_data.filter(function (d) { return d > 0; }); } // use inflator? var extents = d3.extent(all_data); var min_val = extents[0]; var max_val = extents[1]; // bolt scale domain to zero when the right conditions are met: // not pulling the bottom of the range from data // not zero-bottomed // not a time series if (zero_bottom && !args['min_' + namespace + '_from_data'] && min_val > 0 && !scaleArgs.is_time_series) { min_val = args[namespace + '_scale_type'] === 'log' ? 1 : 0; } if (args[namespace + '_scale_type'] !== 'log' && min_val < 0 && !scaleArgs.is_time_series) { min_val = min_val - (min_val - min_val * args.inflator) * use_inflator; } if (!scaleArgs.is_time_series) { max_val = max_val < 0 ? max_val + (max_val - max_val * args.inflator) * use_inflator : max_val * (use_inflator ? args.inflator : 1); } min_val = args['min_' + namespace] != null ? args['min_' + namespace] : min_val; max_val = args['max_' + namespace] != null ? args['max_' + namespace] : max_val; // if there's a single data point, we should custom-set the max values // so we're displaying some kind of range if (min_val === max_val && args['min_' + namespace] == null && args['max_' + namespace] == null) { if (mg_is_date(min_val)) { max_val = new Date(MG.clone(min_val).setDate(min_val.getDate() + 1)); } else if (typeof min_val === 'number') { max_val = min_val + 1; mg_force_xax_count_to_be_two(args); } } args.processed['min_' + namespace] = min_val; args.processed['max_' + namespace] = max_val; if (args.processed['zoom_' + namespace]) { args.processed['min_' + namespace] = args.processed['zoom_' + namespace][0]; args.processed['max_' + namespace] = args.processed['zoom_' + namespace][1]; } MG.call_hook('x_axis.process_min_max', args, args.processed.min_x, args.processed.max_x); MG.call_hook('y_axis.process_min_max', args, args.processed.min_y, args.processed.max_y); } function mg_categorical_group_color_scale(args) { if (args.color_accessor !== false) { if (args.ygroup_accessor) { // add a custom accessor element. if (args.color_accessor === null) { args.color_accessor = args.y_accessor; } else {} } if (args.color_accessor !== null) { new MG.scale_factory(args).namespace('color').categoricalDomainFromData().categoricalColorRange(); } } } function mg_add_color_categorical_scale(args, domain, accessor) { args.scales.color = d3.scaleOrdinal(d3.schemeCategory20).domain(domain); args.scalefns.color = function (d) { return args.scales.color(d[accessor]); }; } function mg_get_categorical_domain(data, accessor) { return d3.set(data.map(function (d) { return d[accessor]; })).values(); } function mg_get_color_domain(args) { var color_domain; if (args.color_domain === null) { if (args.color_type === 'number') { color_domain = d3.extent(args.data[0], function (d) { return d[args.color_accessor]; }); } else if (args.color_type === 'category') { color_domain = mg_get_categorical_domain(args.data[0], args.color_accessor); } } else { color_domain = args.color_domain; } return color_domain; } function mg_get_color_range(args) { var color_range; if (args.color_range === null) { if (args.color_type === 'number') { color_range = ['blue', 'red']; } else { color_range = null; } } else { color_range = args.color_range; } return color_range; } function processScaleTicks(args, axis) { var accessor = args[axis + '_accessor']; var scale_ticks = args.scales[axis.toUpperCase()].ticks(args[axis + 'ax_count']); var max = args.processed['max_' + axis]; function log10(val) { if (val === 1000) { return 3; } if (val === 1000000) { return 7; } return Math.log(val) / Math.LN10; } if (args[axis + '_scale_type'] === 'log') { // get out only whole logs scale_ticks = scale_ticks.filter(function (d) { return Math.abs(log10(d)) % 1 < 1e-6 || Math.abs(log10(d)) % 1 > 1 - 1e-6; }); } // filter out fraction ticks if our data is ints and if xmax > number of generated ticks var number_of_ticks = scale_ticks.length; // is our data object all ints? var data_is_int = true; args.data.forEach(function (d, i) { d.forEach(function (d, i) { if (d[accessor] % 1 !== 0) { data_is_int = false; return false; } }); }); if (data_is_int && number_of_ticks > max && args.format === 'count') { // remove non-integer ticks scale_ticks = scale_ticks.filter(function (d) { return d % 1 === 0; }); } args.processed[axis + '_ticks'] = scale_ticks; } function rugPlacement(args, axisArgs) { var position = axisArgs.position; var ns = axisArgs.namespace; var coordinates = {}; if (position === 'left') { coordinates.x1 = mg_get_left(args) + 1; coordinates.x2 = mg_get_left(args) + args.rug_buffer_size; coordinates.y1 = args.scalefns[ns + 'f']; coordinates.y2 = args.scalefns[ns + 'f']; } if (position === 'right') { coordinates.x1 = mg_get_right(args) - 1, coordinates.x2 = mg_get_right(args) - args.rug_buffer_size, coordinates.y1 = args.scalefns[ns + 'f']; coordinates.y2 = args.scalefns[ns + 'f']; } if (position === 'top') { coordinates.x1 = args.scalefns[ns + 'f']; coordinates.x2 = args.scalefns[ns + 'f']; coordinates.y1 = mg_get_top(args) + 1; coordinates.y2 = mg_get_top(args) + args.rug_buffer_size; } if (position === 'bottom') { coordinates.x1 = args.scalefns[ns + 'f']; coordinates.x2 = args.scalefns[ns + 'f']; coordinates.y1 = mg_get_bottom(args) - 1; coordinates.y2 = mg_get_bottom(args) - args.rug_buffer_size; } return coordinates; } function rimPlacement(args, axisArgs) { var ns = axisArgs.namespace; var position = axisArgs.position; var tick_length = args.processed[ns + '_ticks'].length; var ticks = args.processed[ns + '_ticks']; var scale = args.scales[ns.toUpperCase()]; var coordinates = {}; if (position === 'left') { coordinates.x1 = mg_get_left(args); coordinates.x2 = mg_get_left(args); coordinates.y1 = scale(ticks[0]).toFixed(2); coordinates.y2 = scale(ticks[tick_length - 1]).toFixed(2); } if (position === 'right') { coordinates.x1 = mg_get_right(args); coordinates.x2 = mg_get_right(args); coordinates.y1 = scale(ticks[0]).toFixed(2); coordinates.y2 = scale(ticks[tick_length - 1]).toFixed(2); } if (position === 'top') { coordinates.x1 = mg_get_left(args); coordinates.x2 = mg_get_right(args); coordinates.y1 = mg_get_top(args); coordinates.y2 = mg_get_top(args); } if (position === 'bottom') { coordinates.x1 = mg_get_left(args); coordinates.x2 = mg_get_right(args); coordinates.y1 = mg_get_bottom(args); coordinates.y2 = mg_get_bottom(args); } if (position === 'left' || position === 'right') { if (args.axes_not_compact) { coordinates.y1 = mg_get_bottom(args); coordinates.y2 = mg_get_top(args); } else if (tick_length) { coordinates.y1 = scale(ticks[0]).toFixed(2); coordinates.y2 = scale(ticks[tick_length - 1]).toFixed(2); } } return coordinates; } function labelPlacement(args, axisArgs) { var position = axisArgs.position; var ns = axisArgs.namespace; var tickLength = args[ns + 'ax_tick_length']; var scale = args.scales[ns.toUpperCase()]; var coordinates = {}; if (position === 'left') { coordinates.x = mg_get_left(args) - tickLength * 3 / 2; coordinates.y = function (d) { return scale(d).toFixed(2); }; coordinates.dx = -3; coordinates.dy = '.35em'; coordinates.textAnchor = 'end'; coordinates.text = function (d) { return mg_compute_yax_format(args)(d); }; } if (position === 'right') { coordinates.x = mg_get_right(args) + tickLength * 3 / 2; coordinates.y = function (d) { return scale(d).toFixed(2); }; coordinates.dx = 3; coordinates.dy = '.35em'; coordinates.textAnchor = 'start'; coordinates.text = function (d) { return mg_compute_yax_format(args)(d); }; } if (position === 'top') { coordinates.x = function (d) { return scale(d).toFixed(2); }; coordinates.y = (mg_get_top(args) - tickLength * 7 / 3).toFixed(2); coordinates.dx = 0; coordinates.dy = '0em'; coordinates.textAnchor = 'middle'; coordinates.text = function (d) { return mg_default_xax_format(args)(d); }; } if (position === 'bottom') { coordinates.x = function (d) { return scale(d).toFixed(2); }; coordinates.y = (mg_get_bottom(args) + tickLength * 7 / 3).toFixed(2); coordinates.dx = 0; coordinates.dy = '.50em'; coordinates.textAnchor = 'middle'; coordinates.text = function (d) { return mg_default_xax_format(args)(d); }; } return coordinates; } function addSecondaryLabelElements(args, axisArgs, g) { var tf = mg_get_yformat_and_secondary_time_function(args); var years = tf.secondary(args.processed.min_x, args.processed.max_x); if (years.length === 0) { var first_tick = args.scales.X.ticks(args.xax_count)[0]; years = [first_tick]; } var yg = mg_add_g(g, 'mg-year-marker'); if (tf.timeframe === 'default' && args.show_year_markers) { yearMarkerLine(args, axisArgs, yg, years, tf.yformat); } if (tf.tick_diff_timeframe != 'years') yearMarkerText(args, axisArgs, yg, years, tf.yformat); } function yearMarkerLine(args, axisArgs, g, years, yformat) { g.selectAll('.mg-year-marker').data(years).enter().append('line').attr('x1', function (d) { return args.scales.X(d).toFixed(2); }).attr('x2', function (d) { return args.scales.X(d).toFixed(2); }).attr('y1', mg_get_top(args)).attr('y2', mg_get_bottom(args)); } function yearMarkerText(args, axisArgs, g, years, yformat) { var position = axisArgs.position; var ns = axisArgs.namespace; var scale = args.scales[ns.toUpperCase()]; var x, y, dy, textAnchor, textFcn; var xAxisTextElement = d3.select(args.target).select('.mg-x-axis text').node().getBoundingClientRect(); if (position === 'top') { x = function x(d, i) { return scale(d).toFixed(2); }; y = mg_get_top(args) - args.xax_tick_length * 7 / 3 - xAxisTextElement.height; dy = '.50em'; textAnchor = 'middle'; textFcn = function textFcn(d) { return yformat(new Date(d)); }; } if (position === 'bottom') { x = function x(d, i) { return scale(d).toFixed(2); }; y = mg_get_bottom(args) + args.xax_tick_length * 7 / 3 + xAxisTextElement.height * 0.8; dy = '.50em'; textAnchor = 'middle'; textFcn = function textFcn(d) { return yformat(new Date(d)); }; } g.selectAll('.mg-year-marker').data(years).enter().append('text').attr('x', x).attr('y', y).attr('dy', dy).attr('text-anchor', textAnchor).text(textFcn); } function addNumericalLabels(g, args, axisArgs) { var ns = axisArgs.namespace; var coords = labelPlacement(args, axisArgs); var ticks = args.processed[ns + '_ticks']; var labels = g.selectAll('.mg-yax-labels').data(ticks).enter().append('text').attr('x', coords.x).attr('dx', coords.dx).attr('y', coords.y).attr('dy', coords.dy).attr('text-anchor', coords.textAnchor).text(coords.text); // move the labels if they overlap if (ns == 'x') { if (args.time_series && args.european_clock) { labels.append('tspan').classed('mg-european-hours', true).text(function (_d, i) { var d = new Date(_d); if (i === 0) return d3.timeFormat('%H')(d);else return ''; }); labels.append('tspan').classed('mg-european-minutes-seconds', true).text(function (_d, i) { var d = new Date(_d); return ':' + args.processed.xax_format(d); }); } else { labels.text(function (d) { return args.xax_units + args.processed.xax_format(d); }); } if (args.time_series && (args.show_years || args.show_secondary_x_label)) { addSecondaryLabelElements(args, axisArgs, g); } } if (mg_elements_are_overlapping(labels)) { labels.filter(function (d, i) { return (i + 1) % 2 === 0; }).remove(); var svg = mg_get_svg_child_of(args.target); svg.selectAll('.mg-' + ns + 'ax-ticks').filter(function (d, i) { return (i + 1) % 2 === 0; }).remove(); } } function addTickLines(g, args, axisArgs) { // name var ns = axisArgs.namespace; var position = axisArgs.position; var scale = args.scales[ns.toUpperCase()]; var ticks = args.processed[ns + '_ticks']; var ticksClass = 'mg-' + ns + 'ax-ticks'; var extendedTicksClass = 'mg-extended-' + ns + 'ax-ticks'; var extendedTicks = args[ns + '_extended_ticks']; var tickLength = args[ns + 'ax_tick_length']; var x1, x2, y1, y2; if (position === 'left') { x1 = mg_get_left(args); x2 = extendedTicks ? mg_get_right(args) : mg_get_left(args) - tickLength; y1 = function y1(d) { return scale(d).toFixed(2); }; y2 = function y2(d) { return scale(d).toFixed(2); }; } if (position === 'right') { x1 = mg_get_right(args); x2 = extendedTicks ? mg_get_left(args) : mg_get_right(args) + tickLength; y1 = function y1(d) { return scale(d).toFixed(2); }; y2 = function y2(d) { return scale(d).toFixed(2); }; } if (position === 'top') { x1 = function x1(d) { return scale(d).toFixed(2); }; x2 = function x2(d) { return scale(d).toFixed(2); }; y1 = mg_get_top(args); y2 = extendedTicks ? mg_get_bottom(args) : mg_get_top(args) - tickLength; } if (position === 'bottom') { x1 = function x1(d) { return scale(d).toFixed(2); }; x2 = function x2(d) { return scale(d).toFixed(2); }; y1 = mg_get_bottom(args); y2 = extendedTicks ? mg_get_top(args) : mg_get_bottom(args) + tickLength; } g.selectAll('.' + ticksClass).data(ticks).enter().append('line').classed(extendedTicksClass, extendedTicks).attr('x1', x1).attr('x2', x2).attr('y1', y1).attr('y2', y2); } function initializeAxisRim(g, args, axisArgs) { var namespace = axisArgs.namespace; var tick_length = args.processed[namespace + '_ticks'].length; var rim = rimPlacement(args, axisArgs); if (!args[namespace + '_extended_ticks'] && !args[namespace + '_extended_ticks'] && tick_length) { g.append('line').attr('x1', rim.x1).attr('x2', rim.x2).attr('y1', rim.y1).attr('y2', rim.y2); } } function initializeRug(args, rug_class) { var svg = mg_get_svg_child_of(args.target); var all_data = mg_flatten_array(args.data); var rug = svg.selectAll('line.' + rug_class).data(all_data); // set the attributes that do not change after initialization, per rug.enter().append('svg:line').attr('class', rug_class).attr('opacity', 0.3); // remove rug elements that are no longer in use mg_exit_and_remove(rug); // set coordinates of new rug elements mg_exit_and_remove(rug); return rug; } function rug(args, axisArgs) { 'use strict'; args.rug_buffer_size = args.chart_type === 'point' ? args.buffer / 2 : args.buffer * 2 / 3; var rug = initializeRug(args, 'mg-' + axisArgs.namespace + '-rug'); var rug_positions = rugPlacement(args, axisArgs); rug.attr('x1', rug_positions.x1).attr('x2', rug_positions.x2).attr('y1', rug_positions.y1).attr('y2', rug_positions.y2); mg_add_color_accessor_to_rug(rug, args, 'mg-' + axisArgs.namespace + '-rug-mono'); } function categoricalLabelPlacement(args, axisArgs, group) { var ns = axisArgs.namespace; var position = axisArgs.position; var scale = args.scales[ns.toUpperCase()]; var groupScale = args.scales[(ns + 'group').toUpperCase()]; var coords = {}; coords.cat = {}; coords.group = {}; // x, y, dy, text-anchor if (position === 'left') { coords.cat.x = mg_get_plot_left(args) - args.buffer; coords.cat.y = function (d) { return groupScale(group) + scale(d) + scale.bandwidth() / 2; }; coords.cat.dy = '.35em'; coords.cat.textAnchor = 'end'; coords.group.x = mg_get_plot_left(args) - args.buffer; coords.group.y = groupScale(group) + (groupScale.bandwidth ? groupScale.bandwidth() / 2 : 0); coords.group.dy = '.35em'; coords.group.textAnchor = args['rotate_' + ns + '_labels'] ? 'end' : 'end'; } if (position === 'right') { coords.cat.x = mg_get_plot_right(args) - args.buffer; coords.cat.y = function (d) { return groupScale(group) + scale(d) + scale.bandwidth() / 2; }; coords.cat.dy = '.35em'; coords.cat.textAnchor = 'start'; coords.group.x = mg_get_plot_right(args) - args.buffer; coords.group.y = groupScale(group) + (groupScale.bandwidth ? groupScale.bandwidth() / 2 : 0); coords.group.dy = '.35em'; coords.group.textAnchor = 'start'; } if (position === 'top') { coords.cat.x = function (d) { return groupScale(group) + scale(d) + scale.bandwidth() / 2; }; coords.cat.y = mg_get_plot_top(args) + args.buffer; coords.cat.dy = '.35em'; coords.cat.textAnchor = args['rotate_' + ns + '_labels'] ? 'start' : 'middle'; coords.group.x = groupScale(group) + (groupScale.bandwidth ? groupScale.bandwidth() / 2 : 0); coords.group.y = mg_get_plot_top(args) + args.buffer; coords.group.dy = '.35em'; coords.group.textAnchor = args['rotate_' + ns + '_labels'] ? 'start' : 'middle'; } if (position === 'bottom') { coords.cat.x = function (d) { return groupScale(group) + scale(d) + scale.bandwidth() / 2; }; coords.cat.y = mg_get_plot_bottom(args) + args.buffer; coords.cat.dy = '.35em'; coords.cat.textAnchor = args['rotate_' + ns + '_labels'] ? 'start' : 'middle'; coords.group.x = groupScale(group) + (groupScale.bandwidth ? groupScale.bandwidth() / 2 - scale.bandwidth() / 2 : 0); coords.group.y = mg_get_plot_bottom(args) + args.buffer; coords.group.dy = '.35em'; coords.group.textAnchor = args['rotate_' + ns + '_labels'] ? 'start' : 'middle'; } return coords; } function categoricalLabels(args, axisArgs) { var ns = axisArgs.namespace; var nsClass = 'mg-' + ns + '-axis'; var scale = args.scales[ns.toUpperCase()]; var groupScale = args.scales[(ns + 'group').toUpperCase()]; var groupAccessor = ns + 'group_accessor'; var svg = mg_get_svg_child_of(args.target); mg_selectAll_and_remove(svg, '.' + nsClass); var g = mg_add_g(svg, nsClass); var group_g; var groups = groupScale.domain && groupScale.domain() ? groupScale.domain() : ['1']; groups.forEach(function (group) { // grab group placement stuff. var coords = categoricalLabelPlacement(args, axisArgs, group); group_g = mg_add_g(g, 'mg-group-' + mg_normalize(group)); if (args[groupAccessor] !== null) { var labels = group_g.append('text').classed('mg-barplot-group-label', true).attr('x', coords.group.x).attr('y', coords.group.y).attr('dy', coords.group.dy).attr('text-anchor', coords.group.textAnchor).text(group); } else { var labels = group_g.selectAll('text').data(scale.domain()).enter().append('text').attr('x', coords.cat.x).attr('y', coords.cat.y).attr('dy', coords.cat.dy).attr('text-anchor', coords.cat.textAnchor).text(String); } if (args['rotate_' + ns + '_labels']) { rotateLabels(labels, args['rotate_' + ns + '_labels']); } }); } function categoricalGuides(args, axisArgs) { // for each group // for each data point var ns = axisArgs.namespace; var scalef = args.scalefns[ns + 'f']; var groupf = args.scalefns[ns + 'groupf']; var groupScale = args.scales[(ns + 'group').toUpperCase()]; var scale = args.scales[ns.toUpperCase()]; var position = axisArgs.position; var svg = mg_get_svg_child_of(args.target); var alreadyPlotted = []; var x1, x2, y1, y2; var grs = groupScale.domain && groupScale.domain() ? groupScale.domain() : [null]; mg_selectAll_and_remove(svg, '.mg-category-guides'); var g = mg_add_g(svg, 'mg-category-guides'); grs.forEach(function (group) { scale.domain().forEach(function (cat) { if (position === 'left' || position === 'right') { x1 = mg_get_plot_left(args); x2 = mg_get_plot_right(args); y1 = scale(cat) + groupScale(group) + scale.bandwidth() / 2; y2 = scale(cat) + groupScale(group) + scale.bandwidth() / 2; } if (position === 'top' || position === 'bottom') { x1 = scale(cat) + groupScale(group) + scale.bandwidth() / 2 * (group === null); x2 = scale(cat) + groupScale(group) + scale.bandwidth() / 2 * (group === null); y1 = mg_get_plot_bottom(args); y2 = mg_get_plot_top(args); } g.append('line').attr('x1', x1).attr('x2', x2).attr('y1', y1).attr('y2', y2).attr('stroke-dasharray', '2,1'); }); var first = groupScale(group) + scale(scale.domain()[0]) + scale.bandwidth() / 2 * (group === null || position !== 'top' && position != 'bottom'); var last = groupScale(group) + scale(scale.domain()[scale.domain().length - 1]) + scale.bandwidth() / 2 * (group === null || position !== 'top' && position != 'bottom'); var x11, x21, y11, y21, x12, x22, y12, y22; if (position === 'left' || position === 'right') { x11 = mg_get_plot_left(args); x21 = mg_get_plot_left(args); y11 = first; y21 = last; x12 = mg_get_plot_right(args); x22 = mg_get_plot_right(args); y12 = first; y22 = last; } if (position === 'bottom' || position === 'top') { x11 = first; x21 = last; y11 = mg_get_plot_bottom(args); y21 = mg_get_plot_bottom(args); x12 = first; x22 = last; y12 = mg_get_plot_top(args); y22 = mg_get_plot_top(args); } g.append('line').attr('x1', x11).attr('x2', x21).attr('y1', y11).attr('y2', y21).attr('stroke-dasharray', '2,1'); g.append('line').attr('x1', x12).attr('x2', x22).attr('y1', y12).attr('y2', y22).attr('stroke-dasharray', '2,1'); }); } function rotateLabels(labels, rotation_degree) { if (rotation_degree) { labels.attr('transform', function () { var elem = d3.select(this); return 'rotate(' + rotation_degree + ' ' + elem.attr('x') + ',' + elem.attr('y') + ')'; }); } } function zeroLine(args, axisArgs) { var svg = mg_get_svg_child_of(args.target); var ns = axisArgs.namespace; var position = axisArgs.position; var scale = args.scales[ns.toUpperCase()]; var x1, x2, y1, y2; if (position === 'left' || position === 'right') { x1 = mg_get_plot_left(args); x2 = mg_get_plot_right(args); y1 = scale(0) + 1; y2 = scale(0) + 1; } if (position === 'bottom' || position === 'top') { y1 = mg_get_plot_top(args); y2 = mg_get_plot_bottom(args); x1 = scale(0) - 1; x2 = scale(0) - 1; } svg.append('line').attr('x1', x1).attr('x2', x2).attr('y1', y1).attr('y2', y2).attr('stroke', 'black'); } var mgDrawAxis = {}; mgDrawAxis.categorical = function (args, axisArgs) { var ns = axisArgs.namespace; categoricalLabels(args, axisArgs); categoricalGuides(args, axisArgs); }; mgDrawAxis.numerical = function (args, axisArgs) { var namespace = axisArgs.namespace; var axisName = namespace + '_axis'; var axisClass = 'mg-' + namespace + '-axis'; var svg = mg_get_svg_child_of(args.target); mg_selectAll_and_remove(svg, '.' + axisClass); if (!args[axisName]) { return this; } var g = mg_add_g(svg, axisClass); processScaleTicks(args, namespace); initializeAxisRim(g, args, axisArgs); addTickLines(g, args, axisArgs); addNumericalLabels(g, args, axisArgs); // add label if (args[namespace + '_label']) { axisArgs.label(svg.select('.mg-' + namespace + '-axis'), args); } // add rugs if (args[namespace + '_rug']) { rug(args, axisArgs); } if (args.show_bar_zero) { mg_bar_add_zero_line(args); } return this; }; function axisFactory(args) { var axisArgs = {}; axisArgs.type = 'numerical'; this.namespace = function (ns) { // take the ns in the scale, and use it to axisArgs.namespace = ns; return this; }; this.rug = function (tf) { axisArgs.rug = tf; return this; }; this.label = function (tf) { axisArgs.label = tf; return this; }; this.type = function (t) { axisArgs.type = t; return this; }; this.position = function (pos) { axisArgs.position = pos; return this; }; this.zeroLine = function (tf) { axisArgs.zeroLine = tf; return this; }; this.draw = function () { mgDrawAxis[axisArgs.type](args, axisArgs); return this; }; return this; } MG.axis_factory = axisFactory; /* ================================================================================ */ /* ================================================================================ */ /* ================================================================================ */ function y_rug(args) { 'use strict'; if (!args.y_rug) { return; } args.rug_buffer_size = args.chart_type === 'point' ? args.buffer / 2 : args.buffer * 2 / 3; var rug = mg_make_rug(args, 'mg-y-rug'); rug.attr('x1', args.left + 1).attr('x2', args.left + args.rug_buffer_size).attr('y1', args.scalefns.yf).attr('y2', args.scalefns.yf); mg_add_color_accessor_to_rug(rug, args, 'mg-y-rug-mono'); } MG.y_rug = y_rug; function mg_change_y_extents_for_bars(args, my) { if (args.chart_type === 'bar') { my.min = 0; my.max = d3.max(args.data[0], function (d) { var trio = []; trio.push(d[args.y_accessor]); if (args.baseline_accessor !== null) { trio.push(d[args.baseline_accessor]); } if (args.predictor_accessor !== null) { trio.push(d[args.predictor_accessor]); } return Math.max.apply(null, trio); }); } return my; } function mg_compute_yax_format(args) { var yax_format = args.yax_format; if (!yax_format) { var decimals = args.decimals; if (args.format === 'count') { // increase decimals if we have small values, useful for realtime data if (args.processed.y_ticks.length > 1) { // calculate the number of decimals between the difference of ticks // based on approach in flot: https://github.com/flot/flot/blob/958e5fd43c6dff4bab3e1fd5cb6109df5c1e8003/jquery.flot.js#L1810 decimals = Math.max(0, -Math.floor(Math.log(Math.abs(args.processed.y_ticks[1] - args.processed.y_ticks[0])) / Math.LN10)); } yax_format = function yax_format(d) { var pf; if (decimals !== 0) { // don't scale tiny values pf = d3.format(',.' + decimals + 'f'); } else if (d < 1000) { pf = d3.format(',.0f'); } else { pf = d3.format(',.2s'); } // are we adding units after the value or before? if (args.yax_units_append) { return pf(d) + args.yax_units; } else { return args.yax_units + pf(d); } }; } else { // percentage yax_format = function yax_format(d_) { var n = d3.format('.0%'); return n(d_); }; } } return yax_format; } function mg_bar_add_zero_line(args) { var svg = mg_get_svg_child_of(args.target); var extents = args.scales.X.domain(); if (0 >= extents[0] && extents[1] >= 0) { var r = args.scales.Y.range(); var g = args.categorical_groups.length ? args.scales.YGROUP(args.categorical_groups[args.categorical_groups.length - 1]) : args.scales.YGROUP(); svg.append('svg:line').attr('x1', args.scales.X(0)).attr('x2', args.scales.X(0)).attr('y1', r[0] + mg_get_plot_top(args)).attr('y2', r[r.length - 1] + g).attr('stroke', 'black').attr('opacity', 0.2); } } function mg_y_domain_range(args, scale) { scale.domain([args.processed.min_y, args.processed.max_y]).range([mg_get_plot_bottom(args), args.top]); return scale; } function mg_define_y_scales(args) { var scale = mg_is_function(args.y_scale_type) ? args.y_scale_type() : args.y_scale_type === 'log' ? d3.scaleLog() : d3.scaleLinear(); if (args.y_scale_type === 'log') { if (args.chart_type === 'histogram') { // log histogram plots should start just below 1 // so that bins with single counts are visible args.processed.min_y = 0.2; } else { if (args.processed.min_y <= 0) { args.processed.min_y = 1; } } } args.scales.Y = mg_y_domain_range(args, scale); args.scales.Y.clamp(args.y_scale_type === 'log'); // used for ticks and such, and designed to be paired with log or linear args.scales.Y_axis = mg_y_domain_range(args, d3.scaleLinear()); } function mg_add_y_label(g, args) { if (args.y_label) { g.append('text').attr('class', 'label').attr('x', function () { return -1 * (mg_get_plot_top(args) + (mg_get_plot_bottom(args) - mg_get_plot_top(args)) / 2); }).attr('y', function () { return args.left / 2; }).attr('dy', '-1.2em').attr('text-anchor', 'middle').text(function (d) { return args.y_label; }).attr('transform', function (d) { return 'rotate(-90)'; }); } } function mg_add_y_axis_rim(g, args) { var tick_length = args.processed.y_ticks.length; if (!args.x_extended_ticks && !args.y_extended_ticks && tick_length) { var y1scale, y2scale; if (args.axes_not_compact && args.chart_type !== 'bar') { y1scale = args.height - args.bottom; y2scale = args.top; } else if (tick_length) { y1scale = args.scales.Y(args.processed.y_ticks[0]).toFixed(2); y2scale = args.scales.Y(args.processed.y_ticks[tick_length - 1]).toFixed(2); } else { y1scale = 0; y2scale = 0; } g.append('line').attr('x1', args.left).attr('x2', args.left).attr('y1', y1scale).attr('y2', y2scale); } } function mg_add_y_axis_tick_lines(g, args) { g.selectAll('.mg-yax-ticks').data(args.processed.y_ticks).enter().append('line').classed('mg-extended-yax-ticks', args.y_extended_ticks).attr('x1', args.left).attr('x2', function () { return args.y_extended_ticks ? args.width - args.right : args.left - args.yax_tick_length; }).attr('y1', function (d) { return args.scales.Y(d).toFixed(2); }).attr('y2', function (d) { return args.scales.Y(d).toFixed(2); }); } function mg_add_y_axis_tick_labels(g, args) { var yax_format = mg_compute_yax_format(args); g.selectAll('.mg-yax-labels').data(args.processed.y_ticks).enter().append('text').attr('x', args.left - args.yax_tick_length * 3 / 2).attr('dx', -3).attr('y', function (d) { return args.scales.Y(d).toFixed(2); }).attr('dy', '.35em').attr('text-anchor', 'end').text(function (d) { var o = yax_format(d); return o; }); } // TODO ought to be deprecated, only used by histogram function y_axis(args) { if (!args.processed) { args.processed = {}; } var svg = mg_get_svg_child_of(args.target); MG.call_hook('y_axis.process_min_max', args, args.processed.min_y, args.processed.max_y); mg_selectAll_and_remove(svg, '.mg-y-axis'); if (!args.y_axis) { return this; } var g = mg_add_g(svg, 'mg-y-axis'); mg_add_y_label(g, args); mg_process_scale_ticks(args, 'y'); mg_add_y_axis_rim(g, args); mg_add_y_axis_tick_lines(g, args); mg_add_y_axis_tick_labels(g, args); if (args.y_rug) { y_rug(args); } return this; } MG.y_axis = y_axis; function mg_add_categorical_labels(args) { var svg = mg_get_svg_child_of(args.target); mg_selectAll_and_remove(svg, '.mg-y-axis'); var g = mg_add_g(svg, 'mg-y-axis'); var group_g;(args.categorical_groups.length ? args.categorical_groups : ['1']).forEach(function (group) { group_g = mg_add_g(g, 'mg-group-' + mg_normalize(group)); if (args.ygroup_accessor !== null) { mg_add_group_label(group_g, group, args); } else { var labels = mg_add_graphic_labels(group_g, group, args); mg_rotate_labels(labels, args.rotate_y_labels); } }); } function mg_add_graphic_labels(g, group, args) { return g.selectAll('text').data(args.scales.Y.domain()).enter().append('svg:text').attr('x', args.left - args.buffer).attr('y', function (d) { return args.scales.YGROUP(group) + args.scales.Y(d) + args.scales.Y.bandwidth() / 2; }).attr('dy', '.35em').attr('text-anchor', 'end').text(String); } function mg_add_group_label(g, group, args) { g.append('svg:text').classed('mg-barplot-group-label', true).attr('x', args.left - args.buffer).attr('y', args.scales.YGROUP(group) + args.scales.YGROUP.bandwidth() / 2).attr('dy', '.35em').attr('text-anchor', 'end').text(group); } function mg_draw_group_lines(args) { var svg = mg_get_svg_child_of(args.target); var groups = args.scales.YGROUP.domain(); var first = groups[0]; var last = groups[groups.length - 1]; svg.select('.mg-category-guides').selectAll('mg-group-lines').data(groups).enter().append('line').attr('x1', mg_get_plot_left(args)).attr('x2', mg_get_plot_left(args)).attr('y1', function (d) { return args.scales.YGROUP(d); }).attr('y2', function (d) { return args.scales.YGROUP(d) + args.ygroup_height; }).attr('stroke-width', 1); } function mg_y_categorical_show_guides(args) { // for each group // for each data point var svg = mg_get_svg_child_of(args.target); var alreadyPlotted = []; args.data[0].forEach(function (d) { if (alreadyPlotted.indexOf(d[args.y_accessor]) === -1) { svg.select('.mg-category-guides').append('line').attr('x1', mg_get_plot_left(args)).attr('x2', mg_get_plot_right(args)).attr('y1', args.scalefns.yf(d) + args.scalefns.ygroupf(d)).attr('y2', args.scalefns.yf(d) + args.scalefns.ygroupf(d)).attr('stroke-dasharray', '2,1'); } }); } function y_axis_categorical(args) { if (!args.y_axis) { return this; } mg_add_categorical_labels(args); // mg_draw_group_scaffold(args); if (args.show_bar_zero) mg_bar_add_zero_line(args); if (args.ygroup_accessor) mg_draw_group_lines(args); if (args.y_categorical_show_guides) mg_y_categorical_show_guides(args); return this; } MG.y_axis_categorical = y_axis_categorical; function x_rug(args) { 'use strict'; if (!args.x_rug) { return; } args.rug_buffer_size = args.chart_type === 'point' ? args.buffer / 2 : args.buffer; var rug = mg_make_rug(args, 'mg-x-rug'); rug.attr('x1', args.scalefns.xf).attr('x2', args.scalefns.xf).attr('y1', args.height - args.bottom - args.rug_buffer_size).attr('y2', args.height - args.bottom); mg_add_color_accessor_to_rug(rug, args, 'mg-x-rug-mono'); } MG.x_rug = x_rug; function mg_add_processed_object(args) { if (!args.processed) { args.processed = {}; } } // TODO ought to be deprecated, only used by histogram function x_axis(args) { 'use strict'; var svg = mg_get_svg_child_of(args.target); mg_add_processed_object(args); mg_select_xax_format(args); mg_selectAll_and_remove(svg, '.mg-x-axis'); if (!args.x_axis) { return this; } var g = mg_add_g(svg, 'mg-x-axis'); mg_add_x_ticks(g, args); mg_add_x_tick_labels(g, args); if (args.x_label) { mg_add_x_label(g, args); } if (args.x_rug) { x_rug(args); } return this; } MG.x_axis = x_axis; function x_axis_categorical(args) { var svg = mg_get_svg_child_of(args.target); var additional_buffer = 0; if (args.chart_type === 'bar') { additional_buffer = args.buffer + 5; } mg_add_categorical_scale(args, 'X', args.categorical_variables.reverse(), args.left, mg_get_plot_right(args) - additional_buffer); mg_add_scale_function(args, 'xf', 'X', 'value'); mg_selectAll_and_remove(svg, '.mg-x-axis'); var g = mg_add_g(svg, 'mg-x-axis'); if (!args.x_axis) { return this; } mg_add_x_axis_categorical_labels(g, args, additional_buffer); return this; } function mg_add_x_axis_categorical_labels(g, args, additional_buffer) { var labels = g.selectAll('text').data(args.categorical_variables).enter().append('text'); labels.attr('x', function (d) { return args.scales.X(d) + args.scales.X.bandwidth() / 2 + args.buffer * args.bar_outer_padding_percentage + additional_buffer / 2; }).attr('y', mg_get_plot_bottom(args)).attr('dy', '.35em').attr('text-anchor', 'middle').text(String); if (args.truncate_x_labels) { labels.each(function (d, idx) { var elem = this, width = args.scales.X.bandwidth(); truncate_text(elem, d, width); }); } mg_rotate_labels(labels, args.rotate_x_labels); } MG.x_axis_categorical = x_axis_categorical; function mg_point_add_color_scale(args) { var color_domain, color_range; if (args.color_accessor !== null) { color_domain = mg_get_color_domain(args); color_range = mg_get_color_range(args); if (args.color_type === 'number') { args.scales.color = d3.scaleLinear().domain(color_domain).range(color_range).clamp(true); } else { args.scales.color = args.color_range !== null ? d3.scaleOrdinal().range(color_range) : color_domain.length > 10 ? d3.scaleOrdinal(d3.schemeCategory20) : d3.scaleOrdinal(d3.schemeCategory10); args.scales.color.domain(color_domain); } mg_add_scale_function(args, 'color', 'color', args.color_accessor); } } function mg_get_color_domain(args) { var color_domain; if (args.color_domain === null) { if (args.color_type === 'number') { color_domain = d3.extent(args.data[0], function (d) { return d[args.color_accessor]; }); } else if (args.color_type === 'category') { color_domain = d3.set(args.data[0].map(function (d) { return d[args.color_accessor]; })).values(); color_domain.sort(); } } else { color_domain = args.color_domain; } return color_domain; } function mg_get_color_range(args) { var color_range; if (args.color_range === null) { if (args.color_type === 'number') { color_range = ['blue', 'red']; } else { color_range = null; } } else { color_range = args.color_range; } return color_range; } function mg_point_add_size_scale(args) { var min_size, max_size, size_domain, size_range; if (args.size_accessor !== null) { size_domain = mg_get_size_domain(args); size_range = mg_get_size_range(args); args.scales.size = d3.scaleLinear().domain(size_domain).range(size_range).clamp(true); mg_add_scale_function(args, 'size', 'size', args.size_accessor); } } function mg_get_size_domain(args) { return args.size_domain === null ? d3.extent(args.data[0], function (d) { return d[args.size_accessor]; }) : args.size_domain; } function mg_get_size_range(args) { var size_range; if (args.size_range === null) { size_range = [1, 5]; } else { size_range = args.size_range; } return size_range; } function mg_add_x_label(g, args) { if (args.x_label) { g.append('text').attr('class', 'label').attr('x', function () { return mg_get_plot_left(args) + (mg_get_plot_right(args) - mg_get_plot_left(args)) / 2; }).attr('dx', args.x_label_nudge_x != null ? args.x_label_nudge_x : 0).attr('y', function () { var xAxisTextElement = d3.select(args.target).select('.mg-x-axis text').node().getBoundingClientRect(); return mg_get_bottom(args) + args.xax_tick_length * (7 / 3) + xAxisTextElement.height * 0.8 + 10; }).attr('dy', '.5em').attr('text-anchor', 'middle').text(function (d) { return args.x_label; }); } } function mg_default_bar_xax_format(args) { return function (d) { if (d < 1.0 && d > -1.0 && d !== 0) { // don't scale tiny values return args.xax_units + d.toFixed(args.decimals); } else { var pf = d3.format(',.0f'); return args.xax_units + pf(d); } }; } function mg_get_time_frame(diff) { // diff should be (max_x - min_x) / 1000, in other words, the difference in seconds. var time_frame; if (mg_milisec_diff(diff)) { time_frame = 'millis'; } else if (mg_sec_diff(diff)) { time_frame = 'seconds'; } else if (mg_day_diff(diff)) { time_frame = 'less-than-a-day'; } else if (mg_four_days(diff)) { time_frame = 'four-days'; } else if (mg_many_days(diff)) { // a handful of months? time_frame = 'many-days'; } else if (mg_many_months(diff)) { time_frame = 'many-months'; } else if (mg_years(diff)) { time_frame = 'years'; } else { time_frame = 'default'; } return time_frame; } function mg_milisec_diff(diff) { return diff < 1; } function mg_sec_diff(diff) { return diff < 60; } function mg_day_diff(diff) { return diff / (60 * 60) < 24; } function mg_four_days(diff) { return diff / (60 * 60) < 24 * 4; } function mg_many_days(diff) { return diff / (60 * 60 * 24) < 60; } function mg_many_months(diff) { return diff / (60 * 60 * 24) < 365; } function mg_years(diff) { return diff / (60 * 60 * 24) >= 365; } function mg_get_time_format(utc, diff) { var main_time_format; if (mg_milisec_diff(diff)) { main_time_format = MG.time_format(utc, '%M:%S.%L'); } else if (mg_sec_diff(diff)) { main_time_format = MG.time_format(utc, '%M:%S'); } else if (mg_day_diff(diff)) { main_time_format = MG.time_format(utc, '%H:%M'); } else if (mg_four_days(diff) || mg_many_days(diff)) { main_time_format = MG.time_format(utc, '%b %d'); } else if (mg_many_months(diff)) { main_time_format = MG.time_format(utc, '%b'); } else { main_time_format = MG.time_format(utc, '%Y'); } return main_time_format; } function mg_process_time_format(args) { if (args.time_series) { var diff = (args.processed.max_x - args.processed.min_x) / 1000; var tickDiff = (args.processed.x_ticks[1] - args.processed.x_ticks[0]) / 1000; args.processed.x_time_frame = mg_get_time_frame(diff); args.processed.x_tick_diff_time_frame = mg_get_time_frame(tickDiff); args.processed.main_x_time_format = mg_get_time_format(args.utc_time, tickDiff); } } function mg_default_xax_format(args) { if (args.xax_format) { return args.xax_format; } var data = args.processed.original_data || args.data; var flattened = mg_flatten_array(data)[0]; var test_point_x = flattened[args.processed.original_x_accessor || args.x_accessor]; if (test_point_x === undefined) { test_point_x = flattened; } return function (d) { mg_process_time_format(args); if (mg_is_date(test_point_x)) { return args.processed.main_x_time_format(new Date(d)); } else if (typeof test_point_x === 'number') { var is_float = d % 1 !== 0; var pf; if (is_float) { pf = d3.format(',.' + args.decimals + 'f'); } else if (d < 1000) { pf = d3.format(',.0f'); } else { pf = d3.format(',.2s'); } return args.xax_units + pf(d); } else { return args.xax_units + d; } }; } function mg_add_x_ticks(g, args) { mg_process_scale_ticks(args, 'x'); mg_add_x_axis_rim(args, g); mg_add_x_axis_tick_lines(args, g); } function mg_add_x_axis_rim(args, g) { var last_i = args.scales.X.ticks(args.xax_count).length - 1; if (!args.x_extended_ticks) { g.append('line').attr('x1', function () { if (args.xax_count === 0) { return mg_get_plot_left(args); } else if (args.axes_not_compact && args.chart_type !== 'bar') { return args.left; } else { return args.scales.X(args.scales.X.ticks(args.xax_count)[0]).toFixed(2); } }).attr('x2', function () { if (args.xax_count === 0 || args.axes_not_compact && args.chart_type !== 'bar') { return mg_get_right(args); } else { return args.scales.X(args.scales.X.ticks(args.xax_count)[last_i]).toFixed(2); } }).attr('y1', args.height - args.bottom).attr('y2', args.height - args.bottom); } } function mg_add_x_axis_tick_lines(args, g) { g.selectAll('.mg-xax-ticks').data(args.processed.x_ticks).enter().append('line').attr('x1', function (d) { return args.scales.X(d).toFixed(2); }).attr('x2', function (d) { return args.scales.X(d).toFixed(2); }).attr('y1', args.height - args.bottom).attr('y2', function () { return args.x_extended_ticks ? args.top : args.height - args.bottom + args.xax_tick_length; }).attr('class', function () { if (args.x_extended_ticks) { return 'mg-extended-xax-ticks'; } }).classed('mg-xax-ticks', true); } function mg_add_x_tick_labels(g, args) { mg_add_primary_x_axis_label(args, g); mg_add_secondary_x_axis_label(args, g); } function mg_add_primary_x_axis_label(args, g) { var labels = g.selectAll('.mg-xax-labels').data(args.processed.x_ticks).enter().append('text').attr('x', function (d) { return args.scales.X(d).toFixed(2); }).attr('y', (args.height - args.bottom + args.xax_tick_length * 7 / 3).toFixed(2)).attr('dy', '.50em').attr('text-anchor', 'middle'); if (args.time_series && args.european_clock) { labels.append('tspan').classed('mg-european-hours', true).text(function (_d, i) { var d = new Date(_d); if (i === 0) return d3.timeFormat('%H')(d);else return ''; }); labels.append('tspan').classed('mg-european-minutes-seconds', true).text(function (_d, i) { var d = new Date(_d); return ':' + args.processed.xax_format(d); }); } else { labels.text(function (d) { return args.xax_units + args.processed.xax_format(d); }); } // CHECK TO SEE IF OVERLAP for labels. If so, // remove half of them. This is a dirty hack. // We will need to figure out a more principled way of doing this. if (mg_elements_are_overlapping(labels)) { labels.filter(function (d, i) { return (i + 1) % 2 === 0; }).remove(); var svg = mg_get_svg_child_of(args.target); svg.selectAll('.mg-xax-ticks').filter(function (d, i) { return (i + 1) % 2 === 0; }).remove(); } } function mg_add_secondary_x_axis_label(args, g) { if (args.time_series && (args.show_years || args.show_secondary_x_label)) { mg_add_secondary_x_axis_elements(args, g); } } function mg_get_yformat_and_secondary_time_function(args) { var tf = { timeframe: args.processed.x_time_frame, tick_diff_timeframe: args.processed.x_tick_diff_time_frame }; switch (tf.timeframe) { case 'millis': case 'seconds': tf.secondary = d3.timeDays; if (args.european_clock) tf.yformat = MG.time_format(args.utc_time, '%b %d');else tf.yformat = MG.time_format(args.utc_time, '%I %p'); break; case 'less-than-a-day': tf.secondary = d3.timeDays; tf.yformat = MG.time_format(args.utc_time, '%b %d'); break; case 'four-days': tf.secondary = d3.timeDays; tf.yformat = MG.time_format(args.utc_time, '%b %d'); break; case 'many-days': tf.secondary = d3.timeYears; tf.yformat = MG.time_format(args.utc_time, '%Y'); break; case 'many-months': tf.secondary = d3.timeYears; tf.yformat = MG.time_format(args.utc_time, '%Y'); break; default: tf.secondary = d3.timeYears; tf.yformat = MG.time_format(args.utc_time, '%Y'); } return tf; } function mg_add_secondary_x_axis_elements(args, g) { var tf = mg_get_yformat_and_secondary_time_function(args); var years = tf.secondary(args.processed.min_x, args.processed.max_x); if (years.length === 0) { var first_tick = args.scales.X.ticks(args.xax_count)[0]; years = [first_tick]; } var yg = mg_add_g(g, 'mg-year-marker'); if (tf.timeframe === 'default' && args.show_year_markers) { mg_add_year_marker_line(args, yg, years, tf.yformat); } if (tf.tick_diff_time_frame != 'years') mg_add_year_marker_text(args, yg, years, tf.yformat); } function mg_add_year_marker_line(args, g, years, yformat) { g.selectAll('.mg-year-marker').data(years).enter().append('line').attr('x1', function (d) { return args.scales.X(d).toFixed(2); }).attr('x2', function (d) { return args.scales.X(d).toFixed(2); }).attr('y1', mg_get_top(args)).attr('y2', mg_get_bottom(args)); } function mg_add_year_marker_text(args, g, years, yformat) { g.selectAll('.mg-year-marker').data(years).enter().append('text').attr('x', function (d, i) { return args.scales.X(d).toFixed(2); }).attr('y', function () { var xAxisTextElement = d3.select(args.target).select('.mg-x-axis text').node().getBoundingClientRect(); return mg_get_bottom(args) + args.xax_tick_length * 7 / 3 + xAxisTextElement.height * 0.8; }).attr('dy', '.50em').attr('text-anchor', 'middle').text(function (d) { return yformat(new Date(d)); }); } function mg_min_max_x_for_nonbars(mx, args, data) { var extent_x = d3.extent(data, function (d) { return d[args.x_accessor]; }); mx.min = extent_x[0]; mx.max = extent_x[1]; } function mg_min_max_x_for_bars(mx, args, data) { mx.min = d3.min(data, function (d) { var trio = [d[args.x_accessor], d[args.baseline_accessor] ? d[args.baseline_accessor] : 0, d[args.predictor_accessor] ? d[args.predictor_accessor] : 0]; return Math.min.apply(null, trio); }); if (mx.min > 0) mx.min = 0; mx.max = d3.max(data, function (d) { var trio = [d[args.x_accessor], d[args.baseline_accessor] ? d[args.baseline_accessor] : 0, d[args.predictor_accessor] ? d[args.predictor_accessor] : 0]; return Math.max.apply(null, trio); }); return mx; } function mg_min_max_x_for_dates(mx) { var yesterday = MG.clone(mx.min).setDate(mx.min.getDate() - 1); var tomorrow = MG.clone(mx.min).setDate(mx.min.getDate() + 1); mx.min = yesterday; mx.max = tomorrow; } function mg_min_max_x_for_numbers(mx) { // TODO do we want to rewrite this? mx.min = mx.min - 1; mx.max = mx.max + 1; } function mg_min_max_x_for_strings(mx) { // TODO shouldn't be allowing strings here to be coerced into numbers mx.min = Number(mx.min) - 1; mx.max = Number(mx.max) + 1; } function mg_force_xax_count_to_be_two(args) { args.xax_count = 2; } function mg_select_xax_format(args) { var c = args.chart_type; if (!args.processed.xax_format) { if (args.xax_format) { args.processed.xax_format = args.xax_format; } else { if (c === 'line' || c === 'point' || c === 'histogram') { args.processed.xax_format = mg_default_xax_format(args); } else if (c === 'bar') { args.processed.xax_format = mg_default_bar_xax_format(args); } } } } function mg_merge_args_with_defaults(args) { var defaults = { target: null, title: null, description: null }; if (!args) { args = {}; } if (!args.processed) { args.processed = {}; } args = merge_with_defaults(args, defaults); return args; } function mg_is_time_series(args) { var first_elem = mg_flatten_array(args.processed.original_data || args.data)[0]; args.time_series = mg_is_date(first_elem[args.processed.original_x_accessor || args.x_accessor]); } function mg_init_compute_width(args) { var svg_width = parseInt(args.width); if (args.full_width) { svg_width = get_width(args.target); } if (args.x_axis_type === 'categorical' && svg_width === null) { svg_width = mg_categorical_calculate_height(args, 'x'); } args.width = svg_width; } function mg_init_compute_height(args) { var svg_height = parseInt(args.height); if (args.full_height) { svg_height = get_height(args.target); } if (args.y_axis_type === 'categorical' && svg_height === null) { svg_height = mg_categorical_calculate_height(args, 'y'); } args.height = svg_height; } function mg_remove_svg_if_chart_type_has_changed(svg, args) { if (!svg.selectAll('.mg-main-line').empty() && args.chart_type !== 'line' || !svg.selectAll('.mg-points').empty() && args.chart_type !== 'point' || !svg.selectAll('.mg-histogram').empty() && args.chart_type !== 'histogram' || !svg.selectAll('.mg-barplot').empty() && args.chart_type !== 'bar') { svg.remove(); } } function mg_add_svg_if_it_doesnt_exist(svg, args) { if (mg_get_svg_child_of(args.target).empty()) { svg = d3.select(args.target).append('svg').classed('linked', args.linked).attr('width', args.width).attr('height', args.height); } return svg; } function mg_add_clip_path_for_plot_area(svg, args) { svg.selectAll('.mg-clip-path').remove(); svg.append('defs').attr('class', 'mg-clip-path').append('clipPath').attr('id', 'mg-plot-window-' + mg_target_ref(args.target)).append('svg:rect').attr('x', mg_get_left(args)).attr('y', mg_get_top(args)).attr('width', args.width - args.left - args.right - args.buffer).attr('height', args.height - args.top - args.bottom - args.buffer + 1); } function mg_adjust_width_and_height_if_changed(svg, args) { if (args.width !== Number(svg.attr('width'))) { svg.attr('width', args.width); } if (args.height !== Number(svg.attr('height'))) { svg.attr('height', args.height); } } function mg_set_viewbox_for_scaling(svg, args) { // we need to reconsider how we handle automatic scaling svg.attr('viewBox', '0 0 ' + args.width + ' ' + args.height); if (args.full_width || args.full_height) { svg.attr('preserveAspectRatio', 'xMinYMin meet'); } } function mg_remove_missing_classes_and_text(svg) { // remove missing class svg.classed('mg-missing', false); // remove missing text svg.selectAll('.mg-missing-text').remove(); svg.selectAll('.mg-missing-pane').remove(); } function mg_remove_outdated_lines(svg, args) { // if we're updating an existing chart and we have fewer lines than // before, remove the outdated lines, e.g. if we had 3 lines, and we're calling // data_graphic() on the same target with 2 lines, remove the 3rd line var i = 0; if (svg.selectAll('.mg-main-line').nodes().length >= args.data.length) { // now, the thing is we can't just remove, say, line3 if we have a custom // line-color map, instead, see which are the lines to be removed, and delete those if (args.custom_line_color_map.length > 0) { var array_full_series = function array_full_series(len) { var arr = new Array(len); for (var i = 0; i < arr.length; i++) { arr[i] = i + 1; } return arr; }; // get an array of lines ids to remove var lines_to_remove = arr_diff(array_full_series(args.max_data_size), args.custom_line_color_map); for (i = 0; i < lines_to_remove.length; i++) { svg.selectAll('.mg-main-line.mg-line' + lines_to_remove[i] + '-color').remove(); } } else { // if we don't have a custom line-color map, just remove the lines from the end var num_of_new = args.data.length; var num_of_existing = svg.selectAll('.mg-main-line').nodes() ? svg.selectAll('.mg-main-line').nodes().length : 0; for (i = num_of_existing; i > num_of_new; i--) { svg.selectAll('.mg-main-line.mg-line' + i + '-color').remove(); } } } } function mg_raise_container_error(container, args) { if (container.empty()) { console.warn('The specified target element "' + args.target + '" could not be found in the page. The chart will not be rendered.'); return; } } function categoricalInitialization(args, ns) { var which = ns === 'x' ? args.width : args.height; mg_categorical_count_number_of_groups(args, ns); mg_categorical_count_number_of_lanes(args, ns); mg_categorical_calculate_group_length(args, ns, which); if (which) mg_categorical_calculate_bar_thickness(args, ns); } function selectXaxFormat(args) { var c = args.chart_type; if (!args.processed.xax_format) { if (args.xax_format) { args.processed.xax_format = args.xax_format; } else { if (c === 'line' || c === 'point' || c === 'histogram') { args.processed.xax_format = mg_default_xax_format(args); } else if (c === 'bar') { args.processed.xax_format = mg_default_bar_xax_format(args); } } } } function mg_categorical_count_number_of_groups(args, ns) { var accessor_string = ns + 'group_accessor'; var accessor = args[accessor_string]; args.categorical_groups = []; if (accessor) { var data = args.data[0]; args.categorical_groups = d3.set(data.map(function (d) { return d[accessor]; })).values(); } } function mg_categorical_count_number_of_lanes(args, ns) { var accessor_string = ns + 'group_accessor'; var groupAccessor = args[accessor_string]; args.total_bars = args.data[0].length; if (groupAccessor) { var group_bars = count_array_elements(pluck(args.data[0], groupAccessor)); group_bars = d3.max(Object.keys(group_bars).map(function (d) { return group_bars[d]; })); args.bars_per_group = group_bars; } else { args.bars_per_group = args.data[0].length; } } function mg_categorical_calculate_group_length(args, ns, which) { var groupHeight = ns + 'group_height'; if (which) { var gh = ns === 'y' ? (args.height - args.top - args.bottom - args.buffer * 2) / (args.categorical_groups.length || 1) : (args.width - args.left - args.right - args.buffer * 2) / (args.categorical_groups.length || 1); args[groupHeight] = gh; } else { var step = (1 + args[ns + '_padding_percentage']) * args.bar_thickness; args[groupHeight] = args.bars_per_group * step + args[ns + '_outer_padding_percentage'] * 2 * step; //args.bar_thickness + (((args.bars_per_group-1) * args.bar_thickness) * (args.bar_padding_percentage + args.bar_outer_padding_percentage*2)); } } function mg_categorical_calculate_bar_thickness(args, ns) { // take one group height. var step = args[ns + 'group_height'] / (args.bars_per_group + args[ns + '_outer_padding_percentage']); args.bar_thickness = step - step * args[ns + '_padding_percentage']; } function mg_categorical_calculate_height(args, ns) { var groupContribution = args[ns + 'group_height'] * (args.categorical_groups.length || 1); var marginContribution = ns === 'y' ? args.top + args.bottom + args.buffer * 2 : args.left + args.right + args.buffer * 2; return groupContribution + marginContribution + args.categorical_groups.length * args[ns + 'group_height'] * (args[ns + 'group_padding_percentage'] + args[ns + 'group_outer_padding_percentage']); } function mg_barchart_extrapolate_group_and_thickness_from_height(args) { // we need to set args.bar_thickness, group_height } function init(args) { 'use strict'; args = arguments[0]; args = mg_merge_args_with_defaults(args); // If you pass in a dom element for args.target, the expectation // of a string elsewhere will break. var container = d3.select(args.target); mg_raise_container_error(container, args); var svg = container.selectAll('svg'); // some things that will need to be calculated if we have a categorical axis. if (args.y_axis_type === 'categorical') { categoricalInitialization(args, 'y'); } if (args.x_axis_type === 'categorical') { categoricalInitialization(args, 'x'); } selectXaxFormat(args); mg_is_time_series(args); mg_init_compute_width(args); mg_init_compute_height(args); mg_remove_svg_if_chart_type_has_changed(svg, args); svg = mg_add_svg_if_it_doesnt_exist(svg, args); mg_add_clip_path_for_plot_area(svg, args); mg_adjust_width_and_height_if_changed(svg, args); mg_set_viewbox_for_scaling(svg, args); mg_remove_missing_classes_and_text(svg); chart_title(args); mg_remove_outdated_lines(svg, args); return this; } MG.init = init; function mg_return_label(d) { return d.label; } function mg_remove_existing_markers(svg) { svg.selectAll('.mg-markers').remove(); svg.selectAll('.mg-baselines').remove(); } function mg_in_range(args) { return function (d) { return args.scales.X(d[args.x_accessor]) >= mg_get_plot_left(args) && args.scales.X(d[args.x_accessor]) <= mg_get_plot_right(args); }; } function mg_x_position(args) { return function (d) { return args.scales.X(d[args.x_accessor]); }; } function mg_x_position_fixed(args) { var _mg_x_pos = mg_x_position(args); return function (d) { return _mg_x_pos(d).toFixed(2); }; } function mg_y_position_fixed(args) { var _mg_y_pos = args.scales.Y; return function (d) { return _mg_y_pos(d.value).toFixed(2); }; } function mg_place_annotations(checker, class_name, args, svg, line_fcn, text_fcn) { var g; if (checker) { g = svg.append('g').attr('class', class_name); line_fcn(g, args); text_fcn(g, args); } } function mg_place_markers(args, svg) { mg_place_annotations(args.markers, 'mg-markers', args, svg, mg_place_marker_lines, mg_place_marker_text); } function mg_place_baselines(args, svg) { mg_place_annotations(args.baselines, 'mg-baselines', args, svg, mg_place_baseline_lines, mg_place_baseline_text); } function mg_place_marker_lines(gm, args) { var x_pos_fixed = mg_x_position_fixed(args); gm.selectAll('.mg-markers').data(args.markers.filter(mg_in_range(args))).enter().append('line').attr('x1', x_pos_fixed).attr('x2', x_pos_fixed).attr('y1', args.top).attr('y2', mg_get_plot_bottom(args)).attr('class', function (d) { return d.lineclass; }).attr('stroke-dasharray', '3,1'); } function mg_place_marker_text(gm, args) { gm.selectAll('.mg-markers').data(args.markers.filter(mg_in_range(args))).enter().append('text').attr('class', function (d) { return d.textclass || ''; }).classed('mg-marker-text', true).attr('x', mg_x_position(args)).attr('y', args.x_axis_position === 'bottom' ? mg_get_top(args) * 0.95 : mg_get_bottom(args) + args.buffer).attr('text-anchor', 'middle').text(mg_return_label).each(function (d) { if (d.click) { d3.select(this).style('cursor', 'pointer').on('click', d.click); } if (d.mouseover) { d3.select(this).style('cursor', 'pointer').on('mouseover', d.mouseover); } if (d.mouseout) { d3.select(this).style('cursor', 'pointer').on('mouseout', d.mouseout); } }); mg_prevent_horizontal_overlap(gm.selectAll('.mg-marker-text').nodes(), args); } function mg_place_baseline_lines(gb, args) { var y_pos = mg_y_position_fixed(args); gb.selectAll('.mg-baselines').data(args.baselines).enter().append('line').attr('x1', mg_get_plot_left(args)).attr('x2', mg_get_plot_right(args)).attr('y1', y_pos).attr('y2', y_pos); } function mg_place_baseline_text(gb, args) { var y_pos = mg_y_position_fixed(args); gb.selectAll('.mg-baselines').data(args.baselines).enter().append('text').attr('x', mg_get_plot_right(args)).attr('y', y_pos).attr('dy', -3).attr('text-anchor', 'end').text(mg_return_label); } function markers(args) { 'use strict'; var svg = mg_get_svg_child_of(args.target); mg_remove_existing_markers(svg); mg_place_markers(args, svg); mg_place_baselines(args, svg); return this; } MG.markers = markers; function mg_clear_mouseover_container(svg) { svg.selectAll('.mg-active-datapoint-container').selectAll('*').remove(); } function mg_setup_mouseover_container(svg, args) { svg.select('.mg-active-datapoint').remove(); var text_anchor = args.mouseover_align === 'right' ? 'end' : args.mouseover_align === 'left' ? 'start' : 'middle'; var mouseover_x = args.mouseover_align === 'right' ? mg_get_plot_right(args) : args.mouseover_align === 'left' ? mg_get_plot_left(args) : (args.width - args.left - args.right) / 2 + args.left; var active_datapoint = svg.select('.mg-active-datapoint-container').append('text').attr('class', 'mg-active-datapoint').attr('xml:space', 'preserve').attr('text-anchor', text_anchor); // set the rollover text's position; if we have markers on two lines, // nudge up the rollover text a bit var active_datapoint_y_nudge = 0.75; var y_position = args.x_axis_position === 'bottom' ? mg_get_top(args) * active_datapoint_y_nudge : mg_get_bottom(args) + args.buffer * 3; if (args.markers) { var yPos; svg.selectAll('.mg-marker-text').each(function () { if (!yPos) { yPos = d3.select(this).attr('y'); } else if (yPos !== d3.select(this).attr('y')) { active_datapoint_y_nudge = 0.56; } }); } active_datapoint.attr('transform', 'translate(' + mouseover_x + ',' + y_position + ')'); } function mg_mouseover_tspan(svg, text) { var tspan = svg.append('tspan').text(text); return { bold: function bold() { return tspan.attr('font-weight', 'bold'); }, font_size: function font_size(pts) { return tspan.attr('font-size', pts); }, x: function x(_x2) { return tspan.attr('x', _x2); }, y: function y(_y2) { return tspan.attr('y', _y2); }, elem: tspan }; } function mg_reset_text_container(svg) { var textContainer = svg.select('.mg-active-datapoint'); textContainer.selectAll('*').remove(); return textContainer; } function mg_mouseover_row(row_number, container, rargs) { var lineHeight = 1.1; var rrr = container.append('tspan').attr('x', 0).attr('y', row_number * lineHeight + 'em'); return { rargs: rargs, text: function text(_text) { return mg_mouseover_tspan(rrr, _text); } }; } function mg_mouseover_text(args, rargs) { mg_setup_mouseover_container(rargs.svg, args); var mouseOver = { row_number: 0, rargs: rargs, mouseover_row: function mouseover_row(rargs) { mouseOver.row_number += 1; return mg_mouseover_row(mouseOver.row_number, mouseOver.text_container, rargs); }, text_container: mg_reset_text_container(rargs.svg) }; return mouseOver; } { var filter_in_range_data = function filter_in_range_data(args, range) { var is_data_in_range = function is_data_in_range(data, range) { return data > Math.min(range[0], range[1]) && data < Math.max(range[0], range[1]); }; // if range without this axis return true, else judge is data in range or not. return function (d) { return ['x', 'y'].every(function (dim) { return !(dim in range) || is_data_in_range(d[args[dim + '_accessor']], range[dim]); }); }; }; // the range here is the range of data // range is an object with two optional attributes of x,y, respectively represent ranges on two axes var zoom_to_data_domain = function zoom_to_data_domain(args, range) { var raw_data = args.processed.raw_data || args.data; // store raw data and raw domain to in order to zoom back to the initial state if (!('raw_data' in args.processed)) { args.processed.raw_domain = { x: args.scales.X.domain(), y: args.scales.Y.domain() }; args.processed.raw_data = raw_data; } // to avoid drawing outside the chart in the point chart, unnecessary in line chart. if (args.chart_type === 'point') { if (is_array_of_arrays(raw_data)) { args.data = raw_data.map(function (d) { return d.filter(filter_in_range_data(args, range)); }); } else { args.data = raw_data.filter(filter_in_range_data(args, range)); } } ['x', 'y'].forEach(function (dim) { if (dim in range) args.processed['zoom_' + dim] = range[dim];else delete args.processed['zoom_' + dim]; }); if (args.processed.subplot) { if (range !== args.processed.raw_domain) { MG.create_brushing_pattern(args.processed.subplot, convert_domain_to_range(args.processed.subplot, range)); } else { MG.remove_brushing_pattern(args.processed.subplot); } } new MG.charts[args.chart_type || defaults.chart_type].descriptor(args); }; var zoom_to_raw_range = function zoom_to_raw_range(args) { if (!('raw_domain' in args.processed)) return; zoom_to_data_domain(args, args.processed.raw_domain); delete args.processed.raw_domain; delete args.processed.raw_data; }; // converts the range of selection into the range of data that we can use to // zoom the chart to a particular region var convert_range_to_domain = function convert_range_to_domain(args, range) { return ['x', 'y'].reduce(function (domain, dim) { if (!(dim in range)) return domain; domain[dim] = range[dim].map(function (v) { return +args.scales[dim.toUpperCase()].invert(v); }); if (dim === 'y') domain[dim].reverse(); return domain; }, {}); }; var convert_domain_to_range = function convert_domain_to_range(args, domain) { return ['x', 'y'].reduce(function (range, dim) { if (!(dim in domain)) return range; range[dim] = domain[dim].map(function (v) { return +args.scales[dim.toUpperCase()](v); }); if (dim === 'y') range[dim].reverse(); return range; }, {}); }; // the range here is the range of selection var zoom_to_data_range = function zoom_to_data_range(args, range) { var domain = convert_range_to_domain(args, range); zoom_to_data_domain(args, domain); }; MG.convert_range_to_domain = convert_range_to_domain; MG.zoom_to_data_domain = zoom_to_data_domain; MG.zoom_to_data_range = zoom_to_data_range; MG.zoom_to_raw_range = zoom_to_raw_range; } { var get_extent_rect = function get_extent_rect(args) { return d3.select(args.target).select('.mg-extent').size() ? d3.select(args.target).select('.mg-extent') : d3.select(args.target).select('.mg-rollover-rect, .mg-voronoi').insert('g', '*').classed('mg-brush', true).append('rect').classed('mg-extent', true); }; var create_brushing_pattern = function create_brushing_pattern(args, range) { var x = range.x[0]; var width = range.x[1] - range.x[0]; var y = range.y[0]; var height = range.y[1] - range.y[0]; get_extent_rect(args).attr('x', x).attr('width', width).attr('y', y).attr('height', height).attr('opacity', 1); }; var remove_brushing_pattern = function remove_brushing_pattern(args) { get_extent_rect(args).attr('width', 0).attr('height', 0).attr('opacity', 0); }; var add_event_handler_for_brush = function add_event_handler_for_brush(args, target, axis) { var svg = d3.select(args.target).select('svg'); var rollover = svg.select('.mg-rollover-rect, .mg-voronoi'); var container = rollover.node(); var isDragging = false; var mouseDown = false; var origin = []; var calculateSelectionRange = function calculateSelectionRange() { var min_x = args.left; var max_x = args.width - args.right - args.buffer; var min_y = args.top; var max_y = args.height - args.bottom - args.buffer; var mouse = d3.mouse(container); var range = {}; range.x = axis.x ? [Math.max(min_x, Math.min(origin[0], mouse[0])), Math.min(max_x, Math.max(origin[0], mouse[0]))] : [min_x, max_x]; range.y = axis.y ? [Math.max(min_y, Math.min(origin[1], mouse[1])), Math.min(max_y, Math.max(origin[1], mouse[1]))] : [min_y, max_y]; return range; }; rollover.classed('mg-brush-container', true); rollover.on('mousedown.' + args.target, function () { mouseDown = true; isDragging = false; origin = d3.mouse(container); svg.classed('mg-brushed', false); svg.classed('mg-brushing-in-progress', true); remove_brushing_pattern(args); }); d3.select(document).on('mousemove.' + args.target, function () { if (mouseDown) { isDragging = true; rollover.classed('mg-brushing', true); create_brushing_pattern(args, calculateSelectionRange()); } }); d3.select(document).on('mouseup.' + args.target, function () { if (!mouseDown) return; mouseDown = false; svg.classed('mg-brushing-in-progress', false); var range = calculateSelectionRange(); if (isDragging) { isDragging = false; if (target === args) { MG.zoom_to_data_range(target, range); svg.select('.mg-rollover-rect, .mg-voronoi').classed('mg-brushed', true); } else { var domain = MG.convert_range_to_domain(args, range); MG.zoom_to_data_domain(target, domain); } } else { MG.zoom_to_raw_range(target); } if (mg_is_function(args.brushing_selection_changed)) args.brushing_selection_changed(args, range); }); }; var add_brush_function = function add_brush_function(args) { if (args.x_axis_type === 'categorical' || args.y_axis_type === 'categorical') return console.warn('The option "brush" does not support axis type "categorical" currently.'); if (!args.zoom_target) args.zoom_target = args; if (args.zoom_target !== args) args.zoom_target.processed.subplot = args; var brush_axis = void 0; switch (args.brush) { case 'x': brush_axis = { x: true, y: false }; break; case 'y': brush_axis = { x: false, y: true }; break; case 'xy': brush_axis = { x: true, y: true }; break; default: brush_axis = { x: true, y: true }; } add_event_handler_for_brush(args, args.zoom_target, brush_axis); }; MG.add_brush_function = add_brush_function; MG.create_brushing_pattern = create_brushing_pattern; MG.remove_brushing_pattern = remove_brushing_pattern; } function MG_WindowResizeTracker() { var targets = []; var Observer; if (typeof MutationObserver !== "undefined") { Observer = MutationObserver; } else if (typeof WebKitMutationObserver !== "undefined") { Observer = WebKitMutationObserver; } function window_listener() { targets.forEach(function (target) { var svg = d3.select(target).select('svg'); // skip if svg is not visible if (!svg.empty() && (svg.node().parentNode.offsetWidth > 0 || svg.node().parentNode.offsetHeight > 0)) { var aspect = svg.attr('width') !== 0 ? svg.attr('height') / svg.attr('width') : 0; var newWidth = get_width(target); svg.attr('width', newWidth); svg.attr('height', aspect * newWidth); } }); } function remove_target(target) { var index = targets.indexOf(target); if (index !== -1) { targets.splice(index, 1); } if (targets.length === 0) { window.removeEventListener('resize', window_listener, true); } } return { add_target: function add_target(target) { if (targets.length === 0) { window.addEventListener('resize', window_listener, true); } if (targets.indexOf(target) === -1) { targets.push(target); if (Observer) { var observer = new Observer(function (mutations) { var targetNode = d3.select(target).node(); if (!targetNode || mutations.some(function (mutation) { for (var i = 0; i < mutation.removedNodes.length; i++) { if (mutation.removedNodes[i] === targetNode) { return true; } } })) { observer.disconnect(); remove_target(target); } }); observer.observe(d3.select(target).node().parentNode, { childList: true }); } } } }; } var mg_window_resize_tracker = new MG_WindowResizeTracker(); function mg_window_listeners(args) { mg_if_aspect_ratio_resize_svg(args); } function mg_if_aspect_ratio_resize_svg(args) { // have we asked the svg to fill a div, if so resize with div if (args.full_width || args.full_height) { mg_window_resize_tracker.add_target(args.target); } } if (mg_jquery_exists()) { /*! * Bootstrap v3.3.1 (http://getbootstrap.com) * Copyright 2011-2014 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ /*! * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=c3834cc5b59ef727da53) * Config saved to config.json and https://gist.github.com/c3834cc5b59ef727da53 */ /* ======================================================================== * Bootstrap: dropdown.js v3.3.1 * http://getbootstrap.com/javascript/#dropdowns * ======================================================================== * Copyright 2011-2014 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; if (typeof $().dropdown == 'function') return true; // DROPDOWN CLASS DEFINITION // ========================= var backdrop = '.dropdown-backdrop'; var toggle = '[data-toggle="dropdown"]'; var Dropdown = function Dropdown(element) { $(element).on('click.bs.dropdown', this.toggle); }; Dropdown.VERSION = '3.3.1'; Dropdown.prototype.toggle = function (e) { var $this = $(this); if ($this.is('.disabled, :disabled')) return; var $parent = getParent($this); var isActive = $parent.hasClass('open'); clearMenus(); if (!isActive) { if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { // if mobile we use a backdrop because click events don't delegate $('