summaryrefslogtreecommitdiff
path: root/priv/static/js/metricsgraphics/common
diff options
context:
space:
mode:
authorhref <href@random.sh>2021-09-01 10:30:18 +0200
committerhref <href@random.sh>2021-09-01 10:30:18 +0200
commit75687711f35355bc30e4829439384aab28fcac6d (patch)
tree8f3256f472893c39720a684d390e890a152f7303 /priv/static/js/metricsgraphics/common
parentlink: post_* callbacks; html & pdftitle. (diff)
Commit all the changes that hasn't been committed + updates.
Diffstat (limited to '')
-rwxr-xr-xpriv/static/js/metricsgraphics/common/bootstrap_tooltip_popover.js626
-rw-r--r--priv/static/js/metricsgraphics/common/brush.js126
-rw-r--r--priv/static/js/metricsgraphics/common/chart_title.js67
-rw-r--r--priv/static/js/metricsgraphics/common/data_graphic.js205
-rw-r--r--priv/static/js/metricsgraphics/common/hooks.js63
-rw-r--r--priv/static/js/metricsgraphics/common/init.js273
-rw-r--r--priv/static/js/metricsgraphics/common/markers.js132
-rw-r--r--priv/static/js/metricsgraphics/common/register.js16
-rw-r--r--priv/static/js/metricsgraphics/common/rollover.js98
-rw-r--r--priv/static/js/metricsgraphics/common/scales.js340
-rw-r--r--priv/static/js/metricsgraphics/common/window_listeners.js82
-rw-r--r--priv/static/js/metricsgraphics/common/x_axis.js600
-rw-r--r--priv/static/js/metricsgraphics/common/y_axis.js1072
-rw-r--r--priv/static/js/metricsgraphics/common/zoom.js85
14 files changed, 3785 insertions, 0 deletions
diff --git a/priv/static/js/metricsgraphics/common/bootstrap_tooltip_popover.js b/priv/static/js/metricsgraphics/common/bootstrap_tooltip_popover.js
new file mode 100755
index 0000000..3d1d5b7
--- /dev/null
+++ b/priv/static/js/metricsgraphics/common/bootstrap_tooltip_popover.js
@@ -0,0 +1,626 @@
+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 (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: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
+ 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 () {
+ 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 (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 == '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 (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: '<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'
+ });
+
+
+ // 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 == '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);
+}
diff --git a/priv/static/js/metricsgraphics/common/brush.js b/priv/static/js/metricsgraphics/common/brush.js
new file mode 100644
index 0000000..4a4deff
--- /dev/null
+++ b/priv/static/js/metricsgraphics/common/brush.js
@@ -0,0 +1,126 @@
+{
+
+const 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);
+};
+
+const create_brushing_pattern = (args, range) => {
+ const x = range.x[0];
+ const width = range.x[1] - range.x[0];
+ const y = range.y[0];
+ const 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);
+};
+
+const remove_brushing_pattern = args => {
+ get_extent_rect(args)
+ .attr('width', 0)
+ .attr('height', 0)
+ .attr('opacity', 0);
+};
+
+const add_event_handler_for_brush = (args, target, axis) => {
+ const svg = d3.select(args.target).select('svg');
+ const rollover = svg.select('.mg-rollover-rect, .mg-voronoi');
+ const container = rollover.node();
+ const targetUid = mg_target_ref(args.target);
+ let isDragging = false;
+ let mouseDown = false;
+ let origin = [];
+
+ const calculateSelectionRange = () => {
+ const min_x = args.left;
+ const max_x = args.width - args.right - args.buffer;
+ const min_y = args.top;
+ const max_y = args.height - args.bottom - args.buffer;
+ const mouse = d3.mouse(container);
+ const 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.' + targetUid, () => {
+ 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.' + targetUid, () => {
+ if (mouseDown) {
+ isDragging = true;
+ rollover.classed('mg-brushing', true);
+ create_brushing_pattern(args, calculateSelectionRange());
+ }
+ });
+ d3.select(document).on('mouseup.' + targetUid, () => {
+ if (!mouseDown) return;
+ mouseDown = false;
+ svg.classed('mg-brushing-in-progress', false);
+ const range = calculateSelectionRange();
+ if (isDragging) {
+ isDragging = false;
+ if (target === args) {
+ MG.zoom_to_data_range(target, range);
+ if (args.click_to_zoom_out)
+ svg.select('.mg-rollover-rect, .mg-voronoi').classed('mg-brushed', true);
+ } else {
+ const domain = MG.convert_range_to_domain(args, range);
+ MG.zoom_to_data_domain(target, domain);
+ }
+ } else if (args.click_to_zoom_out) {
+ MG.zoom_to_raw_range(target);
+ }
+ if (mg_is_function(args.brushing_selection_changed))
+ args.brushing_selection_changed(args, range);
+ });
+};
+
+const 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;
+ let brush_axis;
+ 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;
+
+}
diff --git a/priv/static/js/metricsgraphics/common/chart_title.js b/priv/static/js/metricsgraphics/common/chart_title.js
new file mode 100644
index 0000000..b2624dd
--- /dev/null
+++ b/priv/static/js/metricsgraphics/common/chart_title.js
@@ -0,0 +1,67 @@
+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: '<div class="popover mg-popover"><div class="arrow"></div><div class="popover-inner"><h3 class="popover-title"></h3><div class="popover-content"><p></p></div></div></div>'
+ }).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;
diff --git a/priv/static/js/metricsgraphics/common/data_graphic.js b/priv/static/js/metricsgraphics/common/data_graphic.js
new file mode 100644
index 0000000..517b3fa
--- /dev/null
+++ b/priv/static/js/metricsgraphics/common/data_graphic.js
@@ -0,0 +1,205 @@
+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.options = { // <name>: [<defaultValue>, <availableType>]
+ x_axis_type: [null, ['categorical']], // TO BE INTRODUCED IN 2.10
+ y_axis_type: [null, ['categorical']], // TO BE INTRODUCED IN 2.10
+ y_padding_percentage: [0.05, 'number'], // for categorical scales
+ y_outer_padding_percentage: [0.1, 'number'], // for categorical scales
+ ygroup_padding_percentage: [0.25, 'number'], // for categorical scales
+ ygroup_outer_padding_percentage: [0, 'number'], // for categorical scales
+ x_padding_percentage: [0.05, 'number'], // for categorical scales
+ x_outer_padding_percentage: [0.1, 'number'], // for categorical scales
+ xgroup_padding_percentage: [0.25, 'number'], // for categorical scales
+ xgroup_outer_padding_percentage: [0, 'number'], // for categorical scales
+ ygroup_accessor: [null, 'string'],
+ xgroup_accessor: [null, 'string'],
+ y_categorical_show_guides: [false, 'boolean'],
+ x_categorical_show_guide: [false, 'boolean'],
+ rotate_x_labels: [0, 'number'],
+ rotate_y_labels: [0, 'number'],
+ scales: [{}],
+ scalefns: [{}],
+ // Data
+ data: [[], ['object[]', 'number[]']], // the data object
+ missing_is_zero: [false, 'boolean'], // assume missing observations are zero
+ missing_is_hidden: [false, 'boolean'], // show missing observations as missing line segments
+ missing_is_hidden_accessor: [null, 'string'], // the accessor for identifying observations as missing
+ utc_time: [false, 'boolean'], // determines whether to use a UTC or local time scale
+ x_accessor: ['date', 'string'], // the data element that's the x-accessor
+ x_sort: [true, 'boolean'], // determines whether to sort the x-axis' values
+ y_accessor: ['value', ['string', 'string[]']], // the data element that's the y-accessor
+ // Axes
+ axes_not_compact: [true, 'boolean'], // determines whether to draw compact or non-compact axes
+ european_clock: [false, 'boolean'], // determines whether to show labels using a 24-hour clock
+ inflator: [10/9, 'number'], // a multiplier for inflating max_x and max_y
+ max_x: [null, ['number', Date]], // the maximum x-value
+ max_y: [null, ['number', Date]], // the maximum y-value
+ min_x: [null, ['number', Date]], // the minimum x-value
+ min_y: [null, ['number', Date]], // the minimum y-value
+ min_y_from_data: [false, 'boolean'], // starts y-axis at data's minimum value
+ show_year_markers: [false, 'boolean'], // determines whether to show year markers along the x-axis
+ show_secondary_x_label: [true, 'boolean'], // determines whether to show years along the x-axis
+ small_text: [false, 'boolean'],
+ x_extended_ticks: [false, 'boolean'], // determines whether to extend the x-axis ticks across the chart
+ x_axis: [true, 'boolean'], // determines whether to display the x-axis
+ x_label: ['', 'string'], // the label to show below the x-axis
+ xax_count: [6, 'number'], // the number of x-axis ticks
+ xax_format: [null, 'function'], // a function that formats the x-axis' labels
+ xax_tick_length: [5, 'number'], // the x-axis' tick length in pixels
+ xax_units: ['', 'string'], // a prefix symbol to be shown alongside the x-axis' labels
+ x_scale_type: ['linear', 'log'], // the x-axis scale type
+ y_axis: [true, 'boolean'], // determines whether to display the y-axis
+ x_axis_position: ['bottom'], // string
+ y_axis_position: ['left'], // string
+ y_extended_ticks: [false, 'boolean'], // determines whether to extend the y-axis ticks across the chart
+ y_label: ['', 'string'], // the label to show beside the y-axis
+ y_scale_type: ['linear', ['linear', 'log']], // the y-axis scale type
+ yax_count: [3, 'number'], // the number of y-axis ticks
+ yax_format: [null, 'function'], // a function that formats the y-axis' labels
+ yax_tick_length: [5, 'number'], // the y-axis' tick length in pixels
+ yax_units: ['', 'string'], // a prefix symbol to be shown alongside the y-axis' labels
+ yax_units_append: [false, 'boolean'], // determines whether to append rather than prepend units
+ // GraphicOptions
+ aggregate_rollover: [false, 'boolean'], // links the lines in a multi-line graphic
+ animate_on_load: [false, 'boolean'], // determines whether lines are transitioned on first-load
+ area: [true, ['boolean', 'array']], // determines whether to fill the area below the line
+ flip_area_under_y_value: [null, 'number'], // Specify a Y baseline number value to flip area under it
+ baselines: [null, 'object[]'], // horizontal lines that indicate, say, goals.
+ chart_type: ['line', ['line', 'histogram', 'point', 'bar', 'missing-data']], // '{line, histogram, point, bar, missing-data}'],
+ color: [null, ['string', 'string[]']],
+ colors: [null, ['string', 'string[]']],
+ custom_line_color_map: [[], 'number[]'], // maps an arbitrary set of lines to colors
+ decimals: [2, 'number'], // the number of decimals to show in a rollover
+ error: ['', 'string'], // does the graphic have an error that we want to communicate to users
+ format: ['count', ['count', 'percentage']], // the format of the data object (count or percentage)
+ full_height: [false, 'boolean'], // sets height to that of the parent, adjusts dimensions on window resize
+ full_width: [false, 'boolean'], // sets width to that of the parent, adjusts dimensions on window resize
+ interpolate: [d3.curveCatmullRom.alpha(0), [d3.curveBasisClosed, d3.curveBasisOpen, d3.curveBasis, d3.curveBundle, d3.curveCardinalClosed, d3.curveCardinalOpen, d3.curveCardinal, d3.curveCatmullRomClosed, d3.curveCatmullRomOpen, d3.curveLinearClosed, d3.curveLinear, d3.curveMonotoneX, d3.curveMonotoneY, d3.curveNatural, d3.curveStep, d3.curveStepAfter, d3.curveStepBefore]], // the interpolation function to use for rendering lines
+ legend: ['', 'string[]'], // an array of literals used to label lines
+ legend_target: ['', 'string'], // the DOM element to insert the legend in
+ linked: [false, 'boolean'], // used to link multiple graphics together
+ linked_format: ['%Y-%m-%d', 'string'], // specifies the format of linked rollovers
+ list: [false, 'boolean'], // automatically maps the data to x and y accessors
+ markers: [null, 'object[]'], // vertical lines that indicate, say, milestones
+ max_data_size: [null, 'number'], // for use with custom_line_color_map
+ missing_text: [null, 'string'], // The text to display for missing graphics
+ show_missing_background: [true, 'boolean'], // Displays a background for missing graphics
+ mousemove_align: ['right', 'string'], // implemented in point.js
+ x_mouseover: [null, ['string', 'function']],
+ y_mouseover: [null, ['string', 'function']],
+ mouseover: [null, 'function'], // custom rollover function
+ mousemove: [null, 'function'], // custom rollover function
+ mouseout: [null, 'function'], // custom rollover function
+ click: [null, 'function'],
+ point_size: [2.5, 'number'], // the radius of the dots in the scatterplot
+ active_point_on_lines: [false, 'boolean'], // if set, active dot on lines will be displayed.
+ active_point_accessor: ['active', 'string'], // data accessor value to determine if a point is active or not
+ active_point_size: [2, 'number'], // the size of the dot that appears on a line when
+ points_always_visible: [false, 'boolean'], // whether to always display data points and not just on hover
+ rollover_time_format: [null, 'string'], // custom time format for rollovers
+ show_confidence_band: [null, 'string[]'], // determines whether to show a confidence band
+ show_rollover_text: [true, 'boolean'], // determines whether to show text for a data point on rollover
+ show_tooltips: [true, 'boolean'], // determines whether to display descriptions in tooltips
+ showActivePoint: [true, 'boolean'], // If enabled show active data point information in chart
+ target: ['#viz', ['string', HTMLElement]], // the DOM element to insert the graphic in
+ transition_on_update: [true, 'boolean'], // gracefully transitions the lines on data change
+ x_rug: [false, 'boolean'], // show a rug plot along the x-axis
+ y_rug: [false, 'boolean'], // show a rug plot along the y-axis
+ mouseover_align: ['right', ['right', 'left']],
+ brush: [null, ['xy','x','y']], // add brush function
+ brushing_selection_changed: [null, 'function'], // callback function on brushing. the first parameter are the arguments that correspond to this chart, the second parameter is the range of the selection
+ zoom_target: [null, 'object'], // the zooming target of brushing function
+ click_to_zoom_out: [true, 'boolean'], // if true and the graph is currently zoomed in, clicking on the graph will zoom out
+ // Layout
+ buffer: [8, 'number'], // the padding around the graphic
+ bottom: [45, 'number'], // the size of the bottom margin
+ center_title_full_width: [false, 'boolean'], // center title over entire graph
+ height: [220, 'number'], // the graphic's height
+ left: [50, 'number'], // the size of the left margin
+ right: [10, 'number'], // the size of the right margin
+ small_height_threshold: [120, 'number'], // maximum height for a small graphic
+ small_width_threshold: [160, 'number'], // maximum width for a small graphic
+ top: [65, 'number'], // the size of the top margin
+ width: [350, 'number'], // the graphic's width
+ title_y_position: [10, 'number'], // how many pixels from the top edge (0) should we show the title at
+ title: [null, 'string'],
+ description: [null, 'string']
+};
+
+MG.charts = {};
+
+MG.defaults = options_to_defaults(MG.options);
+
+MG.data_graphic = function(args) {
+ 'use strict';
+
+ MG.call_hook('global.defaults', MG.defaults);
+
+ if (!args) { args = {}; }
+
+ for (let key in args) {
+ if (!mg_validate_option(key, args[key])) {
+ if (!(key in MG.options)) {
+ console.warn(`Option ${key} not recognized`);
+ } else {
+ console.warn(`Option ${key} expected type ${MG.options[key][1]} but got ${args[key]} instead`);
+ }
+ }
+ }
+
+ var selected_chart = MG.charts[args.chart_type || MG.defaults.chart_type];
+ merge_with_defaults(args, selected_chart.defaults, MG.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;
+};
diff --git a/priv/static/js/metricsgraphics/common/hooks.js b/priv/static/js/metricsgraphics/common/hooks.js
new file mode 100644
index 0000000..5f2adb5
--- /dev/null
+++ b/priv/static/js/metricsgraphics/common/hooks.js
@@ -0,0 +1,63 @@
+/**
+ 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;
+};
diff --git a/priv/static/js/metricsgraphics/common/init.js b/priv/static/js/metricsgraphics/common/init.js
new file mode 100644
index 0000000..4b43e48
--- /dev/null
+++ b/priv/static/js/metricsgraphics/common/init.js
@@ -0,0 +1,273 @@
+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(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;
diff --git a/priv/static/js/metricsgraphics/common/markers.js b/priv/static/js/metricsgraphics/common/markers.js
new file mode 100644
index 0000000..4371282
--- /dev/null
+++ b/priv/static/js/metricsgraphics/common/markers.js
@@ -0,0 +1,132 @@
+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;
diff --git a/priv/static/js/metricsgraphics/common/register.js b/priv/static/js/metricsgraphics/common/register.js
new file mode 100644
index 0000000..032736f
--- /dev/null
+++ b/priv/static/js/metricsgraphics/common/register.js
@@ -0,0 +1,16 @@
+function register(chartType, descriptor, options) {
+ const defaults = options ? options_to_defaults(options) : {};
+ MG.charts[chartType] = {
+ descriptor: descriptor,
+ defaults: defaults,
+ };
+ if (options) {
+ Object.keys(options).map(key => {
+ if (!(key in MG.options)) {
+ MG.options[key] = options[key];
+ }
+ });
+ }
+}
+
+MG.register = register;
diff --git a/priv/static/js/metricsgraphics/common/rollover.js b/priv/static/js/metricsgraphics/common/rollover.js
new file mode 100644
index 0000000..c462d98
--- /dev/null
+++ b/priv/static/js/metricsgraphics/common/rollover.js
@@ -0,0 +1,98 @@
+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')
+ .attr('transform', 'translate(0 -18)')
+ .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) {
+ let tspan = svg.append('tspan').text(text);
+
+ return {
+ bold: () => tspan.attr('font-weight', 'bold'),
+ font_size: (pts) => tspan.attr('font-size', pts),
+ x: (x) => tspan.attr('x', x),
+ y: (y) => tspan.attr('y', y),
+ 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,
+ text: (text) => {
+ return mg_mouseover_tspan(rrr, text);
+ }
+ };
+}
+
+function mg_mouseover_text(args, rargs) {
+ mg_setup_mouseover_container(rargs.svg, args);
+
+ let mouseOver = {
+ row_number: 0,
+ rargs,
+ 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;
+}
diff --git a/priv/static/js/metricsgraphics/common/scales.js b/priv/static/js/metricsgraphics/common/scales.js
new file mode 100644
index 0000000..4e76285
--- /dev/null
+++ b/priv/static/js/metricsgraphics/common/scales.js
@@ -0,0 +1,340 @@
+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;
+}
diff --git a/priv/static/js/metricsgraphics/common/window_listeners.js b/priv/static/js/metricsgraphics/common/window_listeners.js
new file mode 100644
index 0000000..6dad3e9
--- /dev/null
+++ b/priv/static/js/metricsgraphics/common/window_listeners.js
@@ -0,0 +1,82 @@
+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(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);
+ }
+}
diff --git a/priv/static/js/metricsgraphics/common/x_axis.js b/priv/static/js/metricsgraphics/common/x_axis.js
new file mode 100644
index 0000000..c050f2b
--- /dev/null
+++ b/priv/static/js/metricsgraphics/common/x_axis.js
@@ -0,0 +1,600 @@
+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) {
+ const diff = (args.processed.max_x - args.processed.min_x) / 1000;
+ const 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) {
+ let 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);
+ }
+ }
+ }
+}
diff --git a/priv/static/js/metricsgraphics/common/y_axis.js b/priv/static/js/metricsgraphics/common/y_axis.js
new file mode 100644
index 0000000..773d525
--- /dev/null
+++ b/priv/static/js/metricsgraphics/common/y_axis.js
@@ -0,0 +1,1072 @@
+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 = tick_length ? scale(ticks[0]).toFixed(2) : mg_get_top(args);
+ coordinates.y2 = tick_length ? scale(ticks[tick_length - 1]).toFixed(2) : mg_get_bottom(args);
+ }
+ if (position === 'right') {
+ coordinates.x1 = mg_get_right(args);
+ coordinates.x2 = mg_get_right(args);
+ coordinates.y1 = tick_length ? scale(ticks[0]).toFixed(2) : mg_get_top(args);
+ coordinates.y2 = tick_length ? scale(ticks[tick_length - 1]).toFixed(2) : mg_get_bottom(args);
+ }
+ 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 (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 (d) {
+ return yformat(new Date(d)); };
+ }
+ if (position === 'bottom') {
+ x = function (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 (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 (d) {
+ return scale(d).toFixed(2);
+ };
+ y2 = function (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 (d) {
+ return scale(d).toFixed(2);
+ };
+ y2 = function (d) {
+ return scale(d).toFixed(2);
+ };
+ }
+ if (position === 'top') {
+ x1 = function (d) {
+ return scale(d).toFixed(2);
+ };
+ x2 = function (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 (d) {
+ return scale(d).toFixed(2);
+ };
+ x2 = function (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);
+
+ var labels;
+ group_g = mg_add_g(g, 'mg-group-' + mg_normalize(group));
+ if (args[groupAccessor] !== null) {
+ 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 {
+ 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) {
+ let 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 (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 (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;
diff --git a/priv/static/js/metricsgraphics/common/zoom.js b/priv/static/js/metricsgraphics/common/zoom.js
new file mode 100644
index 0000000..eb877f2
--- /dev/null
+++ b/priv/static/js/metricsgraphics/common/zoom.js
@@ -0,0 +1,85 @@
+{
+
+const filter_in_range_data = (args, range) => {
+ const 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 d => ['x', 'y'].every(dim => !(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
+const zoom_to_data_domain = (args, range) => {
+ const 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;
+ }
+ if (['x', 'y'].some(dim => range[dim][0] === range[dim][1])) return;
+ // 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));
+ }
+ if (mg_flatten_array(args.data).length === 0) return;
+ }
+ ['x', 'y'].forEach(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);
+};
+
+const 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
+const convert_range_to_domain = (args, range) =>
+ ['x', 'y'].reduce((domain, dim) => {
+ if (!(dim in range)) return domain;
+ domain[dim] = range[dim].map(v => +args.scales[dim.toUpperCase()].invert(v));
+ if (dim === 'y') domain[dim].reverse();
+ return domain;
+ }, {});
+
+const convert_domain_to_range = (args, domain) =>
+ ['x', 'y'].reduce((range, dim) => {
+ if (!(dim in domain)) return range;
+ range[dim] = domain[dim].map(v => +args.scales[dim.toUpperCase()](v));
+ if (dim === 'y') range[dim].reverse();
+ return range;
+ }, {});
+
+// the range here is the range of selection
+const zoom_to_data_range = (args, range) => {
+ const 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;
+
+}