From 7c9866d368cb60a97944e6c10c5c09597430f1cb Mon Sep 17 00:00:00 2001 From: almossawi Date: Wed, 25 Mar 2015 09:37:14 -0700 Subject: [PATCH] Update files for issue #358 --- dist/metricsgraphics.js | 5050 +++++++++++++++++++++++++++++++++++ dist/metricsgraphics.min.js | 3 + 2 files changed, 5053 insertions(+) create mode 100644 dist/metricsgraphics.js create mode 100644 dist/metricsgraphics.min.js diff --git a/dist/metricsgraphics.js b/dist/metricsgraphics.js new file mode 100644 index 0000000000..d7b1870b0d --- /dev/null +++ b/dist/metricsgraphics.js @@ -0,0 +1,5050 @@ +(function() { + "use strict"; + (function(root, factory) { + if (typeof define === 'function' && define.amd) { + define(['d3', 'jquery'], factory); + } else if (typeof exports === 'object') { + module.exports = factory(require('d3'), require('jquery')); + } else { + root.MG = factory(root.d3, root.jQuery); + } + }(this, function(d3, $) { + window.MG = {version: '2.2.1'}; + + var charts = {}; + + MG.globals = {}; + MG.deprecations = { + rollover_callback: { replacement: 'mouseover', version: '2.0' }, + rollout_callback: { replacement: 'mouseout', version: '2.0' }, + show_years: { replacement: 'show_secondary_x_label', version: '2.1' } + }; + MG.globals.link = false; + MG.globals.version = "1.1"; + + MG.data_graphic = function() { + 'use strict'; + var defaults = {}; + defaults.all = { + 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 + 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: 40, // the size of the top margin + bottom: 30, // 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 + small_text: false, // coerces small text regardless of graphic size + xax_count: 6, // number of x axis ticks + xax_tick_length: 5, // x axis tick length + xax_start_at_min: false, + yax_count: 5, // 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 + x_accessor: 'date', + xax_units: '', + x_label: '', + x_axis: true, + y_axis: true, + y_accessor: 'value', + y_label: '', + yax_units: '', + x_rug: false, + y_rug: false, + transition_on_update: true, + mouseover: 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, + 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: {}, + show_year_markers: false, + show_secondary_x_label: true, + target: '#viz', + interpolate: 'cardinal', // interpolation method to use when rendering lines + interpolate_tension: 0.7, // its range is from 0 to 1; increase 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 + 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) + }; + + defaults.point = { + buffer: 16, + ls: false, + lowess: false, + point_size: 2.5, + size_accessor: null, + color_accessor: null, + size_range: null, // when we set a size_accessor option, this array determines the size range, e.g. [1,5] + color_range: null, // e.g. ['blue', 'red'] to color different groups of points + size_domain: null, + color_domain: null, + color_type: 'number' // can be either 'number' - the color scale is quantitative - or 'category' - the color scale is qualitative. + }; + + defaults.histogram = { + mouseover: function(d, i) { + d3.select('#histogram svg .mg-active-datapoint') + .text('Frequency Count: ' + d.y); + }, + binned: false, + bins: null, + processed_x_accessor: 'x', + processed_y_accessor: 'y', + processed_dx_accessor: 'dx', + bar_margin: 1 + }; + + defaults.bar = { + y_accessor: 'factor', + x_accessor: 'value', + baseline_accessor: null, + predictor_accessor: null, + predictor_proportion: 5, + dodge_accessor: null, + binned: true, + padding_percentage: 0, + outer_padding_percentage: 0.1, + height: 500, + top: 20, + bar_height: 20, + left: 70 + }; + + defaults.missing = { + top: 40, // the size of the top margin + bottom: 30, // the size of the bottom margin + right: 10, // size of the right margin + left: 10, // size of the left margin + buffer: 8, // the buffer between the actual chart area and the margins + legend_target: '', + width: 350, + height: 220, + missing_text: 'Data currently missing or unavailable', + scalefns: {}, + scales: {}, + show_missing_background: true, + interpolate: 'cardinal' + }; + + var args = arguments[0]; + if (!args) { args = {}; } + + 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 + '`'; + } + + warnDeprecation(message, deprecation.version); + } + } + + //build the chart + var a; + if (args.chart_type === 'missing-data') { + args = merge_with_defaults(args, defaults.missing); + charts.missing(args); + } + else if (args.chart_type === 'point') { + a = merge_with_defaults(defaults.point, defaults.all); + args = merge_with_defaults(args, a); + charts.point(args).mainPlot().markers().rollover().windowListeners(); + } + else if (args.chart_type === 'histogram') { + a = merge_with_defaults(defaults.histogram, defaults.all); + args = merge_with_defaults(args, a); + charts.histogram(args).mainPlot().markers().rollover().windowListeners(); + } + else if (args.chart_type === 'bar') { + a = merge_with_defaults(defaults.bar, defaults.all); + args = merge_with_defaults(args, a); + charts.bar(args).mainPlot().markers().rollover().windowListeners(); + } + else { + args = merge_with_defaults(args, defaults.all); + charts.line(args).markers().mainPlot().rollover().windowListeners(); + } + + return args.data; + }; + + if (typeof jQuery !== 'undefined') { + /*! + * 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=698666b23215c58f23d4) + * Config saved to config.json and https://gist.github.com/698666b23215c58f23d4 + */ + + /* ======================================================================== + * Bootstrap: tooltip.js v3.3.1 + * http://getbootstrap.com/javascript/#tooltip + * Inspired by the original jQuery.tipsy by Jason Frame + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + + +function ($) { + 'use strict'; + + if(typeof $().tooltip == 'function') + return true; + + // TOOLTIP PUBLIC CLASS DEFINITION + // =============================== + + var Tooltip = function (element, options) { + this.type = + this.options = + this.enabled = + this.timeout = + this.hoverState = + this.$element = null; + + this.init('tooltip', element, options); + }; + + Tooltip.VERSION = '3.3.1'; + + 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 && $(this.options.viewport.selector || this.options.viewport); + + 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.$tip && self.$tip.is(':visible')) { + self.hoverState = 'in'; + return; + } + + if (!self) { + self = new this.constructor(obj.currentTarget, this.getDelegateOptions()); + $(obj.currentTarget).data('bs.' + this.type, self); + } + + 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.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); + } + + 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); + + var pos = this.getPosition(); + var actualWidth = $tip[0].offsetWidth; + var actualHeight = $tip[0].offsetHeight; + + if (autoPlace) { + var orgPlacement = placement; + var $container = this.options.container ? $(this.options.container) : this.$element.parent(); + var containerDim = this.getPosition($container); + + placement = placement == 'bottom' && pos.bottom + actualHeight > containerDim.bottom ? 'top' : + placement == 'top' && pos.top - actualHeight < containerDim.top ? 'bottom' : + placement == 'right' && pos.right + actualWidth > containerDim.width ? 'left' : + placement == 'left' && pos.left - actualWidth < containerDim.left ? 'right' : + placement; + + $tip + .removeClass(orgPlacement) + .addClass(placement); + } + + var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight); + + this.applyPlacement(calculatedOffset, placement); + + var complete = function () { + 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 = offset.top + marginTop; + offset.left = 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 (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, isHorizontal) { + this.arrow() + .css(isHorizontal ? 'left' : 'top', 50 * (1 - delta / dimension) + '%') + .css(isHorizontal ? '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 && this.$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.width) { // 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 () { + return (this.$tip = this.$tip || $(this.options.template)); + }; + + 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); + } + } + + 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); + }); + }; + + + // TOOLTIP PLUGIN DEFINITION + // ========================= + + function Plugin(option) { + return this.each(function () { + var $this = $(this); + var data = $this.data('bs.tooltip'); + var options = typeof option == 'object' && option; + var selector = options && options.selector; + + if (!data && option == 'destroy') return; + if (selector) { + if (!data) $this.data('bs.tooltip', (data = {})); + if (!data[selector]) data[selector] = new Tooltip(this, options); + } else { + 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.1 + * http://getbootstrap.com/javascript/#popovers + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + + +function ($) { + 'use strict'; + + if(typeof $().popover == 'function') + return true; + + // POPOVER PUBLIC CLASS DEFINITION + // =============================== + + var Popover = function (element, options) { + this.init('popover', element, options); + }; + + if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js'); + + Popover.VERSION = '3.3.1'; + + 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.prototype.tip = function () { + if (!this.$tip) this.$tip = $(this.options.template); + return this.$tip; + }; + + + // POPOVER PLUGIN DEFINITION + // ========================= + + function Plugin(option) { + return this.each(function () { + var $this = $(this); + var data = $this.data('bs.popover'); + var options = typeof option == 'object' && option; + var selector = options && options.selector; + + if (!data && option == 'destroy') return; + if (selector) { + if (!data) $this.data('bs.popover', (data = {})); + if (!data[selector]) data[selector] = new Popover(this, options); + } else { + 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 container = d3.select(args.target); + + // remove the current title if it exists + container.select('.mg-chart-title').remove(); + + if (args.target && args.title) { + //only show question mark if there's a description + var optional_question_mark = (args.show_tooltips && args.description) + ? '' + : ''; + + container.insert('h2', ':first-child') + .attr('class', 'mg-chart-title') + .html(args.title + optional_question_mark); + + //activate the question mark if we have a description + if (args.show_tooltips && args.description) { + var $newTitle = $(container.node()).find('h2.mg-chart-title'); + + $newTitle.popover({ + html: true, + animation: false, + content: args.description, + trigger: 'hover', + placement: 'top', + container: $newTitle + }); + } + } + + if (args.error) { + error(args); + } + } + + function y_rug(args) { + 'use strict'; + var svg = mg_get_svg_child_of(args.target); + + var buffer_size = args.chart_type === 'point' + ? args.buffer / 2 + : args.buffer * 2 / 3; + + var all_data = []; + for (var i = 0; i < args.data.length; i++) { + for (var j = 0; j < args.data[i].length; j++) { + all_data.push(args.data[i][j]); + } + } + + var rug = svg.selectAll('line.mg-y-rug').data(all_data); + + //set the attributes that do not change after initialization, per + //D3's general update pattern + rug.enter().append('svg:line') + .attr('class', 'mg-y-rug') + .attr('opacity', 0.3); + + //remove rug elements that are no longer in use + rug.exit().remove(); + + //set coordinates of new rug elements + rug.exit().remove(); + + rug.attr('x1', args.left + 1) + .attr('x2', args.left+buffer_size) + .attr('y1', args.scalefns.yf) + .attr('y2', args.scalefns.yf); + + if (args.color_accessor) { + rug.attr('stroke', args.scalefns.color); + rug.classed('mg-y-rug-mono', false); + } else { + rug.attr('stroke', null); + rug.classed('mg-y-rug-mono', true); + } + } + + function y_axis(args) { + if (!args.processed) { + args.processed = {}; + } + + var svg = mg_get_svg_child_of(args.target); + + var g; + + var min_y, + max_y; + + args.scalefns.yf = function(di) { + //since we want to show actual zeros when missing_is_hidden is on + if(args.missing_is_hidden && di['missing']) { + return args.scales.Y(di[args.y_accessor]) + 42.1234; + } + + return args.scales.Y(di[args.y_accessor]); + }; + + var _set = false, + gtZeroFilter = function(d) { return d[args.y_accessor] > 0; }, + mapToY = function(d) { return d[args.y_accessor]; }; + for (var i = 0; i < args.data.length; i++) { + var a = args.data[i]; + + if (args.y_scale_type === 'log') { + // filter positive values + a = a.filter(gtZeroFilter); + } + + if (a.length > 0) { // get min/max in one pass + var extent = d3.extent(a, mapToY); + + if (!_set) { + // min_y and max_y haven't been set + min_y = extent[0]; + max_y = extent[1]; + _set = true; + } else { + min_y = Math.min(extent[0], min_y); + max_y = Math.max(extent[1], max_y); + } + } + } + + // the default case is for the y-axis to start at 0, unless we explicitly want it + // to start at an arbitrary number or from the data's minimum value + if (min_y >= 0 && !args.min_y && !args.min_y_from_data) { + min_y = 0; + } + + if (args.chart_type === 'bar') { + min_y = 0; + max_y = 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); + }); + } + //if a min_y or max_y have been set, use those instead + min_y = args.min_y !== null ? args.min_y : min_y; + max_y = args.max_y !== null ? args.max_y : max_y * args.inflator; + if (args.y_scale_type !== 'log') { + //we are currently saying that if the min val > 0, set 0 as min y + if (min_y >= 0) { + args.y_axis_negative = false; + } else { + min_y = min_y - (max_y * (args.inflator - 1)); + args.y_axis_negative = true; + } + } + + if (!args.min_y && args.min_y_from_data) { + min_y = min_y / args.inflator; + } + + 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 + min_y = 0.2; + } else { + if (min_y <= 0) { + min_y = 1; + } + } + args.scales.Y = d3.scale.log() + .domain([min_y, max_y]) + .range([args.height - args.bottom - args.buffer, args.top]) + .clamp(true); + } else { + args.scales.Y = d3.scale.linear() + .domain([min_y, max_y]) + .range([args.height - args.bottom - args.buffer, args.top]); + } + args.processed.min_y = min_y; + args.processed.max_y = max_y; + //used for ticks and such, and designed to be paired with log or linear + args.scales.Y_axis = d3.scale.linear() + .domain([args.processed.min_y, args.processed.max_y]) + .range([args.height - args.bottom - args.buffer, args.top]); + + var yax_format = args.yax_format; + if (!yax_format) { + if (args.format === 'count') { + yax_format = function(f) { + if (f < 1.0) { + // Don't scale tiny values. + return args.yax_units + d3.round(f, args.decimals); + } else { + var pf = d3.formatPrefix(f); + return args.yax_units + pf.scale(f) + pf.symbol; + } + }; + } else { //percentage + yax_format = function(d_) { + var n = d3.format('%p'); + return n(d_); + }; + } + } + + //remove the old y-axis, add new one + svg.selectAll('.mg-y-axis').remove(); + + if (!args.y_axis) { + return this; + } + + //y axis + g = svg.append('g') + .classed('mg-y-axis', true) + .classed('mg-y-axis-small', args.use_small_class); + + //are we adding a label? + if (args.y_label) { + g.append('text') + .attr('class', 'label') + .attr('x', function() { + return -1 * (args.top + args.buffer + + ((args.height - args.bottom - args.buffer) + - (args.top + args.buffer)) / 2); + }) + .attr('y', function() { + return args.left / 2; + }) + .attr("dy", "0.4em") + .attr('text-anchor', 'middle') + .text(function(d) { + return args.y_label; + }) + .attr("transform", function(d) { + return "rotate(-90)"; + }); + } + + var scale_ticks = args.scales.Y.ticks(args.yax_count); + + function log10(val) { + if (val === 1000) { + return 3; + } + if (val === 1000000) { + return 7; + } + return Math.log(val) / Math.LN10; + } + + if (args.y_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 ymax > number of generated ticks + var number_of_ticks = args.scales.Y.ticks(args.yax_count).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[args.y_accessor] % 1 !== 0) { + data_is_int = false; + return false; + } + }); + }); + + if (data_is_int && number_of_ticks > max_y && args.format === 'count') { + //remove non-integer ticks + scale_ticks = scale_ticks.filter(function(d) { + return d % 1 === 0; + }); + } + + var last_i = scale_ticks.length - 1; + if (!args.x_extended_ticks && !args.y_extended_ticks) { + g.append('line') + .attr('x1', args.left) + .attr('x2', args.left) + .attr('y1', args.scales.Y(scale_ticks[0]).toFixed(2)) + .attr('y2', args.scales.Y(scale_ticks[last_i]).toFixed(2)); + } + + //add y ticks + g.selectAll('.mg-yax-ticks') + .data(scale_ticks).enter() + .append('line') + .classed('mg-extended-y-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); }); + + g.selectAll('.mg-yax-labels') + .data(scale_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; + }); + + if (args.y_rug) { + y_rug(args); + } + + return this; + } + + function y_axis_categorical(args) { + // first, come up with y_axis + args.scales.Y = d3.scale.ordinal() + .domain(args.categorical_variables) + .rangeRoundBands([args.height - args.bottom - args.buffer, args.top], args.padding_percentage, args.outer_padding_percentage); + + args.scalefns.yf = function(di) { + return args.scales.Y(di[args.y_accessor]); + }; + + var svg = mg_get_svg_child_of(args.target); + + //remove the old y-axis, add new one + svg.selectAll('.mg-y-axis').remove(); + + var g = svg.append('g') + .classed('mg-y-axis', true) + .classed('mg-y-axis-small', args.use_small_class); + + if (!args.y_axis) { + return this; + } + + g.selectAll('text').data(args.categorical_variables).enter().append('svg:text') + .attr('x', args.left) + .attr('y', function(d) { + return args.scales.Y(d) + args.scales.Y.rangeBand() / 2 + + (args.buffer)*args.outer_padding_percentage; + }) + .attr('dy', '.35em') + .attr('text-anchor', 'end') + .text(String); + + return this; + } + + function x_rug(args) { + 'use strict'; + var buffer_size = args.chart_type === 'point' + ? args.buffer / 2 + : args.buffer; + + var svg = mg_get_svg_child_of(args.target); + + var all_data=[]; + for (var i=0; i 10 + ? d3.scale.category20() : d3.scale.category10()); + + args.scales.color.domain(color_domain); + } + + args.scalefns.color = function(di) { + return args.scales.color(di[args.color_accessor]); + }; + } + } + + function mg_point_add_size_scale(args) { + var min_size, max_size, size_domain, size_range; + if (args.size_accessor !== null) { + if (args.size_domain === null) { + min_size = d3.min(args.data[0], function(d) { + return d[args.size_accessor]; + }); + + max_size = d3.max(args.data[0], function(d) { + return d[args.size_accessor]; + }); + + size_domain = [min_size, max_size]; + } else { + size_domain = args.size_domain; + } + if (args.size_range === null) { + size_range = [1,5]; //args.size_domain; + } else { + size_range = args.size_range; + } + + args.scales.size = d3.scale.linear() + .domain(size_domain) + .range(size_range) + .clamp(true); + + args.scalefns.size = function(di) { + return args.scales.size(di[args.size_accessor]); + }; + } + } + + function mg_add_x_label(g, args) { + g.append('text') + .attr('class', 'label') + .attr('x', function() { + return (args.left + args.width - args.right) / 2; + }) + .attr('y', (args.height - args.bottom / 2).toFixed(2)) + .attr('dy', '.50em') + .attr('text-anchor', 'middle') + .text(function(d) { + return args.x_label; + }); + } + + function mg_default_bar_xax_format(args) { + if (args.xax_format) { + return args.xax_format; + } + + return function(f) { + if (f < 1.0) { + //don't scale tiny values + return args.xax_units + d3.round(f, args.decimals); + } else { + var pf = d3.formatPrefix(f); + return args.xax_units + pf.scale(f) + pf.symbol; + } + }; + } + + function mg_default_xax_format(args) { + if (args.xax_format) { + return args.xax_format; + } + + var diff, + main_time_format, + time_frame; + + if (args.time_series) { + diff = (args.processed.max_x - args.processed.min_x) / 1000; + + if (diff < 60) { + main_time_format = d3.time.format('%M:%S'); + time_frame = 'seconds'; + } else if (diff / (60 * 60) <= 24) { + main_time_format = d3.time.format('%H:%M'); + time_frame = 'less-than-a-day'; + } else if (diff / (60 * 60) <= 24 * 4) { + main_time_format = d3.time.format('%H:%M'); + time_frame = 'four-days'; + } else { + main_time_format = d3.time.format('%b %d'); + time_frame = 'default'; + } + } + + args.processed.main_x_time_format = main_time_format; + args.processed.x_time_frame = time_frame; + + return function(d) { + var df = d3.time.format('%b %d'); + var pf = d3.formatPrefix(d); + + // format as date or not, of course user can pass in + // a custom function if desired + if(args.data[0][0][args.x_accessor] instanceof Date) { + return args.processed.main_x_time_format(d); + } else if (typeof args.data[0][0][args.x_accessor] === 'number') { + if (d < 1.0) { + //don't scale tiny values + return args.xax_units + d3.round(d, args.decimals); + } else { + pf = d3.formatPrefix(d); + return args.xax_units + pf.scale(d) + pf.symbol; + } + } else { + return d; + } + }; + } + + function mg_add_x_ticks(g, args) { + var last_i = args.scales.X.ticks(args.xax_count).length - 1; + var ticks = args.scales.X.ticks(args.xax_count); + + //force min to be the first tick rather than the first element in ticks + if(args.xax_start_at_min) { + ticks[0] = args.processed.min_x; + } + + if (args.chart_type !== 'bar' && !args.x_extended_ticks && !args.y_extended_ticks) { + //extend axis line across bottom, rather than from domain's min..max + g.append('line') + .attr('x1', function() { + //start the axis line from the beginning, domain's min, or the auto-generated + //ticks' first element, depending on whether xax_count is set to 0 or + //xax_start_at_min is set to true + if (args.xax_count === 0) { + return args.left + args.buffer; + } + else if (args.xax_start_at_min) { + return args.scales.X(args.processed.min_x).toFixed(2) + } + else { + return (args.scales.X(args.scales.X.ticks(args.xax_count)[0])).toFixed(2); + } + }) + .attr('x2', + (args.xax_count === 0) + ? args.width - args.right - args.buffer + : (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); + } + + g.selectAll('.mg-xax-ticks') + .data(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-x-ticks'; + } + }); + } + + function mg_add_x_tick_labels(g, args) { + var ticks = args.scales.X.ticks(args.xax_count); + + //force min to be the first tick rather than the first element in ticks + if(args.xax_start_at_min) { + ticks[0] = args.processed.min_x; + } + + g.selectAll('.mg-xax-labels') + .data(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') + .text(function(d) { + return args.xax_units + args.xax_format(d); + }); + + if (args.time_series && (args.show_years || args.show_secondary_x_label)) { + var secondary_marks, + secondary_function, yformat; + + var time_frame = args.processed.x_time_frame; + + switch(time_frame) { + case 'seconds': + secondary_function = d3.time.days; + yformat = d3.time.format('%I %p'); + break; + case 'less-than-a-day': + secondary_function = d3.time.days; + yformat = d3.time.format('%b %d'); + break; + case 'four-days': + secondary_function = d3.time.days; + yformat = d3.time.format('%b %d'); + break; + default: + secondary_function = d3.time.years; + yformat = d3.time.format('%Y'); + } + + var years = secondary_function(args.processed.min_x, args.processed.max_x); + + //if xax_start_at_min is set + if (args.xax_start_at_min && years.length === 0) { + var first_tick = ticks[0]; + years = [first_tick]; + } else if (years.length === 0) { + var first_tick = args.scales.X.ticks(args.xax_count)[0]; + years = [first_tick]; + } + + //append year marker to x-axis group + g = g.append('g') + .classed('mg-year-marker', true) + .classed('mg-year-marker-small', args.use_small_class); + + if (time_frame === 'default' && args.show_year_markers) { + 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', args.top) + .attr('y2', args.height - args.bottom); + } + + g.selectAll('.mg-year-marker') + .data(years).enter() + .append('text') + .attr('x', function(d, i) { + if (args.xax_start_at_min && i == 0) { + d = ticks[0]; + } + + return args.scales.X(d).toFixed(2); + }) + .attr('y', (args.height - args.bottom + args.xax_tick_length * 7 / 1.3).toFixed(2)) + .attr('dy', args.use_small_class ? -3 : 0) + .attr('text-anchor', 'middle') + .text(function(d) { + return yformat(d); + }); + } + } + + function mg_find_min_max_x(args) { + var last_i, + extent_x = [], + min_x, + max_x, + all_data = [].concat.apply([], args.data), + mapDtoX = function(d) { return d[args.x_accessor]; }; + + // clear the cached xax_format in case we need to recalculate + if(args.xax_format === null) { + delete args.xax_format; + } + + if (args.chart_type === 'line' || args.chart_type === 'point' || args.chart_type === 'histogram') { + extent_x = d3.extent(all_data, mapDtoX); + min_x = extent_x[0]; + max_x = extent_x[1]; + + } else if (args.chart_type === 'bar') { + min_x = 0; + max_x = d3.max(all_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); + }); + } + //if data set is of length 1, expand the range so that we can build the x-axis + //of course, a line chart doesn't make sense in this case, so the preferred + //method would be to check for said object's length and, if appropriate, + //change the chart type to 'point' + if (min_x === max_x) { + if (min_x instanceof Date) { + var yesterday = MG.clone(min_x).setDate(min_x.getDate() - 1); + var tomorrow = MG.clone(min_x).setDate(min_x.getDate() + 1); + + min_x = yesterday; + max_x = tomorrow; + } else if (typeof min_x === 'number') { + min_x = min_x - 1; + max_x = max_x + 1; + } else if (typeof min_x === 'string') { + min_x = Number(min_x) - 1; + max_x = Number(max_x) + 1; + } + + //force xax_count to be 2 + args.xax_count = 2; + } + + min_x = args.min_x ? args.min_x : min_x; + max_x = args.max_x ? args.max_x : max_x; + args.x_axis_negative = false; + + args.processed.min_x = min_x; + args.processed.max_x = max_x; + + mg_select_xax_format(args); + + if (!args.time_series) { + if (args.processed.min_x < 0) { + args.processed.min_x = args.processed.min_x - (args.processed.max_x * (args.inflator - 1)); + args.x_axis_negative = true; + } + } + + if (args.chart_type === 'bar') { + args.additional_buffer = args.buffer * 5; + } else { + args.additional_buffer = 0; + } + } + + function mg_select_xax_format(args) { + if (!args.xax_format && args.chart_type === 'line') args.xax_format = mg_default_xax_format(args); + if (!args.xax_format && args.chart_type === 'point') args.xax_format = mg_default_xax_format(args); + if (!args.xax_format && args.chart_type === 'histogram') args.xax_format = mg_default_xax_format(args); + if (!args.xax_format && args.chart_type === 'bar') args.xax_format = mg_default_bar_xax_format(args); + } + + function init(args) { + 'use strict'; + var defaults = { + target: null, + title: null, + description: null + }; + + // If you pass in a dom element for args.target, the expectation + // of a string elsewhere will break. + + args = arguments[0]; + if (!args) { args = {}; } + args = merge_with_defaults(args, defaults); + + if (d3.select(args.target).empty()) { + console.warn('The specified target element "' + args.target + '" could not be found in the page. The chart will not be rendered.'); + return; + } + + var container = d3.select(args.target); + var svg = container.selectAll('svg'); + + //this is how we're dealing with passing in a single array of data, + //but with the intention of using multiple values for multilines, etc. + + //do we have a time_series? + if (args.data[0][0][args.x_accessor] instanceof Date) { + args.time_series = true; + } else { + args.time_series = false; + } + + var svg_width = args.width; + var svg_height = args.height; + + //are we setting the aspect ratio + if (args.full_width) { + // get parent element + svg_width = get_width(args.target); + } + + if (args.full_height) { + svg_height = get_height(args.target); + } + + if (args.chart_type === 'bar' && svg_height === null) { + svg_height = args.height = args.data[0].length * args.bar_height + args.top + args.bottom; + } + + //remove the svg if the chart type has changed + 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(); + } + + //add svg if it doesn't already exist + //using trim on html rather than :empty to ignore white spaces if they exist + if (mg_get_svg_child_of(args.target).empty()) { + //add svg + svg = d3.select(args.target) + .append('svg') + .classed('linked', args.linked) + .attr('width', svg_width) + .attr('height', svg_height); + } + + args.width = svg_width; + args.height = svg_height; + + // add clip path element to svg + svg.selectAll('.mg-clip-path').remove(); + + svg.append('defs') + .attr('class', 'mg-clip-path') + .append('clipPath') + .attr('id', 'mg-plot-window-' + mg_strip_punctuation(args.target)) + .append('svg:rect') + .attr('x', args.left) + .attr('y', args.top) + .attr('width', args.width - args.left - args.right - args.buffer) + .attr('height', args.height - args.top - args.bottom - args.buffer + 1); + + //has the width or height changed? + if (svg_width !== Number(svg.attr('width'))) { + svg.attr('width', svg_width); + } + + if (svg_height !== Number(svg.attr('height'))) { + svg.attr('height', svg_height); + } + + // This is an unfinished feature. Need to reconsider how we handle automatic scaling. + svg.attr('viewBox', '0 0 ' + svg_width + ' ' + svg_height); + + if (args.full_width || args.full_height) { + svg.attr('preserveAspectRatio', 'xMinYMin meet'); + } + + // remove missing class + svg.classed('mg-missing', false); + + // remove missing text + svg.selectAll('.mg-missing-text').remove(); + svg.selectAll('.mg-missing-pane').remove(); + + //add chart title if it's different than existing one + chart_title(args); + + //draw axes + args.use_small_class = args.height - args.top - args.bottom - args.buffer + <= args.small_height_threshold && args.width - args.left-args.right - args.buffer * 2 + <= args.small_width_threshold || args.small_text; + + //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 (args.data.length < svg.selectAll('.mg-main-line')[0].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(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 = arrDiff( + 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(); + } + } + //if we don't have a customer line-color map, just remove the lines from the end + else { + var num_of_new = args.data.length; + var num_of_existing = svg.selectAll('.mg-main-line')[0].length; + + for (i = num_of_existing; i > num_of_new; i--) { + svg.selectAll('.mg-main-line.mg-line' + i + '-color') + .remove(); + } + } + } + + return this; + } + + function markers(args) { + 'use strict'; + var svg = mg_get_svg_child_of(args.target); + var gm; + var gb; + + //remove existing markers and baselines + svg.selectAll('.mg-markers').remove(); + svg.selectAll('.mg-baselines').remove(); + + if (args.markers) { + gm = svg.append('g') + .attr('class', 'mg-markers'); + + gm.selectAll('.mg-markers') + .data(args.markers.filter(inRange)) + .enter() + .append('line') + .attr('x1', xPositionFixed) + .attr('x2', xPositionFixed) + .attr('y1', args.top) + .attr('y2', function() { + return args.height - args.bottom - args.buffer; + }) + .attr('stroke-dasharray', '3,1'); + + gm.selectAll('.mg-markers') + .data(args.markers.filter(inRange)) + .enter() + .append('text') + .attr('class', 'mg-marker-text') + .attr('x', xPosition) + .attr('y', args.top - 8) + .attr('text-anchor', 'middle') + .text(function(d) { + return d.label; + }); + + preventOverlap(gm.selectAll('.mg-marker-text')[0]); + } + + if (args.baselines) { + gb = svg.append('g') + .attr('class', 'mg-baselines'); + + gb.selectAll('.mg-baselines') + .data(args.baselines) + .enter().append('line') + .attr('x1', args.left + args.buffer) + .attr('x2', args.width-args.right-args.buffer) + .attr('y1', function(d){ + return args.scales.Y(d.value).toFixed(2); + }) + .attr('y2', function(d){ + return args.scales.Y(d.value).toFixed(2); + }); + + gb.selectAll('.mg-baselines') + .data(args.baselines) + .enter().append('text') + .attr('x', args.width-args.right - args.buffer) + .attr('y', function(d){ + return args.scales.Y(d.value).toFixed(2); + }) + .attr('dy', -3) + .attr('text-anchor', 'end') + .text(function(d) { + return d.label; + }); + } + + function preventOverlap(labels) { + 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 (isOverlapping(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 isOverlapping(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 xPosition(d) { + return args.scales.X(d[args.x_accessor]); + } + + function xPositionFixed(d) { + return xPosition(d).toFixed(2); + } + + function inRange(d) { + return (args.scales.X(d[args.x_accessor]) > args.buffer + args.left) + && (args.scales.X(d[args.x_accessor]) < args.width - args.buffer - args.right); + } + + return this; + } + + function mg_window_listeners(args){ + mg_if_aspect_ratio_resize_svg(args); + } + + function mg_if_aspect_ratio_resize_svg(args){ + // If we've asked the svg to fill a div, resize with div. + if (args.full_width || args.full_height){ + window.addEventListener('resize', function(){ + var svg = d3.select(args.target).select('svg'); + var aspect = svg.attr('height') / svg.attr('width'); + var newWidth = get_width(args.target); + + svg.attr('width', newWidth); + svg.attr('height', aspect * newWidth); + }, true); + } + + } + if (typeof jQuery !== 'undefined') { + /*! + * 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 (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 + $('