summaryrefslogtreecommitdiff
path: root/priv/static/js/metricsgraphics
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--priv/static/js/metricsgraphics.min.js1
-rw-r--r--priv/static/js/metricsgraphics/MG.js1
-rw-r--r--priv/static/js/metricsgraphics/charts/bar.js792
-rw-r--r--priv/static/js/metricsgraphics/charts/histogram.js222
-rw-r--r--priv/static/js/metricsgraphics/charts/line.js922
-rw-r--r--priv/static/js/metricsgraphics/charts/missing.js144
-rw-r--r--priv/static/js/metricsgraphics/charts/point.js383
-rw-r--r--priv/static/js/metricsgraphics/charts/table.js220
-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
-rwxr-xr-xpriv/static/js/metricsgraphics/layout/bootstrap_dropdown.js177
-rw-r--r--priv/static/js/metricsgraphics/layout/button.js126
-rw-r--r--priv/static/js/metricsgraphics/misc/error.js16
-rw-r--r--priv/static/js/metricsgraphics/misc/formatters.js147
-rw-r--r--priv/static/js/metricsgraphics/misc/markup.js66
-rw-r--r--priv/static/js/metricsgraphics/misc/process.js368
-rw-r--r--priv/static/js/metricsgraphics/misc/smoothers.js280
-rw-r--r--priv/static/js/metricsgraphics/misc/transitions.js31
-rw-r--r--priv/static/js/metricsgraphics/misc/utility.js619
31 files changed, 8300 insertions, 0 deletions
diff --git a/priv/static/js/metricsgraphics.min.js b/priv/static/js/metricsgraphics.min.js
new file mode 100644
index 0000000..19e96b7
--- /dev/null
+++ b/priv/static/js/metricsgraphics.min.js
@@ -0,0 +1 @@
+!function(t,e){"function"==typeof define&&define.amd?define(["d3"],e):"object"==typeof exports?module.exports=e(require("d3")):t.MG=e(t.d3)}(this,function(E){"use strict";var c="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};function k(){return"undefined"!=typeof jQuery||"undefined"!=typeof $}function i(t,e){return t[e.x_accessor]>=e.processed.min_x&&t[e.x_accessor]<=e.processed.max_x&&t[e.y_accessor]>=e.processed.min_y&&t[e.y_accessor]<=e.processed.max_y}function u(t){return"[object Array]"===Object.prototype.toString.call(t)}function a(t){var e=t.map(function(t){return!0===u(t)&&0<t.length});return E.sum(e)===t.length}function n(t){var e=t.map(function(t){return!0==(e=t,"[object Object]"===Object.prototype.toString.call(e));var e});return E.sum(e)===t.length}function e(t){return u(e=t)&&0===e.length||n(t);var e}function w(t){return t.height-t.bottom}function v(t){return w(t)-t.buffer}function M(t){return t.top}function b(t){return M(t)+t.buffer}function G(t){return t.left}function Y(t){return G(t)+t.buffer}function A(t){return t.width-t.right}function P(t){return A(t)-t.buffer}function _(t){t.exit().remove()}function D(t,e){t.selectAll(e).remove()}function S(t,e){return t.append("g").classed(e,!0)}function r(t,e){var r=O(t.target),a=h(t.data),n=r.selectAll("line."+e).data(a);return n.enter().append("line").attr("class",e).attr("opacity",.3),_(n),_(n),n}function m(t,e,r){e.color_accessor?(t.attr("stroke",e.scalefns.colorf),t.classed(r,!1)):(t.attr("stroke",null),t.classed(r,!0))}function l(t,e){e&&t.attr({dy:0,transform:function(){var t=E.select(this);return"rotate("+e+" "+t.attr("x")+","+t.attr("y")+")"}})}function g(t){if(!(t=t.node()))return!1;for(var e=0;e<t.length;e++)if(o(t[e],t))return!0;return!1}function o(t,e){for(var r=t.getBoundingClientRect(),a=0;a<e.length;a++)if(e[a]!=t){var n=e[a].getBoundingClientRect();if(r.top===n.top&&!(n.left>r.right||n.right<r.left))return!0}return!1}function s(t,e){var r=h(t.data);return"string"==typeof(r=r[0][t[e+"_accessor"]])?"categorical":"numerical"}function O(t){return E.select(t).select("svg")}function h(t){var e=[];return e.concat.apply(e,t)}function z(t){if("string"==typeof t)return F(t);if(t instanceof window.HTMLElement){var e=t.getAttribute("data-mg-uid");return e||(void 0===MG._next_elem_id&&(MG._next_elem_id=0),e="mg-"+MG._next_elem_id++,t.setAttribute("data-mg-uid",e)),e}return console.warn("The specified target should be a string or an HTMLElement.",t),F(t)}function F(t){return t.replace(/[^a-zA-Z0-9 _-]+/g,"").replace(/ +?/g,"")}function d(t,e){return Number(E.select(t).style(e).replace(/px/g,""))}function f(t){return d(t,"width")}("undefined"==typeof window?global:window).MG={version:"2.11"},MG.convert={},MG.convert.date=function(t,e,r){r=void 0===r?"%Y-%m-%d":r;var a=E.timeParse(r);return t=t.map(function(t){return t[e]=a(t[e].trim()),t})},MG.convert.number=function(t,e){return t=t.map(function(t){return t[e]=Number(t[e]),t})},MG.time_format=function(t,e){return t?E.utcFormat(e):E.timeFormat(e)};var t=function(t,e,r){var a={};if(null===t)return t;if(Array.prototype.forEach&&t.forEach===Array.prototype.forEach)t.forEach(e,r);else if(t.length===+t.length){for(var n=0,o=t.length;n<o;n++)if(e.call(r,t[n],n,t)===a)return}else for(var i in t)if(e.call(r,t[i],i,t)===a)return;return t};function p(r){return t(Array.prototype.slice.call(arguments,1),function(t){if(t)for(var e in t)void 0===r[e]&&(r[e]=t[e])}),r}function x(t){return"[object Date]"===Object.prototype.toString.call(t)}function y(t){return Array.isArray?Array.isArray(t):"[object Array]"===Object.prototype.toString.call(t)}function C(t){return"[object Function]"===Object.prototype.toString.call(t)}function T(t,e){var r,a=[],n=[];for(r=0;r<e.length;r++)a[e[r]]=!0;for(r=0;r<t.length;r++)a[t[r]]||n.push(t[r]);return n}function R(t,e){console.warn("Deprecation: "+t+(e?". This feature will be removed in "+e+".":" the near future.")),console.trace()}function L(t,e,r){var a,n=0;for(t.textContent=e,a=t.getBBox();a.width>r&&(t.textContent=e.slice(0,--n)+"...",a=t.getBBox(),"..."!==t.textContent););}function j(t){var e=O(t.target);if(e.select(".mg-header").remove(),t.target&&t.title){var r=e.insert("text").attr("class","mg-header").attr("x",t.center_title_full_width?t.width/2:(t.width+t.left-t.right)/2).attr("y",t.title_y_position).attr("text-anchor","middle").attr("dy","0.55em");if(r.append("tspan").attr("class","mg-chart-title").text(t.title),t.show_tooltips&&t.description&&k()){r.append("tspan").attr("class","mg-chart-description").attr("dx","0.3em").text("");var a=$(r.node());a.popover({html:!0,animation:!1,placement:"top",content:t.description,container:t.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(){E.selectAll(t.target).selectAll(".mg-popover").remove(),$(this).popover("show"),$(E.select(t.target).select(".popover").node()).on("mouseleave",function(){a.popover("hide")})}).on("mouseleave",function(){setTimeout(function(){$(".popover:hover").length||a.popover("hide")},120)})}else t.show_tooltips&&t.description&&"undefined"==typeof $&&(t.error="In order to enable tooltips, please make sure you include jQuery.")}t.error&&Xe(t)}function X(e,t,r,a,n){e.scalefns[t]=function(t){return void 0===n?e.scales[r](t[a]):e.scales[r](t[a])+n}}function U(t,e){return"bottom"===t||"top"===t?[Y(e),P(e)]:"left"===t||"right"===t?[v(e),e.top]:void 0}function N(t,e){return E.set(t.map(function(t){return t[e]})).values()}function B(e){var t;return null===e.color_domain?"number"===e.color_type?t=E.extent(e.data[0],function(t){return t[e.color_accessor]}):"category"===e.color_type&&(t=N(e.data[0],e.color_accessor)):t=e.color_domain,t}function I(t){return null===t.color_range?"number"===t.color_type?["blue","red"]:null:t.color_range}function H(t,e,r){var a=xt(t),n=a.secondary(t.processed.min_x,t.processed.max_x);0===n.length&&(n=[t.scales.X.ticks(t.xax_count)[0]]);var o,i,s,c=S(r,"mg-year-marker");"default"===a.timeframe&&t.show_year_markers&&(o=t,i=c,s=n,a.yformat,i.selectAll(".mg-year-marker").data(s).enter().append("line").attr("x1",function(t){return o.scales.X(t).toFixed(2)}).attr("x2",function(t){return o.scales.X(t).toFixed(2)}).attr("y1",M(o)).attr("y2",w(o))),"years"!=a.tick_diff_timeframe&&function(t,e,r,a,n){var o,i,s,c,l,u=e.position,d=e.namespace,f=t.scales[d.toUpperCase()],p=E.select(t.target).select(".mg-x-axis text").node().getBoundingClientRect();"top"===u&&(o=function(t,e){return f(t).toFixed(2)},i=M(t)-7*t.xax_tick_length/3-p.height,s=".50em",c="middle",l=function(t){return n(new Date(t))});"bottom"===u&&(o=function(t,e){return f(t).toFixed(2)},i=w(t)+7*t.xax_tick_length/3+.8*p.height,s=".50em",c="middle",l=function(t){return n(new Date(t))});r.selectAll(".mg-year-marker").data(a).enter().append("text").attr("x",o).attr("y",i).attr("dy",s).attr("text-anchor",c).text(l)}(t,e,c,n,a.yformat)}function q(t,a,e){var r,n,o,i,s,c,l,u=e.namespace,d=(r=a,o=(n=e).position,i=n.namespace,s=r[i+"ax_tick_length"],c=r.scales[i.toUpperCase()],l={},"left"===o&&(l.x=G(r)-3*s/2,l.y=function(t){return c(t).toFixed(2)},l.dx=-3,l.dy=".35em",l.textAnchor="end",l.text=function(t){return J(r)(t)}),"right"===o&&(l.x=A(r)+3*s/2,l.y=function(t){return c(t).toFixed(2)},l.dx=3,l.dy=".35em",l.textAnchor="start",l.text=function(t){return J(r)(t)}),"top"===o&&(l.x=function(t){return c(t).toFixed(2)},l.y=(M(r)-7*s/3).toFixed(2),l.dx=0,l.dy="0em",l.textAnchor="middle",l.text=function(t){return ht(r)(t)}),"bottom"===o&&(l.x=function(t){return c(t).toFixed(2)},l.y=(w(r)+7*s/3).toFixed(2),l.dx=0,l.dy=".50em",l.textAnchor="middle",l.text=function(t){return ht(r)(t)}),l),f=a.processed[u+"_ticks"],p=t.selectAll(".mg-yax-labels").data(f).enter().append("text").attr("x",d.x).attr("dx",d.dx).attr("y",d.y).attr("dy",d.dy).attr("text-anchor",d.textAnchor).text(d.text);("x"==u&&(a.time_series&&a.european_clock?(p.append("tspan").classed("mg-european-hours",!0).text(function(t,e){var r=new Date(t);return 0===e?E.timeFormat("%H")(r):""}),p.append("tspan").classed("mg-european-minutes-seconds",!0).text(function(t,e){var r=new Date(t);return":"+a.processed.xax_format(r)})):p.text(function(t){return a.xax_units+a.processed.xax_format(t)}),a.time_series&&(a.show_years||a.show_secondary_x_label)&&H(a,e,t)),g(p))&&(p.filter(function(t,e){return(e+1)%2==0}).remove(),O(a.target).selectAll(".mg-"+u+"ax-ticks").filter(function(t,e){return(e+1)%2==0}).remove())}function V(t,e,r){var a,n,o,i,s,c,l,u,d=r.namespace,f=e.processed[d+"_ticks"].length,p=(a=e,o=(n=r).namespace,i=n.position,s=a.processed[o+"_ticks"].length,c=a.processed[o+"_ticks"],l=a.scales[o.toUpperCase()],u={},"left"===i&&(u.x1=G(a),u.x2=G(a),u.y1=l(c[0]).toFixed(2),u.y2=l(c[s-1]).toFixed(2)),"right"===i&&(u.x1=A(a),u.x2=A(a),u.y1=l(c[0]).toFixed(2),u.y2=l(c[s-1]).toFixed(2)),"top"===i&&(u.x1=G(a),u.x2=A(a),u.y1=M(a),u.y2=M(a)),"bottom"===i&&(u.x1=G(a),u.x2=A(a),u.y1=w(a),u.y2=w(a)),"left"!==i&&"right"!==i||(a.axes_not_compact?(u.y1=w(a),u.y2=M(a)):s&&(u.y1=l(c[0]).toFixed(2),u.y2=l(c[s-1]).toFixed(2))),u);e[d+"_extended_ticks"]||e[d+"_extended_ticks"]||!f||t.append("line").attr("x1",p.x1).attr("x2",p.x2).attr("y1",p.y1).attr("y2",p.y2)}function W(t,e){t.rug_buffer_size="point"===t.chart_type?t.buffer/2:2*t.buffer/3;var r,a,n,o,i,s,c,l,u,d,f=(r=t,a="mg-"+e.namespace+"-rug",n=O(r.target),o=h(r.data),(i=n.selectAll("line."+a).data(o)).enter().append("svg:line").attr("class",a).attr("opacity",.3),_(i),_(i),i),p=(s=t,l=(c=e).position,u=c.namespace,d={},"left"===l&&(d.x1=G(s)+1,d.x2=G(s)+s.rug_buffer_size,d.y1=s.scalefns[u+"f"],d.y2=s.scalefns[u+"f"]),"right"===l&&(d.x1=A(s)-1,d.x2=A(s)-s.rug_buffer_size,d.y1=s.scalefns[u+"f"],d.y2=s.scalefns[u+"f"]),"top"===l&&(d.x1=s.scalefns[u+"f"],d.x2=s.scalefns[u+"f"],d.y1=M(s)+1,d.y2=M(s)+s.rug_buffer_size),"bottom"===l&&(d.x1=s.scalefns[u+"f"],d.x2=s.scalefns[u+"f"],d.y1=w(s)-1,d.y2=w(s)-s.rug_buffer_size),d);f.attr("x1",p.x1).attr("x2",p.x2).attr("y1",p.y1).attr("y2",p.y2),m(f,t,"mg-"+e.namespace+"-rug-mono")}function Q(p,_){var m=_.namespace,t="mg-"+m+"-axis",g=p.scales[m.toUpperCase()],e=p.scales[(m+"group").toUpperCase()],h=m+"group_accessor",r=O(p.target);D(r,"."+t);var x,y=S(r,t);(e.domain&&e.domain()?e.domain():["1"]).forEach(function(t){var e,r,a,n,o,i,s,c,l,u,d=(e=p,a=t,n=(r=_).namespace,o=r.position,i=e.scales[n.toUpperCase()],s=e.scales[(n+"group").toUpperCase()],c={cat:{},group:{}},"left"===o&&(c.cat.x=Y(e)-e.buffer,c.cat.y=function(t){return s(a)+i(t)+i.bandwidth()/2},c.cat.dy=".35em",c.cat.textAnchor="end",c.group.x=Y(e)-e.buffer,c.group.y=s(a)+(s.bandwidth?s.bandwidth()/2:0),c.group.dy=".35em",c.group.textAnchor=(e["rotate_"+n+"_labels"],"end")),"right"===o&&(c.cat.x=P(e)-e.buffer,c.cat.y=function(t){return s(a)+i(t)+i.bandwidth()/2},c.cat.dy=".35em",c.cat.textAnchor="start",c.group.x=P(e)-e.buffer,c.group.y=s(a)+(s.bandwidth?s.bandwidth()/2:0),c.group.dy=".35em",c.group.textAnchor="start"),"top"===o&&(c.cat.x=function(t){return s(a)+i(t)+i.bandwidth()/2},c.cat.y=b(e)+e.buffer,c.cat.dy=".35em",c.cat.textAnchor=e["rotate_"+n+"_labels"]?"start":"middle",c.group.x=s(a)+(s.bandwidth?s.bandwidth()/2:0),c.group.y=b(e)+e.buffer,c.group.dy=".35em",c.group.textAnchor=e["rotate_"+n+"_labels"]?"start":"middle"),"bottom"===o&&(c.cat.x=function(t){return s(a)+i(t)+i.bandwidth()/2},c.cat.y=v(e)+e.buffer,c.cat.dy=".35em",c.cat.textAnchor=e["rotate_"+n+"_labels"]?"start":"middle",c.group.x=s(a)+(s.bandwidth?s.bandwidth()/2-i.bandwidth()/2:0),c.group.y=v(e)+e.buffer,c.group.dy=".35em",c.group.textAnchor=e["rotate_"+n+"_labels"]?"start":"middle"),c);if(x=S(y,"mg-group-"+F(t)),null!==p[h])var f=x.append("text").classed("mg-barplot-group-label",!0).attr("x",d.group.x).attr("y",d.group.y).attr("dy",d.group.dy).attr("text-anchor",d.group.textAnchor).text(t);else f=x.selectAll("text").data(g.domain()).enter().append("text").attr("x",d.cat.x).attr("y",d.cat.y).attr("dy",d.cat.dy).attr("text-anchor",d.cat.textAnchor).text(String);p["rotate_"+m+"_labels"]&&(l=f,(u=p["rotate_"+m+"_labels"])&&l.attr("transform",function(){var t=E.select(this);return"rotate("+u+" "+t.attr("x")+","+t.attr("y")+")"}))})}MG.merge_with_defaults=p,MG.clone=function(t){var e,r;if(null===t||"object"!==(void 0===t?"undefined":c(t)))return t;if(x(t))return(e=new Date).setTime(t.getTime()),e;if(y(t)){e=[];for(var a=0,n=t.length;a<n;a++)e[a]=MG.clone(t[a]);return e}if(r=t,"[object Object]"===Object.prototype.toString.call(r)){for(var o in e={},t)t.hasOwnProperty(o)&&(e[o]=MG.clone(t[o]));return e}throw new Error("Unable to copy obj! Its type isn't supported.")},MG.arr_diff=T,MG.warn_deprecation=R,MG.truncate_text=L,MG.wrap_text=function(t,i,s,c){t.each(function(){for(var t,e=E.select(this),r=e.text().split(s||/\s+/).reverse(),a=[],n=0,o=(e.attr("y"),e.text(null).append("tspan").attr("x",0).attr("y","0em").attr(c||{}));t=r.pop();)a.push(t),o.text(a.join(" ")),(null===i||o.node().getComputedTextLength()>i)&&(a.pop(),o.text(a.join(" ")),a=[t],o=e.append("tspan").attr("x",0).attr("y",1.1*++n+0+"em").attr(c||{}).text(t))})},MG.register=function(t,e,r){MG.charts[t]={descriptor:e,defaults:r||{}}},MG._hooks={},MG.add_hook=function(t,e,r){var a;if(MG._hooks[t]||(MG._hooks[t]=[]),0<(a=MG._hooks[t]).filter(function(t){return t.func===e}).length)throw"That function is already registered.";a.push({func:e,context:r})},MG.call_hook=function(t){var r,e=MG._hooks[t],a=[].slice.apply(arguments,[1]);return e&&e.forEach(function(t){if(t.func){var e=r||a;e&&e.constructor!==Array&&(e=[e]),e=[].concat.apply([],e),r=t.func.apply(t.context,e)}}),r||a},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=!1,MG.globals.version="1.1",MG.charts={},MG.data_graphic=function(t){var e={missing_is_zero:!1,missing_is_hidden:!1,missing_is_hidden_accessor:null,legend:"",legend_target:"",error:"",animate_on_load:!1,top:65,title_y_position:10,center_title_full_width:!1,bottom:45,right:10,left:50,buffer:8,width:350,height:220,full_width:!1,full_height:!1,small_height_threshold:120,small_width_threshold:160,xax_count:6,xax_tick_length:5,axes_not_compact:!0,yax_count:3,yax_tick_length:5,x_extended_ticks:!1,y_extended_ticks:!1,y_scale_type:"linear",max_x:null,max_y:null,min_x:null,min_y:null,min_y_from_data:!1,point_size:2.5,active_point_on_lines:!1,active_point_accessor:"active",active_point_size:2,points_always_visible:!1,x_accessor:"date",xax_units:"",x_label:"",x_sort:!0,x_axis:!0,y_axis:!0,x_axis_position:"bottom",y_axis_position:"left",x_axis_type:null,y_axis_type:null,ygroup_accessor:null,xgroup_accessor:null,y_padding_percentage:.05,y_outer_padding_percentage:.1,ygroup_padding_percentage:.25,ygroup_outer_padding_percentage:0,x_padding_percentage:.05,x_outer_padding_percentage:.1,xgroup_padding_percentage:.25,xgroup_outer_padding_percentage:0,y_categorical_show_guides:!1,x_categorical_show_guide:!1,rotate_x_labels:0,rotate_y_labels:0,y_accessor:"value",y_label:"",yax_units:"",yax_units_append:!1,x_rug:!1,y_rug:!1,mouseover_align:"right",x_mouseover:null,y_mouseover:null,transition_on_update:!0,mouseover:null,click:null,show_rollover_text:!0,show_confidence_band:null,xax_format:null,area:!0,flip_area_under_y_value:null,chart_type:"line",data:[],decimals:2,format:"count",inflator:10/9,linked:!1,linked_format:"%Y-%m-%d",list:!1,baselines:null,markers:null,scalefns:{},scales:{},utc_time:!1,european_clock:!1,show_year_markers:!1,show_secondary_x_label:!0,target:"#viz",interpolate:E.curveCatmullRom.alpha(0),custom_line_color_map:[],colors:null,max_data_size:null,aggregate_rollover:!1,show_tooltips:!0,showActivePoint:!0,brush:null,zoom_target:null,brushing_selection_changed:null};MG.call_hook("global.defaults",e),t||(t={});var r=MG.charts[t.chart_type||e.chart_type];for(var a in p(t,r.defaults,e),t.list&&(t.x_accessor=0,t.y_accessor=1),MG.deprecations)if(t.hasOwnProperty(a)){var n=MG.deprecations[a],o="Use of `args."+a+"` has been deprecated",i=n.replacement;if(i&&(t[i]?o+=". The replacement - `args."+i+"` - has already been defined. This definition will be discarded.":t[i]=t[a]),n.warned)continue;n.warned=!0,i&&(o+=" in favor of `args."+i+"`"),R(o,n.version)}return MG.call_hook("global.before_init",t),new r.descriptor(t),t.data},k()&&(function(m){var g=function(t,e){this.type=null,this.options=null,this.enabled=null,this.timeout=null,this.hoverState=null,this.$element=null,this.inState=null,this.init("tooltip",t,e)};g.VERSION="3.3.5",g.TRANSITION_DURATION=150,g.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},g.prototype.init=function(t,e,r){if(this.enabled=!0,this.type=t,this.$element=m(e),this.options=this.getOptions(r),this.$viewport=this.options.viewport&&m(m.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},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!");for(var a=this.options.trigger.split(" "),n=a.length;n--;){var o=a[n];if("click"==o)this.$element.on("click."+this.type,this.options.selector,m.proxy(this.toggle,this));else if("manual"!=o){var i="hover"==o?"mouseenter":"focusin",s="hover"==o?"mouseleave":"focusout";this.$element.on(i+"."+this.type,this.options.selector,m.proxy(this.enter,this)),this.$element.on(s+"."+this.type,this.options.selector,m.proxy(this.leave,this))}}this.options.selector?this._options=m.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},g.prototype.getDefaults=function(){return g.DEFAULTS},g.prototype.getOptions=function(t){return(t=m.extend({},this.getDefaults(),this.$element.data(),t)).delay&&"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),t},g.prototype.getDelegateOptions=function(){var r={},a=this.getDefaults();return this._options&&m.each(this._options,function(t,e){a[t]!=e&&(r[t]=e)}),r},g.prototype.enter=function(t){var e=t instanceof this.constructor?t:m(t.currentTarget).data("bs."+this.type);if(e||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),m(t.currentTarget).data("bs."+this.type,e)),t instanceof m.Event&&(e.inState["focusin"==t.type?"focus":"hover"]=!0),e.tip().hasClass("in")||"in"==e.hoverState)e.hoverState="in";else{if(clearTimeout(e.timeout),e.hoverState="in",!e.options.delay||!e.options.delay.show)return e.show();e.timeout=setTimeout(function(){"in"==e.hoverState&&e.show()},e.options.delay.show)}},g.prototype.isInStateTrue=function(){for(var t in this.inState)if(this.inState[t])return!0;return!1},g.prototype.leave=function(t){var e=t instanceof this.constructor?t:m(t.currentTarget).data("bs."+this.type);if(e||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),m(t.currentTarget).data("bs."+this.type,e)),t instanceof m.Event&&(e.inState["focusout"==t.type?"focus":"hover"]=!1),!e.isInStateTrue()){if(clearTimeout(e.timeout),e.hoverState="out",!e.options.delay||!e.options.delay.hide)return e.hide();e.timeout=setTimeout(function(){"out"==e.hoverState&&e.hide()},e.options.delay.hide)}},g.prototype.show=function(){var t=m.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(t);var e=m.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(t.isDefaultPrevented()||!e)return;var r=this,a=this.tip(),n=this.getUID(this.type);this.setContent(),a.attr("id",n),this.$element.attr("aria-describedby",n),this.options.animation&&a.addClass("fade");var o="function"==typeof this.options.placement?this.options.placement.call(this,a[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,s=i.test(o);s&&(o=o.replace(i,"")||"top"),a.detach().css({top:0,left:0,display:"block"}).addClass(o).data("bs."+this.type,this),this.options.container?a.appendTo(this.options.container):a.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var c=this.getPosition(),l=a[0].offsetWidth,u=a[0].offsetHeight;if(s){var d=o,f=this.getPosition(this.$viewport);o="bottom"==o&&c.bottom+u>f.bottom?"top":"top"==o&&c.top-u<f.top?"bottom":"right"==o&&c.right+l>f.width?"left":"left"==o&&c.left-l<f.left?"right":o,a.removeClass(d).addClass(o)}var p=this.getCalculatedOffset(o,c,l,u);this.applyPlacement(p,o);var _=function(){var t=r.hoverState;r.$element.trigger("shown.bs."+r.type),r.hoverState=null,"out"==t&&r.leave(r)};m.support.transition&&this.$tip.hasClass("fade")?a.one("bsTransitionEnd",_).emulateTransitionEnd(g.TRANSITION_DURATION):_()}},g.prototype.applyPlacement=function(t,e){var r=this.tip(),a=r[0].offsetWidth,n=r[0].offsetHeight,o=parseInt(r.css("margin-top"),10),i=parseInt(r.css("margin-left"),10);isNaN(o)&&(o=0),isNaN(i)&&(i=0),t.top+=o,t.left+=i,m.offset.setOffset(r[0],m.extend({using:function(t){r.css({top:Math.round(t.top),left:Math.round(t.left)})}},t),0),r.addClass("in");var s=r[0].offsetWidth,c=r[0].offsetHeight;"top"==e&&c!=n&&(t.top=t.top+n-c);var l=this.getViewportAdjustedDelta(e,t,s,c);l.left?t.left+=l.left:t.top+=l.top;var u=/top|bottom/.test(e),d=u?2*l.left-a+s:2*l.top-n+c,f=u?"offsetWidth":"offsetHeight";r.offset(t),this.replaceArrow(d,r[0][f],u)},g.prototype.replaceArrow=function(t,e,r){this.arrow().css(r?"left":"top",50*(1-t/e)+"%").css(r?"top":"left","")},g.prototype.setContent=function(){var t=this.tip(),e=this.getTitle();t.find(".tooltip-inner")[this.options.html?"html":"text"](e),t.removeClass("fade in top bottom left right")},g.prototype.hide=function(t){var e=this,r=m(this.$tip),a=m.Event("hide.bs."+this.type);function n(){"in"!=e.hoverState&&r.detach(),e.$element.removeAttr("aria-describedby").trigger("hidden.bs."+e.type),t&&t()}if(this.$element.trigger(a),!a.isDefaultPrevented())return r.removeClass("in"),m.support.transition&&r.hasClass("fade")?r.one("bsTransitionEnd",n).emulateTransitionEnd(g.TRANSITION_DURATION):n(),this.hoverState=null,this},g.prototype.fixTitle=function(){var t=this.$element;(t.attr("title")||"string"!=typeof t.attr("data-original-title"))&&t.attr("data-original-title",t.attr("title")||"").attr("title","")},g.prototype.hasContent=function(){return this.getTitle()},g.prototype.getPosition=function(t){var e=(t=t||this.$element)[0],r="BODY"==e.tagName,a=e.getBoundingClientRect();null==a.width&&(a=m.extend({},a,{width:a.right-a.left,height:a.bottom-a.top}));var n=r?{top:0,left:0}:t.offset(),o={scroll:r?document.documentElement.scrollTop||document.body.scrollTop:t.scrollTop()},i=r?{width:m(window).width(),height:m(window).height()}:null;return m.extend({},a,o,i,n)},g.prototype.getCalculatedOffset=function(t,e,r,a){return"bottom"==t?{top:e.top+e.height,left:e.left+e.width/2-r/2}:"top"==t?{top:e.top-a,left:e.left+e.width/2-r/2}:"left"==t?{top:e.top+e.height/2-a/2,left:e.left-r}:{top:e.top+e.height/2-a/2,left:e.left+e.width}},g.prototype.getViewportAdjustedDelta=function(t,e,r,a){var n={top:0,left:0};if(!this.$viewport)return n;var o=this.options.viewport&&this.options.viewport.padding||0,i=this.getPosition(this.$viewport);if(/right|left/.test(t)){var s=e.top-o-i.scroll,c=e.top+o-i.scroll+a;s<i.top?n.top=i.top-s:c>i.top+i.height&&(n.top=i.top+i.height-c)}else{var l=e.left-o,u=e.left+o+r;l<i.left?n.left=i.left-l:u>i.right&&(n.left=i.left+i.width-u)}return n},g.prototype.getTitle=function(){var t=this.$element,e=this.options;return t.attr("data-original-title")||("function"==typeof e.title?e.title.call(t[0]):e.title)},g.prototype.getUID=function(t){for(;t+=~~(1e6*Math.random()),document.getElementById(t););return t},g.prototype.tip=function(){if(!this.$tip&&(this.$tip=m(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},g.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},g.prototype.enable=function(){this.enabled=!0},g.prototype.disable=function(){this.enabled=!1},g.prototype.toggleEnabled=function(){this.enabled=!this.enabled},g.prototype.toggle=function(t){var e=this;t&&((e=m(t.currentTarget).data("bs."+this.type))||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),m(t.currentTarget).data("bs."+this.type,e))),t?(e.inState.click=!e.inState.click,e.isInStateTrue()?e.enter(e):e.leave(e)):e.tip().hasClass("in")?e.leave(e):e.enter(e)},g.prototype.destroy=function(){var t=this;clearTimeout(this.timeout),this.hide(function(){t.$element.off("."+t.type).removeData("bs."+t.type),t.$tip&&t.$tip.detach(),t.$tip=null,t.$arrow=null,t.$viewport=null})};var t=m.fn.tooltip;m.fn.tooltip=function(a){return this.each(function(){var t=m(this),e=t.data("bs.tooltip"),r="object"==(void 0===a?"undefined":c(a))&&a;!e&&/destroy|hide/.test(a)||(e||t.data("bs.tooltip",e=new g(this,r)),"string"==typeof a&&e[a]())})},m.fn.tooltip.Constructor=g,m.fn.tooltip.noConflict=function(){return m.fn.tooltip=t,this}}(jQuery),function(n){var o=function(t,e){this.init("popover",t,e)};if(!n.fn.tooltip)throw new Error("Popover requires tooltip.js");o.VERSION="3.3.5",o.DEFAULTS=n.extend({},n.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>'}),((o.prototype=n.extend({},n.fn.tooltip.Constructor.prototype)).constructor=o).prototype.getDefaults=function(){return o.DEFAULTS},o.prototype.setContent=function(){var t=this.tip(),e=this.getTitle(),r=this.getContent();t.find(".popover-title")[this.options.html?"html":"text"](e),t.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof r?"html":"append":"text"](r),t.removeClass("fade top bottom left right in"),t.find(".popover-title").html()||t.find(".popover-title").hide()},o.prototype.hasContent=function(){return this.getTitle()||this.getContent()},o.prototype.getContent=function(){var t=this.$element,e=this.options;return t.attr("data-content")||("function"==typeof e.content?e.content.call(t[0]):e.content)},o.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var t=n.fn.popover;n.fn.popover=function(a){return this.each(function(){var t=n(this),e=t.data("bs.popover"),r="object"==(void 0===a?"undefined":c(a))&&a;!e&&/destroy|hide/.test(a)||(e||t.data("bs.popover",e=new o(this,r)),"string"==typeof a&&e[a]())})},n.fn.popover.Constructor=o,n.fn.popover.noConflict=function(){return n.fn.popover=t,this}}(jQuery)),MG.chart_title=j,MG.scale_factory=function(o){var i={use_inflator:!1,zero_bottom:!1,scaleType:"numerical"};return this.namespace=function(t){return i.namespace=t,i.namespace_accessor_name=i.namespace+"_accessor",i.scale_name=i.namespace.toUpperCase(),i.scalefn_name=i.namespace+"f",this},this.scaleName=function(t){return i.scale_name=t.toUpperCase(),i.scalefn_name=t+"f",this},this.inflateDomain=function(t){return i.use_inflator=t,this},this.zeroBottom=function(t){return i.zero_bottom=t,this},this.numericalDomainFromData=function(){var t,e=[];0<arguments.length&&(e=arguments);for(var r=0;r<o.data.length;r++)0<o.data[r].length&&(t=o.data[r]);i.is_time_series=!!x(t[0][o[i.namespace_accessor_name]]),X(o,i.scalefn_name,i.scale_name,o[i.namespace_accessor_name]),function(t,e,r){var a=e.namespace,n=e.namespace_accessor_name,o=e.use_inflator,i=e.zero_bottom,s=t[n],c=h(t.data).map(function(t){return t[s]}).concat(h(r));"log"===t[a+"_scale_type"]&&(c=c.filter(function(t){return 0<t}));var l=E.extent(c),u=l[0],d=l[1];i&&!t["min_"+a+"_from_data"]&&0<u&&!e.is_time_series&&(u="log"===t[a+"_scale_type"]?1:0),"log"!==t[a+"_scale_type"]&&u<0&&!e.is_time_series&&(u-=(u-u*t.inflator)*o),e.is_time_series||(d=d<0?d+(d-d*t.inflator)*o:d*(o?t.inflator:1)),u=null!=t["min_"+a]?t["min_"+a]:u,d=null!=t["max_"+a]?t["max_"+a]:d,u===d&&null==t["min_"+a]&&null==t["max_"+a]&&(x(u)?d=new Date(MG.clone(u).setDate(u.getDate()+1)):"number"==typeof u&&(d=u+1,t.xax_count=2)),t.processed["min_"+a]=u,t.processed["max_"+a]=d,t.processed["zoom_"+a]&&(t.processed["min_"+a]=t.processed["zoom_"+a][0],t.processed["max_"+a]=t.processed["zoom_"+a][1]),MG.call_hook("x_axis.process_min_max",t,t.processed.min_x,t.processed.max_x),MG.call_hook("y_axis.process_min_max",t,t.processed.min_y,t.processed.max_y)}(o,i,e,i.use_inflator);var a=o.utc_time?E.scaleUtc():E.scaleTime();return o.scales[i.scale_name]=i.is_time_series?a:C(o[i.namespace+"_scale_type"])?o.y_scale_type():"log"===o[i.namespace+"_scale_type"]?E.scaleLog():E.scaleLinear(),o.scales[i.scale_name].domain([o.processed["min_"+i.namespace],o.processed["max_"+i.namespace]]),i.scaleType="numerical",this},this.categoricalDomain=function(t){return o.scales[i.scale_name]=E.scaleOrdinal().domain(t),X(o,i.scalefn_name,i.scale_name,o[i.namespace_accessor_name]),this},this.categoricalDomainFromData=function(){var t=h(o.data);return i.categoricalVariables=E.set(t.map(function(t){return t[o[i.namespace_accessor_name]]})).values(),o.scales[i.scale_name]=E.scaleBand().domain(i.categoricalVariables),i.scaleType="categorical",this},this.numericalRange=function(t){return"string"==typeof t?o.scales[i.scale_name].range(U(t,o)):o.scales[i.scale_name].range(t),this},this.categoricalRangeBands=function(t,e){void 0===e&&(e=!1);var r=i.namespace,a=o[r+"_padding_percentage"],n=o[r+"_outer_padding_percentage"];return"string"==typeof t?o.scales[i.scale_name].range(U(t,o)).paddingInner(a).paddingOuter(n):o.scales[i.scale_name].range(t).paddingInner(a).paddingOuter(n),X(o,i.scalefn_name,i.scale_name,o[i.namespace_accessor_name],e?o.scales[i.scale_name].bandwidth()/2:0),this},this.categoricalRange=function(t){return o.scales[i.scale_name].range(t),X(o,i.scalefn_name,i.scale_name,o[i.namespace_accessor_name]),this},this.categoricalColorRange=function(){return o.scales[i.scale_name]=10<o.scales[i.scale_name].domain().length?E.scaleOrdinal(E.schemeCategory20):E.scaleOrdinal(E.schemeCategory10),o.scales[i.scale_name].domain(i.categoricalVariables),X(o,i.scalefn_name,i.scale_name,o[i.namespace_accessor_name]),this},this.clamp=function(t){return o.scales[i.scale_name].clamp(t),this},this};var Z={};function K(t){if(t.y_rug){t.rug_buffer_size="point"===t.chart_type?t.buffer/2:2*t.buffer/3;var e=r(t,"mg-y-rug");e.attr("x1",t.left+1).attr("x2",t.left+t.rug_buffer_size).attr("y1",t.scalefns.yf).attr("y2",t.scalefns.yf),m(e,t,"mg-y-rug-mono")}}function J(r){var t=r.yax_format;if(!t){var a=r.decimals;"count"===r.format?(1<r.processed.y_ticks.length&&(a=Math.max(0,-Math.floor(Math.log(Math.abs(r.processed.y_ticks[1]-r.processed.y_ticks[0]))/Math.LN10))),t=function(t){var e;return e=0!==a?E.format(",."+a+"f"):t<1e3?E.format(",.0f"):E.format(",.2s"),r.yax_units_append?e(t)+r.yax_units:r.yax_units+e(t)}):t=function(t){return E.format(".0%")(t)}}return t}function tt(t){var e=O(t.target),r=t.scales.X.domain();if(r[0]<=0&&0<=r[1]){var a=t.scales.Y.range(),n=t.categorical_groups.length?t.scales.YGROUP(t.categorical_groups[t.categorical_groups.length-1]):t.scales.YGROUP();e.append("svg:line").attr("x1",t.scales.X(0)).attr("x2",t.scales.X(0)).attr("y1",a[0]+b(t)).attr("y2",a[a.length-1]+n).attr("stroke","black").attr("opacity",.2)}}function et(t,e){e.y_label&&t.append("text").attr("class","label").attr("x",function(){return-1*(b(e)+(v(e)-b(e))/2)}).attr("y",function(){return e.left/2}).attr("dy","-1.2em").attr("text-anchor","middle").text(function(t){return e.y_label}).attr("transform",function(t){return"rotate(-90)"})}function rt(t){t.processed||(t.processed={});var e=O(t.target);if(MG.call_hook("y_axis.process_min_max",t,t.processed.min_y,t.processed.max_y),D(e,".mg-y-axis"),!t.y_axis)return this;var r,a,n,o,i,s,c,l,u,d=S(e,"mg-y-axis");return et(d,t),ke(t,"y"),r=d,i=(a=t).processed.y_ticks.length,a.x_extended_ticks||a.y_extended_ticks||!i||(a.axes_not_compact&&"bar"!==a.chart_type?(n=a.height-a.bottom,o=a.top):i?(n=a.scales.Y(a.processed.y_ticks[0]).toFixed(2),o=a.scales.Y(a.processed.y_ticks[i-1]).toFixed(2)):o=n=0,r.append("line").attr("x1",a.left).attr("x2",a.left).attr("y1",n).attr("y2",o)),s=t,d.selectAll(".mg-yax-ticks").data(s.processed.y_ticks).enter().append("line").classed("mg-extended-yax-ticks",s.y_extended_ticks).attr("x1",s.left).attr("x2",function(){return s.y_extended_ticks?s.width-s.right:s.left-s.yax_tick_length}).attr("y1",function(t){return s.scales.Y(t).toFixed(2)}).attr("y2",function(t){return s.scales.Y(t).toFixed(2)}),c=d,u=J(l=t),c.selectAll(".mg-yax-labels").data(l.processed.y_ticks).enter().append("text").attr("x",l.left-3*l.yax_tick_length/2).attr("dx",-3).attr("y",function(t){return l.scales.Y(t).toFixed(2)}).attr("dy",".35em").attr("text-anchor","end").text(function(t){return u(t)}),t.y_rug&&K(t),this}function at(o){var t=O(o.target);D(t,".mg-y-axis");var i,s=S(t,"mg-y-axis");(o.categorical_groups.length?o.categorical_groups:["1"]).forEach(function(t){var e,r,a,n;(i=S(s,"mg-group-"+F(t)),null!==o.ygroup_accessor)?(a=t,n=o,i.append("svg:text").classed("mg-barplot-group-label",!0).attr("x",n.left-n.buffer).attr("y",n.scales.YGROUP(a)+n.scales.YGROUP.bandwidth()/2).attr("dy",".35em").attr("text-anchor","end").text(a)):l((e=t,r=o,i.selectAll("text").data(r.scales.Y.domain()).enter().append("svg:text").attr("x",r.left-r.buffer).attr("y",function(t){return r.scales.YGROUP(e)+r.scales.Y(t)+r.scales.Y.bandwidth()/2}).attr("dy",".35em").attr("text-anchor","end").text(String)),o.rotate_y_labels)})}function nt(t){if(t.x_rug){t.rug_buffer_size="point"===t.chart_type?t.buffer/2:t.buffer;var e=r(t,"mg-x-rug");e.attr("x1",t.scalefns.xf).attr("x2",t.scalefns.xf).attr("y1",t.height-t.bottom-t.rug_buffer_size).attr("y2",t.height-t.bottom),m(e,t,"mg-x-rug-mono")}}function ot(t){var e,r,a,n=O(t.target);if((e=t).processed||(e.processed={}),a=(r=t).chart_type,r.processed.xax_format||(r.xax_format?r.processed.xax_format=r.xax_format:"line"===a||"point"===a||"histogram"===a?r.processed.xax_format=ht(r):"bar"===a&&(r.processed.xax_format=ct(r))),D(n,".mg-x-axis"),!t.x_axis)return this;var o,i,s,c,l,u,d,f,p,_,m=S(n,"mg-x-axis");return o=m,ke(i=t,"x"),l=o,u=(c=i).scales.X.ticks(c.xax_count).length-1,c.x_extended_ticks||l.append("line").attr("x1",function(){return 0===c.xax_count?Y(c):c.axes_not_compact&&"bar"!==c.chart_type?c.left:c.scales.X(c.scales.X.ticks(c.xax_count)[0]).toFixed(2)}).attr("x2",function(){return 0===c.xax_count||c.axes_not_compact&&"bar"!==c.chart_type?A(c):c.scales.X(c.scales.X.ticks(c.xax_count)[u]).toFixed(2)}).attr("y1",c.height-c.bottom).attr("y2",c.height-c.bottom),s=i,o.selectAll(".mg-xax-ticks").data(s.processed.x_ticks).enter().append("line").attr("x1",function(t){return s.scales.X(t).toFixed(2)}).attr("x2",function(t){return s.scales.X(t).toFixed(2)}).attr("y1",s.height-s.bottom).attr("y2",function(){return s.x_extended_ticks?s.top:s.height-s.bottom+s.xax_tick_length}).attr("class",function(){if(s.x_extended_ticks)return"mg-extended-xax-ticks"}).classed("mg-xax-ticks",!0),function(a,t){var e=t.selectAll(".mg-xax-labels").data(a.processed.x_ticks).enter().append("text").attr("x",function(t){return a.scales.X(t).toFixed(2)}).attr("y",(a.height-a.bottom+7*a.xax_tick_length/3).toFixed(2)).attr("dy",".50em").attr("text-anchor","middle");if(a.time_series&&a.european_clock?(e.append("tspan").classed("mg-european-hours",!0).text(function(t,e){var r=new Date(t);return 0===e?E.timeFormat("%H")(r):""}),e.append("tspan").classed("mg-european-minutes-seconds",!0).text(function(t,e){var r=new Date(t);return":"+a.processed.xax_format(r)})):e.text(function(t){return a.xax_units+a.processed.xax_format(t)}),g(e)){e.filter(function(t,e){return(e+1)%2==0}).remove();var r=O(a.target);r.selectAll(".mg-xax-ticks").filter(function(t,e){return(e+1)%2==0}).remove()}}(f=t,d=m),_=d,(p=f).time_series&&(p.show_years||p.show_secondary_x_label)&&function(t,e){var r=xt(t),a=r.secondary(t.processed.min_x,t.processed.max_x);if(0===a.length){var n=t.scales.X.ticks(t.xax_count)[0];a=[n]}var o,i,s,c,l,u,d,f=S(e,"mg-year-marker");"default"===r.timeframe&&t.show_year_markers&&(o=t,i=f,s=a,r.yformat,i.selectAll(".mg-year-marker").data(s).enter().append("line").attr("x1",function(t){return o.scales.X(t).toFixed(2)}).attr("x2",function(t){return o.scales.X(t).toFixed(2)}).attr("y1",M(o)).attr("y2",w(o))),"years"!=r.tick_diff_time_frame&&(c=t,l=f,u=a,d=r.yformat,l.selectAll(".mg-year-marker").data(u).enter().append("text").attr("x",function(t,e){return c.scales.X(t).toFixed(2)}).attr("y",function(){var t=E.select(c.target).select(".mg-x-axis text").node().getBoundingClientRect();return w(c)+7*c.xax_tick_length/3+.8*t.height}).attr("dy",".50em").attr("text-anchor","middle").text(function(t){return d(new Date(t))}))}(p,_),t.x_label&&st(m,t),t.x_rug&&nt(t),this}function B(e){var t;return null===e.color_domain?"number"===e.color_type?t=E.extent(e.data[0],function(t){return t[e.color_accessor]}):"category"===e.color_type&&(t=E.set(e.data[0].map(function(t){return t[e.color_accessor]})).values()).sort():t=e.color_domain,t}function I(t){return null===t.color_range?"number"===t.color_type?["blue","red"]:null:t.color_range}function it(t){return null===t.size_range?[1,5]:t.size_range}function st(t,e){e.x_label&&t.append("text").attr("class","label").attr("x",function(){return Y(e)+(P(e)-Y(e))/2}).attr("dx",null!=e.x_label_nudge_x?e.x_label_nudge_x:0).attr("y",function(){var t=E.select(e.target).select(".mg-x-axis text").node().getBoundingClientRect();return w(e)+e.xax_tick_length*(7/3)+.8*t.height+10}).attr("dy",".5em").attr("text-anchor","middle").text(function(t){return e.x_label})}function ct(r){return function(t){if(t<1&&-1<t&&0!==t)return r.xax_units+t.toFixed(r.decimals);var e=E.format(",.0f");return r.xax_units+e(t)}}function lt(t){var e;return ut(t)?e="millis":dt(t)?e="seconds":ft(t)?e="less-than-a-day":pt(t)?e="four-days":_t(t)?e="many-days":mt(t)?e="many-months":e=365<=t/86400?"years":"default",e}function ut(t){return t<1}function dt(t){return t<60}function ft(t){return t/3600<24}function pt(t){return t/3600<96}function _t(t){return t/86400<60}function mt(t){return t/86400<365}function gt(t){if(t.time_series){var e=(t.processed.max_x-t.processed.min_x)/1e3,r=(t.processed.x_ticks[1]-t.processed.x_ticks[0])/1e3;t.processed.x_time_frame=lt(e),t.processed.x_tick_diff_time_frame=lt(r),t.processed.main_x_time_format=(a=t.utc_time,ut(n=r)?MG.time_format(a,"%M:%S.%L"):dt(n)?MG.time_format(a,"%M:%S"):ft(n)?MG.time_format(a,"%H:%M"):pt(n)||_t(n)?MG.time_format(a,"%b %d"):mt(n)?MG.time_format(a,"%b"):MG.time_format(a,"%Y"))}var a,n}function ht(r){if(r.xax_format)return r.xax_format;var t=h(r.processed.original_data||r.data)[0],a=t[r.processed.original_x_accessor||r.x_accessor];return void 0===a&&(a=t),function(t){return gt(r),x(a)?r.processed.main_x_time_format(new Date(t)):"number"==typeof a?(e=t%1!=0?E.format(",."+r.decimals+"f"):t<1e3?E.format(",.0f"):E.format(",.2s"),r.xax_units+e(t)):r.xax_units+t;var e}}function xt(t){var e={timeframe:t.processed.x_time_frame,tick_diff_timeframe:t.processed.x_tick_diff_time_frame};switch(e.timeframe){case"millis":case"seconds":e.secondary=E.timeDays,t.european_clock?e.yformat=MG.time_format(t.utc_time,"%b %d"):e.yformat=MG.time_format(t.utc_time,"%I %p");break;case"less-than-a-day":case"four-days":e.secondary=E.timeDays,e.yformat=MG.time_format(t.utc_time,"%b %d");break;case"many-days":case"many-months":e.secondary=E.timeYears,e.yformat=MG.time_format(t.utc_time,"%Y");break;default:e.secondary=E.timeYears,e.yformat=MG.time_format(t.utc_time,"%Y")}return e}function yt(t){var e=parseInt(t.width);t.full_width&&(e=f(t.target)),"categorical"===t.x_axis_type&&null===e&&(e=Dt(t,"x")),t.width=e}function vt(t){var e=parseInt(t.height);t.full_height&&(e=d(t.target,"height")),"categorical"===t.y_axis_type&&null===e&&(e=Dt(t,"y")),t.height=e}function bt(t,e){(!t.selectAll(".mg-main-line").empty()&&"line"!==e.chart_type||!t.selectAll(".mg-points").empty()&&"point"!==e.chart_type||!t.selectAll(".mg-histogram").empty()&&"histogram"!==e.chart_type||!t.selectAll(".mg-barplot").empty()&&"bar"!==e.chart_type)&&t.remove()}function wt(t,e){return O(e.target).empty()&&(t=E.select(e.target).append("svg").classed("linked",e.linked).attr("width",e.width).attr("height",e.height)),t}function kt(t,e){e.width!==Number(t.attr("width"))&&t.attr("width",e.width),e.height!==Number(t.attr("height"))&&t.attr("height",e.height)}function Mt(t,e){t.attr("viewBox","0 0 "+e.width+" "+e.height),(e.full_width||e.full_height)&&t.attr("preserveAspectRatio","xMinYMin meet")}function Gt(t,e){t.empty()&&console.warn('The specified target element "'+e.target+'" could not be found in the page. The chart will not be rendered.')}function At(t,e){var r,a,n,o="x"===e?t.width:t.height;!function(t,e){var r=t[e+"group_accessor"];if(t.categorical_groups=[],r){var a=t.data[0];t.categorical_groups=E.set(a.map(function(t){return t[r]})).values()}}(t,e),function(t,e){var r=t[e+"group_accessor"];if(t.total_bars=t.data[0].length,r){var a=(n=t.data[0],o=r,n.map(function(t){return t[o]}).reduce(function(t,e){return t[e]=t[e]+1||1,t},{}));a=E.max(Object.keys(a).map(function(t){return a[t]})),t.bars_per_group=a}else t.bars_per_group=t.data[0].length;var n,o}(t,e),function(t,e,r){var a=e+"group_height";if(r){var n="y"===e?(t.height-t.top-t.bottom-2*t.buffer)/(t.categorical_groups.length||1):(t.width-t.left-t.right-2*t.buffer)/(t.categorical_groups.length||1);t[a]=n}else{var o=(1+t[e+"_padding_percentage"])*t.bar_thickness;t[a]=t.bars_per_group*o+2*t[e+"_outer_padding_percentage"]*o}}(t,e,o),o&&(n=(r=t)[(a=e)+"group_height"]/(r.bars_per_group+r[a+"_outer_padding_percentage"]),r.bar_thickness=n-n*r[a+"_padding_percentage"])}function Dt(t,e){return t[e+"group_height"]*(t.categorical_groups.length||1)+("y"===e?t.top+t.bottom+2*t.buffer:t.left+t.right+2*t.buffer)+t.categorical_groups.length*t[e+"group_height"]*(t[e+"group_padding_percentage"]+t[e+"group_outer_padding_percentage"])}function Ot(t){var e;(e=t=t)||(e={}),e.processed||(e.processed={}),t=e=p(e,{target:null,title:null,description:null});var r=E.select(t.target);Gt(r,t);var a,n,o,i,s,c,l,u=r.selectAll("svg");return"categorical"===t.y_axis_type&&At(t,"y"),"categorical"===t.x_axis_type&&At(t,"x"),n=(a=t).chart_type,a.processed.xax_format||(a.xax_format?a.processed.xax_format=a.xax_format:"line"===n||"point"===n||"histogram"===n?a.processed.xax_format=ht(a):"bar"===n&&(a.processed.xax_format=ct(a))),i=h((o=t).processed.original_data||o.data)[0],o.time_series=x(i[o.processed.original_x_accessor||o.x_accessor]),yt(t),vt(t),bt(u,t),u=wt(u,t),c=t,(s=u).selectAll(".mg-clip-path").remove(),s.append("defs").attr("class","mg-clip-path").append("clipPath").attr("id","mg-plot-window-"+z(c.target)).append("svg:rect").attr("x",G(c)).attr("y",M(c)).attr("width",c.width-c.left-c.right-c.buffer).attr("height",c.height-c.top-c.bottom-c.buffer+1),kt(u,t),Mt(u,t),(l=u).classed("mg-missing",!1),l.selectAll(".mg-missing-text").remove(),l.selectAll(".mg-missing-pane").remove(),j(t),function(t,e){var r=0;if(t.selectAll(".mg-main-line").nodes().length>=e.data.length)if(0<e.custom_line_color_map.length){var a=T(function(t){for(var e=new Array(t),r=0;r<e.length;r++)e[r]=r+1;return e}(e.max_data_size),e.custom_line_color_map);for(r=0;r<a.length;r++)t.selectAll(".mg-main-line.mg-line"+a[r]+"-color").remove()}else{var n=e.data.length;for(r=t.selectAll(".mg-main-line").nodes()?t.selectAll(".mg-main-line").nodes().length:0;n<r;r--)t.selectAll(".mg-main-line.mg-line"+r+"-color").remove()}}(u,t),this}function zt(t){return t.label}function Ft(e){return function(t){return e.scales.X(t[e.x_accessor])>=Y(e)&&e.scales.X(t[e.x_accessor])<=P(e)}}function Ct(e){return function(t){return e.scales.X(t[e.x_accessor])}}function Tt(t){var e=t.scales.Y;return function(t){return e(t.value).toFixed(2)}}function Rt(t,e,r,a,n,o){var i;t&&(n(i=a.append("g").attr("class",e),r),o(i,r))}function Et(t,e){var r,a=(r=Ct(e),function(t){return r(t).toFixed(2)});t.selectAll(".mg-markers").data(e.markers.filter(Ft(e))).enter().append("line").attr("x1",a).attr("x2",a).attr("y1",e.top).attr("y2",v(e)).attr("class",function(t){return t.lineclass}).attr("stroke-dasharray","3,1")}function Yt(t,e){t.selectAll(".mg-markers").data(e.markers.filter(Ft(e))).enter().append("text").attr("class",function(t){return t.textclass||""}).classed("mg-marker-text",!0).attr("x",Ct(e)).attr("y","bottom"===e.x_axis_position?.95*M(e):w(e)+e.buffer).attr("text-anchor","middle").text(zt).each(function(t){t.click&&E.select(this).style("cursor","pointer").on("click",t.click),t.mouseover&&E.select(this).style("cursor","pointer").on("mouseover",t.mouseover),t.mouseout&&E.select(this).style("cursor","pointer").on("mouseout",t.mouseout)}),function(t,e){if(t&&1!=t.length)for(var r=0;r<t.length;r++)if(o(t[r],t)){var a=E.select(t[r]),n=+a.attr("y");n+8>=e.top&&(n=e.top-16),a.attr("y",n)}}(t.selectAll(".mg-marker-text").nodes(),e)}function $t(t,e){var r=Tt(e);t.selectAll(".mg-baselines").data(e.baselines).enter().append("line").attr("x1",Y(e)).attr("x2",P(e)).attr("y1",r).attr("y2",r)}function Pt(t,e){var r=Tt(e);t.selectAll(".mg-baselines").data(e.baselines).enter().append("text").attr("x",P(e)).attr("y",r).attr("dy",-3).attr("text-anchor","end").text(zt)}function St(t){var e,r,a,n,o,i=O(t.target);return(e=i).selectAll(".mg-markers").remove(),e.selectAll(".mg-baselines").remove(),a=i,Rt((r=t).markers,"mg-markers",r,a,Et,Yt),o=i,Rt((n=t).baselines,"mg-baselines",n,o,$t,Pt),this}function Lt(t){t.selectAll(".mg-active-datapoint-container").selectAll("*").remove()}function jt(t,e){t.select(".mg-active-datapoint").remove();var r,a="right"===e.mouseover_align?"end":"left"===e.mouseover_align?"start":"middle",n="right"===e.mouseover_align?P(e):"left"===e.mouseover_align?Y(e):(e.width-e.left-e.right)/2+e.left,o=t.select(".mg-active-datapoint-container").append("text").attr("class","mg-active-datapoint").attr("xml:space","preserve").attr("text-anchor",a),i=.75,s="bottom"===e.x_axis_position?M(e)*i:w(e)+3*e.buffer;e.markers&&t.selectAll(".mg-marker-text").each(function(){r?r!==E.select(this).attr("y")&&(i=.56):r=E.select(this).attr("y")});o.attr("transform","translate("+n+","+s+")")}function Xt(t,e){jt(e.svg,t);var r,a,o={row_number:0,rargs:e,mouseover_row:function(t){return o.row_number+=1,e=o.row_number,r=o.text_container,a=t,n=r.append("tspan").attr("x",0).attr("y",1.1*e+"em"),{rargs:a,text:function(t){return e=t,{bold:function(){return r.attr("font-weight","bold")},font_size:function(t){return r.attr("font-size",t)},x:function(t){return r.attr("x",t)},y:function(t){return r.attr("y",t)},elem:r=n.append("tspan").text(e)};var e,r}};var e,r,a,n},text_container:(r=e.svg,a=r.select(".mg-active-datapoint"),a.selectAll("*").remove(),a)};return o}Z.categorical=function(t,e){e.namespace;Q(t,e),function(d,t){var f,p,_,m,e=t.namespace,g=(d.scalefns[e+"f"],d.scalefns[e+"groupf"],d.scales[(e+"group").toUpperCase()]),h=d.scales[e.toUpperCase()],x=t.position,r=O(d.target),a=g.domain&&g.domain()?g.domain():[null];D(r,".mg-category-guides");var y=S(r,"mg-category-guides");a.forEach(function(e){h.domain().forEach(function(t){"left"!==x&&"right"!==x||(f=Y(d),p=P(d),_=h(t)+g(e)+h.bandwidth()/2,m=h(t)+g(e)+h.bandwidth()/2),"top"!==x&&"bottom"!==x||(f=h(t)+g(e)+h.bandwidth()/2*(null===e),p=h(t)+g(e)+h.bandwidth()/2*(null===e),_=v(d),m=b(d)),y.append("line").attr("x1",f).attr("x2",p).attr("y1",_).attr("y2",m).attr("stroke-dasharray","2,1")});var t,r,a,n,o,i,s,c,l=g(e)+h(h.domain()[0])+h.bandwidth()/2*(null===e||"top"!==x&&"bottom"!=x),u=g(e)+h(h.domain()[h.domain().length-1])+h.bandwidth()/2*(null===e||"top"!==x&&"bottom"!=x);"left"!==x&&"right"!==x||(t=Y(d),r=Y(d),a=l,n=u,o=P(d),i=P(d),s=l,c=u),"bottom"!==x&&"top"!==x||(t=l,r=u,a=v(d),n=v(d),o=l,i=u,s=b(d),c=b(d)),y.append("line").attr("x1",t).attr("x2",r).attr("y1",a).attr("y2",n).attr("stroke-dasharray","2,1"),y.append("line").attr("x1",o).attr("x2",i).attr("y1",s).attr("y2",c).attr("stroke-dasharray","2,1")})}(t,e)},Z.numerical=function(t,e){var r=e.namespace,a=r+"_axis",n="mg-"+r+"-axis",o=O(t.target);if(D(o,"."+n),!t[a])return this;var i,s,c,l,u,d,f,p,_,m,g,h,x,y,v,b=S(o,n);return function(t,e){var r=t[e+"_accessor"],a=t.scales[e.toUpperCase()].ticks(t[e+"ax_count"]),n=t.processed["max_"+e];function o(t){return 1e3===t?3:1e6===t?7:Math.log(t)/Math.LN10}"log"===t[e+"_scale_type"]&&(a=a.filter(function(t){return Math.abs(o(t))%1<1e-6||Math.abs(o(t))%1>1-1e-6}));var i=a.length,s=!0;t.data.forEach(function(t,e){t.forEach(function(t,e){if(t[r]%1!=0)return s=!1})}),s&&n<i&&"count"===t.format&&(a=a.filter(function(t){return t%1==0})),t.processed[e+"_ticks"]=a}(t,r),V(b,t,e),i=b,s=t,p=(c=e).namespace,_=c.position,m=s.scales[p.toUpperCase()],g=s.processed[p+"_ticks"],h="mg-"+p+"ax-ticks",x="mg-extended-"+p+"ax-ticks",y=s[p+"_extended_ticks"],v=s[p+"ax_tick_length"],"left"===_&&(l=G(s),u=y?A(s):G(s)-v,d=function(t){return m(t).toFixed(2)},f=function(t){return m(t).toFixed(2)}),"right"===_&&(l=A(s),u=y?G(s):A(s)+v,d=function(t){return m(t).toFixed(2)},f=function(t){return m(t).toFixed(2)}),"top"===_&&(l=function(t){return m(t).toFixed(2)},u=function(t){return m(t).toFixed(2)},d=M(s),f=y?w(s):M(s)-v),"bottom"===_&&(l=function(t){return m(t).toFixed(2)},u=function(t){return m(t).toFixed(2)},d=w(s),f=y?M(s):w(s)+v),i.selectAll("."+h).data(g).enter().append("line").classed(x,y).attr("x1",l).attr("x2",u).attr("y1",d).attr("y2",f),q(b,t,e),t[r+"_label"]&&e.label(o.select(".mg-"+r+"-axis"),t),t[r+"_rug"]&&W(t,e),t.show_bar_zero&&tt(t),this},MG.axis_factory=function(t){var e={type:"numerical"};return this.namespace=function(t){return e.namespace=t,this},this.rug=function(t){return e.rug=t,this},this.label=function(t){return e.label=t,this},this.type=function(t){return e.type=t,this},this.position=function(t){return e.position=t,this},this.zeroLine=function(t){return e.zeroLine=t,this},this.draw=function(){return Z[e.type](t,e),this},this},MG.y_rug=K,MG.y_axis=rt,MG.y_axis_categorical=function(t){return t.y_axis&&(at(t),t.show_bar_zero&&tt(t),t.ygroup_accessor&&(r=O((e=t).target),(a=e.scales.YGROUP.domain())[0],a[a.length-1],r.select(".mg-category-guides").selectAll("mg-group-lines").data(a).enter().append("line").attr("x1",Y(e)).attr("x2",Y(e)).attr("y1",function(t){return e.scales.YGROUP(t)}).attr("y2",function(t){return e.scales.YGROUP(t)+e.ygroup_height}).attr("stroke-width",1)),t.y_categorical_show_guides&&(o=O((n=t).target),i=[],n.data[0].forEach(function(t){-1===i.indexOf(t[n.y_accessor])&&o.select(".mg-category-guides").append("line").attr("x1",Y(n)).attr("x2",P(n)).attr("y1",n.scalefns.yf(t)+n.scalefns.ygroupf(t)).attr("y2",n.scalefns.yf(t)+n.scalefns.ygroupf(t)).attr("stroke-dasharray","2,1")}))),this;var e,r,a,n,o,i},MG.x_rug=nt,MG.x_axis=ot,MG.x_axis_categorical=function(t){var e=O(t.target),r=0;"bar"===t.chart_type&&(r=t.buffer+5),mg_add_categorical_scale(t,"X",t.categorical_variables.reverse(),t.left,P(t)-r),X(t,"xf","X","value"),D(e,".mg-x-axis");var a=S(e,"mg-x-axis");return t.x_axis&&(n=a,o=t,i=r,s=n.selectAll("text").data(o.categorical_variables).enter().append("text"),s.attr("x",function(t){return o.scales.X(t)+o.scales.X.bandwidth()/2+o.buffer*o.bar_outer_padding_percentage+i/2}).attr("y",v(o)).attr("dy",".35em").attr("text-anchor","middle").text(String),o.truncate_x_labels&&s.each(function(t,e){var r=o.scales.X.bandwidth();L(this,t,r)}),l(s,o.rotate_x_labels)),this;var n,o,i,s},MG.init=Ot,MG.markers=St;var Ut=function(n,o){return function(a){return["x","y"].every(function(t){return!(t in o)||(e=a[n[t+"_accessor"]],r=o[t],e>Math.min(r[0],r[1])&&e<Math.max(r[0],r[1]));var e,r})}},Nt=function(e,r){var t=e.processed.raw_data||e.data;"raw_data"in e.processed||(e.processed.raw_domain={x:e.scales.X.domain(),y:e.scales.Y.domain()},e.processed.raw_data=t),"point"===e.chart_type&&(a(t)?e.data=t.map(function(t){return t.filter(Ut(e,r))}):e.data=t.filter(Ut(e,r))),["x","y"].forEach(function(t){t in r?e.processed["zoom_"+t]=r[t]:delete e.processed["zoom_"+t]}),e.processed.subplot&&(r!==e.processed.raw_domain?MG.create_brushing_pattern(e.processed.subplot,It(e.processed.subplot,r)):MG.remove_brushing_pattern(e.processed.subplot)),new MG.charts[e.chart_type||defaults.chart_type].descriptor(e)},Bt=function(r,a){return["x","y"].reduce(function(t,e){return e in a&&(t[e]=a[e].map(function(t){return+r.scales[e.toUpperCase()].invert(t)}),"y"===e&&t[e].reverse()),t},{})},It=function(r,a){return["x","y"].reduce(function(t,e){return e in a&&(t[e]=a[e].map(function(t){return+r.scales[e.toUpperCase()](t)}),"y"===e&&t[e].reverse()),t},{})};MG.convert_range_to_domain=Bt,MG.zoom_to_data_domain=Nt,MG.zoom_to_data_range=function(t,e){var r=Bt(t,e);Nt(t,r)},MG.zoom_to_raw_range=function(t){"raw_domain"in t.processed&&(Nt(t,t.processed.raw_domain),delete t.processed.raw_domain,delete t.processed.raw_data)};var Ht=function(t){return E.select(t.target).select(".mg-extent").size()?E.select(t.target).select(".mg-extent"):E.select(t.target).select(".mg-rollover-rect, .mg-voronoi").insert("g","*").classed("mg-brush",!0).append("rect").classed("mg-extent",!0)},qt=function(t,e){var r=e.x[0],a=e.x[1]-e.x[0],n=e.y[0],o=e.y[1]-e.y[0];Ht(t).attr("x",r).attr("width",a).attr("y",n).attr("height",o).attr("opacity",1)},Vt=function(t){Ht(t).attr("width",0).attr("height",0).attr("opacity",0)};MG.add_brush_function=function(t){if("categorical"===t.x_axis_type||"categorical"===t.y_axis_type)return console.warn('The option "brush" does not support axis type "categorical" currently.');t.zoom_target||(t.zoom_target=t),t.zoom_target!==t&&(t.zoom_target.processed.subplot=t);var i,r,s,a,e,c,n,o,l,u,d=void 0;switch(t.brush){case"x":d={x:!0,y:!1};break;case"y":d={x:!1,y:!0};break;case"xy":d={x:!0,y:!0};break;default:d={x:!0,y:!0}}r=(i=t).zoom_target,s=d,a=E.select(i.target).select("svg"),e=a.select(".mg-rollover-rect, .mg-voronoi"),c=e.node(),o=n=!1,l=[],u=function(){var t=i.left,e=i.width-i.right-i.buffer,r=i.top,a=i.height-i.bottom-i.buffer,n=E.mouse(c),o={};return o.x=s.x?[Math.max(t,Math.min(l[0],n[0])),Math.min(e,Math.max(l[0],n[0]))]:[t,e],o.y=s.y?[Math.max(r,Math.min(l[1],n[1])),Math.min(a,Math.max(l[1],n[1]))]:[r,a],o},e.classed("mg-brush-container",!0),e.on("mousedown."+i.target,function(){n=!(o=!0),l=E.mouse(c),a.classed("mg-brushed",!1),a.classed("mg-brushing-in-progress",!0),Vt(i)}),E.select(document).on("mousemove."+i.target,function(){o&&(n=!0,e.classed("mg-brushing",!0),qt(i,u()))}),E.select(document).on("mouseup."+i.target,function(){if(o){o=!1,a.classed("mg-brushing-in-progress",!1);var t=u();if(n)if(n=!1,r===i)MG.zoom_to_data_range(r,t),a.select(".mg-rollover-rect, .mg-voronoi").classed("mg-brushed",!0);else{var e=MG.convert_range_to_domain(i,t);MG.zoom_to_data_domain(r,e)}else MG.zoom_to_raw_range(r);C(i.brushing_selection_changed)&&i.brushing_selection_changed(i,t)}})},MG.create_brushing_pattern=qt,MG.remove_brushing_pattern=Vt;var Wt=new function(){var t,i=[];function s(){i.forEach(function(t){var e=E.select(t).select("svg");if(!e.empty()&&(0<e.node().parentNode.offsetWidth||0<e.node().parentNode.offsetHeight)){var r=0!==e.attr("width")?e.attr("height")/e.attr("width"):0,a=f(t);e.attr("width",a),e.attr("height",r*a)}})}return"undefined"!=typeof MutationObserver?t=MutationObserver:"undefined"!=typeof WebKitMutationObserver&&(t=WebKitMutationObserver),{add_target:function(n){if(0===i.length&&window.addEventListener("resize",s,!0),-1===i.indexOf(n)&&(i.push(n),t)){var o=new t(function(t){var e,r,a=E.select(n).node();a&&!t.some(function(t){for(var e=0;e<t.removedNodes.length;e++)if(t.removedNodes[e]===a)return!0})||(o.disconnect(),e=n,-1!==(r=i.indexOf(e))&&i.splice(r,1),0===i.length&&window.removeEventListener("resize",s,!0))});o.observe(E.select(n).node().parentNode,{childList:!0})}}}};function Qt(t){var e;((e=t).full_width||e.full_height)&&Wt.add_target(e.target)}k()&&function(s){if("function"==typeof s().dropdown)return;var c='[data-toggle="dropdown"]',a=function(t){s(t).on("click.bs.dropdown",this.toggle)};function o(a){a&&3===a.which||(s(".dropdown-backdrop").remove(),s(c).each(function(){var t=s(this),e=l(t),r={relatedTarget:this};e.hasClass("open")&&(e.trigger(a=s.Event("hide.bs.dropdown",r)),a.isDefaultPrevented()||(t.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",r)))}))}function l(t){var e=t.attr("data-target");e||(e=(e=t.attr("href"))&&/#[A-Za-z]/.test(e)&&e.replace(/.*(?=#[^\s]*$)/,""));var r=e&&s(e);return r&&r.length?r:t.parent()}a.VERSION="3.3.1",a.prototype.toggle=function(t){var e=s(this);if(!e.is(".disabled, :disabled")){var r=l(e),a=r.hasClass("open");if(o(),!a){"ontouchstart"in document.documentElement&&!r.closest(".navbar-nav").length&&s('<div class="dropdown-backdrop"/>').insertAfter(s(this)).on("click",o);var n={relatedTarget:this};if(r.trigger(t=s.Event("show.bs.dropdown",n)),t.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),r.toggleClass("open").trigger("shown.bs.dropdown",n)}return!1}},a.prototype.keydown=function(t){if(/(38|40|27|32)/.test(t.which)&&!/input|textarea/i.test(t.target.tagName)){var e=s(this);if(t.preventDefault(),t.stopPropagation(),!e.is(".disabled, :disabled")){var r=l(e),a=r.hasClass("open");if(!a&&27!=t.which||a&&27==t.which)return 27==t.which&&r.find(c).trigger("focus"),e.trigger("click");var n=" li:not(.divider):visible a",o=r.find('[role="menu"]'+n+', [role="listbox"]'+n);if(o.length){var i=o.index(t.target);38==t.which&&0<i&&i--,40==t.which&&i<o.length-1&&i++,~i||(i=0),o.eq(i).trigger("focus")}}}};var t=s.fn.dropdown;s.fn.dropdown=function(r){return this.each(function(){var t=s(this),e=t.data("bs.dropdown");e||t.data("bs.dropdown",e=new a(this)),"string"==typeof r&&e[r].call(t)})},s.fn.dropdown.Constructor=a,s.fn.dropdown.noConflict=function(){return s.fn.dropdown=t,this},s(document).on("click.bs.dropdown.data-api",o).on("click.bs.dropdown.data-api",".dropdown form",function(t){t.stopPropagation()}).on("click.bs.dropdown.data-api",c,a.prototype.toggle).on("keydown.bs.dropdown.data-api",c,a.prototype.keydown).on("keydown.bs.dropdown.data-api",'[role="menu"]',a.prototype.keydown).on("keydown.bs.dropdown.data-api",'[role="listbox"]',a.prototype.keydown)}(jQuery),MG.button_layout=function(t){return this.target=t,this.feature_set={},this.public_name={},this.sorters={},this.manual=[],this.manual_map={},this.manual_callback={},this._strip_punctuation=function(t){return t.replace(/[^a-zA-Z0-9 _]+/g,"").replace(/ +?/g,"")},this.data=function(t){return this._data=t,this},this.manual_button=function(t,e,r){return this.feature_set[t]=e,this.manual_map[this._strip_punctuation(t)]=t,this.manual_callback[t]=r,this},this.button=function(t){return 1<arguments.length&&(this.public_name[t]=arguments[1]),2<arguments.length&&(this.sorters[t]=arguments[2]),this.feature_set[t]=[],this},this.callback=function(t){return this._callback=t,this},this.display=function(){var e,t,r,a,n=this._callback,o=this.manual_callback,i=this.manual_map;r=Object.keys(this.feature_set);var s,c=function(t){return e[t]};for(s=0;s<this._data.length;s++){e=this._data[s],t=r.map(c);for(var l=0;l<r.length;l++)a=r[l],-1===this.feature_set[a].indexOf(t[l])&&this.feature_set[a].push(t[l])}for(a in this.feature_set)this.sorters.hasOwnProperty(a)&&this.feature_set[a].sort(this.sorters[a]);$(this.target).empty(),$(this.target).append("<div class='col-lg-12 segments text-center'></div>");var u=function(){var t,e=$(this).data("key"),r=$(this).data("feature");return $("."+r+"-btns button.btn span.title").html(e),i.hasOwnProperty(r)?(t=i[r],o[t](e)):n(r,e),!1};for(var d in this.feature_set){for(r=this.feature_set[d],$(this.target+" div.segments").append('<div class="btn-group '+this._strip_punctuation(d)+'-btns text-left"><button type="button" class="btn btn-default btn-lg dropdown-toggle" data-toggle="dropdown"><span class=\'which-button\'>'+(this.public_name.hasOwnProperty(d)?this.public_name[d]:d)+"</span><span class='title'>"+(this.manual_callback.hasOwnProperty(d)?this.feature_set[d][0]:"all")+'</span><span class="caret"></span></button><ul class="dropdown-menu" role="menu">'+(this.manual_callback.hasOwnProperty(d)?"":'<li><a href="#" data-feature="'+d+'" data-key="all">All</a></li>')+(this.manual_callback.hasOwnProperty(d)?"":'<li class="divider"></li>')+"</ul></div>"),s=0;s<r.length;s++)"all"!==r[s]&&void 0!==r[s]&&$(this.target+" div."+this._strip_punctuation(d)+"-btns ul.dropdown-menu").append('<li><a href="#" data-feature="'+this._strip_punctuation(d)+'" data-key="'+r[s]+'">'+r[s]+"</a></li>");$("."+this._strip_punctuation(d)+"-btns .dropdown-menu li a").on("click",u)}return this},this};var Zt=function(t,e,r){var a=e.line_id,n=r.color,o=r.colors;t.classed("mg-hover-line-color",null===n).classed("mg-hover-line"+a+"-color",null===o).attr("fill",null===o?"":o[a-1])},Kt=function(r,t,e){t.existing_band=e.selectAll(".mg-confidence-band").nodes(),r.show_confidence_band&&(t.confidence_area=E.area().defined(t.line.defined()).x(r.scalefns.xf).y0(function(t){var e=r.show_confidence_band[0];return null!=t[e]?r.scales.Y(t[e]):r.scales.Y(t[r.y_accessor])}).y1(function(t){var e=r.show_confidence_band[1];return null!=t[e]?r.scales.Y(t[e]):r.scales.Y(t[r.y_accessor])}).curve(r.interpolate))},Jt=function(t,e){var r=t.scalefns,a=t.scales,n=t.interpolate,o=t.flip_area_under_y_value,i=Number.isFinite(o)?a.Y(o):a.Y.range()[0];e.area=E.area().defined(e.line.defined()).x(r.xf).y0(function(){return i}).y1(r.yf).curve(n)},te=function(t,e){var r=t.y_accessor,a=t.scalefns,n=t.scales,o=t.interpolate;e.flat_line=E.line().defined(function(t){return(void 0===t._missing||!0!==t._missing)&&null!==t[r]}).x(a.xf).y(function(){return n.Y(e.data_median)}).curve(o)},ee=function(t,e){var r=t.scalefns,a=t.interpolate,n=t.missing_is_zero,o=t.y_accessor;e.line=E.line().x(r.xf).y(r.yf).curve(a),n||(e.line=e.line.defined(function(t){return(void 0===t._missing||!0!==t._missing)&&null!==t[o]}))},re=function(t,e,r,a){var n=t.show_confidence_band,o=t.transition_on_update,i=t.data,s=t.target;if(n){r.select(".mg-confidence-band-"+a).empty()&&r.append("path").attr("class","mg-confidence-band mg-confidence-band-"+a),r.select(".mg-confidence-band-"+a).transition().duration(function(){return o?1e3:0}).attr("d",e.confidence_area(i[a-1])).attr("clip-path","url(#mg-plot-window-"+z(s)+")")}},ae=function(t,e,r,a,n){var o=t.data,i=t.target,s=t.colors,c=r.selectAll(".mg-main-area.mg-area"+n);e.display_area?c.empty()?r.append("path").classed("mg-main-area",!0).classed("mg-area"+n,!0).classed("mg-area-color",null===s).classed("mg-area"+n+"-color",null===s).attr("d",e.area(o[a])).attr("fill",null===s?"":s[n-1]).attr("clip-path","url(#mg-plot-window-"+z(i)+")"):(r.node().appendChild(c.node()),c.transition().duration(e.update_transition_duration).attr("d",e.area(o[a])).attr("clip-path","url(#mg-plot-window-"+z(i)+")")):c.empty()||c.remove()},ne=function(t,e){t.classed("mg-line-color",!0).classed("mg-line"+e+"-color",!0)},oe=function(t,e,r,a,n,o){if(a.empty()){var i=r.append("path").attr("class","mg-main-line mg-line"+o);g=i,h=n,x=o,(y=t.colors)&&y.constructor===Array?(g.attr("stroke",y[h]),y.length<h+1&&ne(g,x)):ne(g,x),l=e,u=i,d=n,f=(c=t).animate_on_load,p=c.data,_=c.y_accessor,m=c.target,f?(l.data_median=E.median(p[d],function(t){return t[_]}),u.attr("d",l.flat_line(p[d])).transition().duration(1e3).attr("d",l.line(p[d])).attr("clip-path","url(#mg-plot-window-"+z(m)+")")):u.attr("d",l.line(p[d])).attr("clip-path","url(#mg-plot-window-"+z(m)+")")}else{r.node().appendChild(a.node());var s=a.transition().duration(e.update_transition_duration);e.display_area||!t.transition_on_update||t.missing_is_hidden?s.attr("d",e.line(t.data[n])):s.attrTween("d",je(e.line(t.data[n]),4))}var c,l,u,d,f,p,_,m,g,h,x,y},ie=function(t,e,r,a){var n,o=void 0;if(t.legend)if(u(t.legend)?o=t.legend[r]:(n=t.legend,"[object Function]"===Object.prototype.toString.call(n)&&(o=t.legend(t.data[r]))),t.legend_target)t.colors&&t.colors.constructor===Array?e.legend_text="<span style='color:"+t.colors[r]+"'>&mdash; "+o+"&nbsp; </span>"+e.legend_text:e.legend_text="<span class='mg-line"+a+"-legend-color'>&mdash; "+o+"&nbsp; </span>"+e.legend_text;else{var i=void 0,s=void 0,c=void 0;"left"===t.y_axis_position?(i=t.data[r][t.data[r].length-1],s="start",c=t.buffer):(i=t.data[r][0],s="end",c=-t.buffer);var l=e.legend_group.append("svg:text").attr("x",t.scalefns.xf(i)).attr("dx",c).attr("y",t.scalefns.yf(i)).attr("dy",".35em").attr("font-size",10).attr("text-anchor",s).attr("font-weight","300").text(o);t.colors&&t.colors.constructor===Array?t.colors.length<r+1?l.classed("mg-line"+a+"-legend-color",!0):l.attr("fill",t.colors[r]):l.classed("mg-line-legend-color",!0).classed("mg-line"+a+"-legend-color",!0),function(t,e){if(t&&1!=t.length){var r,a,n;t.sort(function(t,e){return E.select(e).attr("y")-E.select(t).attr("y")}),t.reverse();for(var o=0;o<t.length;o++){a=E.select(t[o]).text();for(var i=0;i<t.length;i++)if(n=E.select(t[i]).text(),!1!==(l=t[o],u=t[i],void 0,d=l.getBoundingClientRect(),f=u.getBoundingClientRect(),r=d.top<=f.bottom&&d.top>=f.top&&f.bottom-d.top)&&a!==n){var s=E.select(t[o]),c=+s.attr("y");c+=r,s.attr("y",c)}}}var l,u,d,f}(e.legend_group.selectAll(".mg-line-legend text").nodes())}},se=function(e,t,r,a,n,o){var i,s,c,l=E.voronoi().x(function(t){return e.scales.X(t[e.x_accessor]).toFixed(2)}).y(function(t){return e.scales.Y(t[e.y_accessor]).toFixed(2)}).extent([[e.buffer,e.buffer+(e.title?e.title_y_position:0)],[e.width-e.buffer,e.height-e.buffer]]);S(t,"mg-voronoi").selectAll("path").data(l.polygons((s=e,c=s.data,E.merge(c)))).enter().append("path").filter(function(t){return void 0!==t&&0<t.length}).attr("d",function(t){return null==t?null:"M"+t.join("L")+"Z"}).datum(function(t){return null==t?null:t.data}).attr("class",(i=e,function(t){var e=void 0;if(i.linked){var r=t[i.x_accessor],a=MG.time_format(i.utc_time,i.linked_format);return e="roll_"+("number"==typeof r?t.line_id-1:a(r))+" mg-line"+t.line_id,null===i.color&&(e+=" mg-line"+t.line_id+"-color"),e}return e="mg-line"+t.line_id,null===i.color&&(e+=" mg-line"+t.line_id+"-color"),e})).on("click",o).on("mouseover",r).on("mouseout",a).on("mousemove",n),le(e,t)},ce=function(a,t,e,r,n,o){var i,s,c,l,u,d=(s=(i=a).x_accessor,c=i.data,l=i.x_sort,(u=E.nest().key(function(t){return t[s]}).entries(E.merge(c))).forEach(function(t){var e=t.values[0];t.key=e[s]}),l?u.sort(function(t,e){return new Date(t.key)-new Date(e.key)}):u),f=d.map(function(t){var e=t.key;return a.scales.X(e)});t.append("g").attr("class","mg-rollover-rect").selectAll(".mg-rollover-rects").data(d).enter().append("rect").attr("x",function(t,e){return 1===f.length?Y(a):0===e?f[e].toFixed(2):((f[e-1]+f[e])/2).toFixed(2)}).attr("y",a.top).attr("width",function(t,e){return 1===f.length?P(a):0===e?((f[e+1]-f[e])/2).toFixed(2):e===f.length-1?((f[e]-f[e-1])/2).toFixed(2):((f[e+1]-f[e-1])/2).toFixed(2)}).attr("class",function(t){var e=t.values,r=e.map(function(t){var e=t.line_id,r=ue(e);return null===a.colors&&(r+=" "+de(e)),r}).join(" ");return a.linked&&0<e.length&&(r+=" "+fe(pe(e[0],a))),r}).attr("height",a.height-a.bottom-a.top-a.buffer).attr("opacity",0).on("click",o).on("mouseover",e).on("mouseout",r).on("mousemove",n),_e(a,t)},le=function(t,e){for(var r=t.data,a=t.custom_line_color_map,n=0;n<r.length;n++){var o=n+1;0<a.length&&void 0!==a[n]&&(o=a[n]),1!==r[n].length||e.selectAll(".mg-voronoi .mg-line"+o).empty()||(e.selectAll(".mg-voronoi .mg-line"+o).on("mouseover")(r[n][0],0),e.selectAll(".mg-voronoi .mg-line"+o).on("mouseout")(r[n][0],0))}},ue=function(t){return"mg-line"+t},de=function(t){return"mg-line"+t+"-color"},fe=function(t){return"roll_"+t},pe=function(t,e){var r=e.x_accessor,a=e.utc_time,n=e.linked_format,o=t[r],i=MG.time_format(a,n);return"number"==typeof o?o.toString().replace(".","_"):i(o)},_e=function(t,e){var r=t.data,a=e.selectAll(".mg-rollover-rect rect"),n=a.nodes()[0][0]||a.nodes()[0];0<r.filter(function(t){return 1===t.length}).length&&a.on("mouseover")(n.__data__,0)},me=function(t){var e=t.data;return 1===e.length&&1===e[0].length},ge=function(t,e){var r=t.existing_band;r[0]&&r[0].length>e.selectAll(".mg-main-line").node().length&&e.selectAll(".mg-confidence-band").remove()},he=function(t){var e,r,a,n,o,i,s,c={},l=O(t.target);D(l,".mg-line-legend"),e=c,r=l,t.legend&&(e.legend_group=S(r,"mg-line-legend")),c.data_median=0,c.update_transition_duration=t.transition_on_update?1e3:0,c.display_area=t.area&&!t.use_data_y_min&&t.data.length<=1&&!1===t.aggregate_rollover||Array.isArray(t.area)&&0<t.area.length,c.legend_text="",o=l,ee(a=t,n=c),Jt(a,n),te(a,n),Kt(a,n,o),c.existing_band=l.selectAll(".mg-confidence-band").nodes(),!1!==MG.call_hook("line.before_all_series",[t])&&function(e,t,r){ge(t,r),e.active_point_on_lines&&r.selectAll("circle.mg-shown-active-point").remove();for(var a=e.data.length-1;0<=a;a--){var n=e.data[a];MG.call_hook("line.before_each_series",[n,e]);var o=a+1;if(0<e.custom_line_color_map.length&&(o=e.custom_line_color_map[a]),e.data[a].line_id=o,e.active_point_on_lines&&r.selectAll("circle-"+o).data(e.data[a]).enter().filter(function(t){return t[e.active_point_accessor]}).append("circle").attr("class","mg-area"+o+"-color mg-shown-active-point").attr("cx",e.scalefns.xf).attr("cy",e.scalefns.yf).attr("r",function(){return e.active_point_size}),0!==n.length){var i=r.select("path.mg-main-line.mg-line"+o);re(e,t,r,o),Array.isArray(e.area)?e.area[o-1]&&ae(e,t,r,a,o):ae(e,t,r,a,o),oe(e,t,r,i,a,o),ie(e,t,a,o),MG.call_hook("line.after_each_series",[n,i,e])}}}(t,c,l),i=t.legend_target,s=c.legend_text,i&&E.select(i).html(s)},xe=function(t,e){var r,a,n,o,i,s,c,l,u,d,f,p,_=O(t.target);t.showActivePoint&&0===_.selectAll(".mg-active-datapoint-container").nodes().length&&S(_,"mg-active-datapoint-container"),D(r=_,".mg-rollover-rect"),D(r,".mg-voronoi"),D(r,".mg-active-datapoint"),D(r,".mg-line-rollover-circle"),n=_,o=(a=t).data,i=a.colors,s=n.selectAll(".mg-line-rollover-circle").data(o).enter().append("circle").attr("cx",0).attr("cy",0).attr("r",0),i&&i.constructor===Array?s.attr("class",function(t){return"mg-line"+t.line_id}).attr("fill",function(t,e){return i[e]}).attr("stroke",function(t,e){return i[e]}):s.attr("class",function(t,e){var r=t.line_id;return["mg-line"+r,"mg-line"+r+"-color","mg-area"+r+"-color"].join(" ")}),s.classed("mg-line-rollover-circle",!0),function(t){for(var e=t.data,r=t.custom_line_color_map,a=0;a<e.length;a++)for(var n=0;n<e[a].length;n++)e[a][n].index=a+1,0<r.length?e[a][n].line_id=r[a]:e[a][n].line_id=a+1}(t),f=(d=t).data,p=d.aggregate_rollover,1<f.length&&!p?se(t,_,e.rolloverOn(t),e.rolloverOff(t),e.rolloverMove(t),e.rolloverClick(t)):(l=(c=t).data,u=c.aggregate_rollover,1<l.length&&u?ce(t,_,e.rolloverOn(t),e.rolloverOff(t),e.rolloverMove(t),e.rolloverClick(t)):function(a,t,e,r,n,o){var i=1;0<a.custom_line_color_map.length&&(i=a.custom_line_color_map[0]);var s,c,l=t.append("g").attr("class","mg-rollover-rect"),u=a.data[0].map(a.scalefns.xf);l.selectAll(".mg-rollover-rects").data(a.data[0]).enter().append("rect").attr("class",function(t,e){var r=de(i)+" "+ue(t.line_id);return a.linked&&(r+=r+" "+fe(pe(t,a))),r}).attr("x",function(t,e){return 1===u.length?Y(a):0===e?u[e].toFixed(2):((u[e-1]+u[e])/2).toFixed(2)}).attr("y",function(t,e){return 1<a.data.length?a.scalefns.yf(t)-6:a.top}).attr("width",function(t,e){return 1===u.length?P(a):0===e?((u[e+1]-u[e])/2).toFixed(2):e===u.length-1?((u[e]-u[e-1])/2).toFixed(2):((u[e+1]-u[e-1])/2).toFixed(2)}).attr("height",function(t,e){return 1<a.data.length?12:a.height-a.bottom-a.top-a.buffer}).attr("opacity",0).on("click",o).on("mouseover",e).on("mouseout",r).on("mousemove",n),me(a)&&(s=t,c=a.data,s.select(".mg-rollover-rect rect").on("mouseover")(c[0][0],0))}(t,_,e.rolloverOn(t),e.rolloverOff(t),e.rolloverMove(t),e.rolloverClick(t)))},ye=function(t,e,r){var a=t.scales,n=t.x_accessor,o=t.y_accessor,i=t.point_size;e.select("circle.mg-line-rollover-circle.mg-line"+r.line_id).attr("cx",a.X(r[n]).toFixed(2)).attr("cy",a.Y(r[o]).toFixed(2)).attr("r",i).style("opacity",1)},ve=function(t,e,r){var a=t.scales,n=t.x_accessor,o=t.y_accessor,i=t.point_size;e.selectAll("circle.mg-line-rollover-circle.mg-line"+r.line_id).classed("mg-line-rollover-circle",!0).attr("cx",function(){return a.X(r[n]).toFixed(2)}).attr("cy",function(){return a.Y(r[o]).toFixed(2)}).attr("r",i).style("opacity",1)};MG.register("line",function(t){this.init=function(e){if(!(this.args=e).data||0===e.data.length)return e.internal_error="No data was supplied",t=e,console.error("INTERNAL ERROR : ",t.target," : ",t.internal_error),this;var t;e.internal_error=void 0,Me(e),Ae(e),MG.call_hook("line.before_destroy",this),Ot(e),new MG.scale_factory(e).namespace("x").numericalDomainFromData().numericalRange("bottom");var r=(e.baselines||[]).map(function(t){return t[e.y_accessor]});return new MG.scale_factory(e).namespace("y").zeroBottom(!0).inflateDomain(!0).numericalDomainFromData(r).numericalRange("left"),e.x_axis&&new MG.axis_factory(e).namespace("x").type("numerical").position(e.x_axis_position).rug(nt(e)).label(st).draw(),e.y_axis&&new MG.axis_factory(e).namespace("y").type("numerical").position(e.y_axis_position).rug(K(e)).label(et).draw(),this.markers(),this.mainPlot(),this.rollover(),this.windowListeners(),e.brush&&MG.add_brush_function(e),MG.call_hook("line.after_init",this),this},this.mainPlot=function(){return he(t),this},this.markers=function(){return St(t),this},this.rollover=function(){return xe(t,this),MG.call_hook("line.after_rollover",t),this},this.rolloverClick=function(r){return function(t,e){r.click&&r.click(t,e)}},this.rolloverOn=function(n){var o=O(n.target);return function(r,t){if(function(a,n,t){if(a.aggregate_rollover&&1<a.data.length)n.selectAll("circle.mg-line-rollover-circle").style("opacity",0),t.values.forEach(function(t,e,r){a.missing_is_hidden&&r[e]._missing||i(t,a)&&ye(a,n,t)});else{if(a.missing_is_hidden&&t._missing||null===t[a.y_accessor])return;i(t,a)&&ve(a,n,t)}}(n,o,r),function(t,e,r){if(t.linked&&!MG.globals.link&&(MG.globals.link=!0,!t.aggregate_rollover||void 0!==e[t.y_accessor]||e.values&&0<e.values.length)){var a=e.values?e.values[0]:e,n=pe(a,t);E.selectAll("."+ue(a.line_id)+"."+fe(n)).each(function(t){E.select(this).on("mouseover")(t,r)})}}(n,r,t),o.selectAll("text").filter(function(t,e){return r===t}).attr("opacity",.3),n.show_rollover_text&&!(n.missing_is_hidden&&r._missing||null===r[n.y_accessor])){var e=Xt(n,{svg:o}),a=e.mouseover_row();n.aggregate_rollover&&a.text((n.aggregate_rollover&&1<n.data.length?function(t,e){return Pe(t,e,t.x_mouseover,"key",t.time_series)}:Se)(n,r)),(n.aggregate_rollover&&1<n.data.length?r.values:[r]).forEach(function(t){n.aggregate_rollover&&(a=e.mouseover_row()),n.legend&&Zt(a.text(n.legend[t.index-1]+" ").bold(),t,n),Zt(a.text("— ").elem,t,n),n.aggregate_rollover||a.text(Se(n,t)),a.text(Le(n,t,n.time_series))})}n.mouseover&&n.mouseover(r,t)}},this.rolloverOff=function(c){var l=O(c.target);return function(t,e){var r,a,n,o,i,s;!function(t,e,a){var r=t.linked,n=t.utc_time,o=t.linked_format,i=t.x_accessor;if(r&&MG.globals.link){MG.globals.link=!1;var s=MG.time_format(n,o);(e.values?e.values:[e]).forEach(function(t){var e=t[i],r="number"==typeof e?a:s(e);E.selectAll(".roll_"+r).each(function(t){E.select(this).on("mouseout")(t)})})}}(c,t,e),c.aggregate_rollover?l.selectAll("circle.mg-line-rollover-circle").filter(function(t){return 1<t.length}).style("opacity",0):(a=l,n=t,o=(r=c).custom_line_color_map,i=r.data,s=n.line_id,a.selectAll("circle.mg-line-rollover-circle.mg-line"+s).style("opacity",function(){var t=s-1;return 0<o.length&&void 0!==o.indexOf(s)&&(t=o.indexOf(s)),1===i[t].length?1:0})),1<c.data[0].length&&Lt(l),c.mouseout&&c.mouseout(t,e)}},this.rolloverMove=function(r){return function(t,e){r.mousemove&&r.mousemove(t,e)}},this.windowListeners=function(){return Qt(this.args),this},this.init(t)});function be(t,e,r){var a,n,o,i,s,c=Xt(t,{svg:e}).mouseover_row();if(null!==t.color_accessor&&"category"===t.color_type){var l=r[t.color_accessor];c.text(l+" ").bold().attr("fill",t.scalefns.colorf(r))}a=t,n=c.text("● ").elem,o=r,i=a.color_accessor,s=a.scalefns,null!==i?(n.attr("fill",s.colorf(o)),n.attr("stroke",s.colorf(o))):n.classed("mg-points-mono",!0),c.text(Se(t,r)),c.text(Le(t,r,t.time_series))}MG.register("histogram",function(r){var a=this;this.init=function(e){Me(a.args=e),De(e),Ot(e),new MG.scale_factory(e).namespace("x").numericalDomainFromData().numericalRange("bottom");var t=(e.baselines||[]).map(function(t){return t[e.y_accessor]});return new MG.scale_factory(e).namespace("y").zeroBottom(!0).inflateDomain(!0).numericalDomainFromData(t).numericalRange("left"),ot(e),rt(e),a.mainPlot(),a.markers(),a.rollover(),a.windowListeners(),a},this.mainPlot=function(){var t=O(r.target);return t.selectAll(".mg-histogram").remove(),t.append("g").attr("class","mg-histogram").selectAll(".mg-bar").data(r.data[0]).enter().append("g").attr("class","mg-bar").attr("transform",function(t){return"translate("+r.scales.X(t[r.x_accessor]).toFixed(2)+","+r.scales.Y(t[r.y_accessor]).toFixed(2)+")"}).append("rect").attr("x",1).attr("width",function(t,e){return 1===r.data[0].length?(r.scalefns.xf(r.data[0][0])-r.bar_margin).toFixed(0):e!==r.data[0].length-1?(r.scalefns.xf(r.data[0][e+1])-r.scalefns.xf(t)).toFixed(0):(r.scalefns.xf(r.data[0][1])-r.scalefns.xf(r.data[0][0])).toFixed(0)}).attr("height",function(t){return 0===t[r.y_accessor]?0:(r.height-r.bottom-r.buffer-r.scales.Y(t[r.y_accessor])).toFixed(2)}),a},this.markers=function(){return St(r),a},this.rollover=function(){var t=O(r.target);return 0===t.selectAll(".mg-active-datapoint-container").nodes().length&&S(t,"mg-active-datapoint-container"),t.selectAll(".mg-rollover-rect").remove(),t.selectAll(".mg-active-datapoint").remove(),t.append("g").attr("class","mg-rollover-rect").selectAll(".mg-bar").data(r.data[0]).enter().append("g").attr("class",function(t,e){return r.linked?"mg-rollover-rects roll_"+e:"mg-rollover-rects"}).attr("transform",function(t){return"translate("+r.scales.X(t[r.x_accessor])+",0)"}).append("rect").attr("x",1).attr("y",r.buffer+(r.title?r.title_y_position:0)).attr("width",function(t,e){return 1===r.data[0].length?(r.scalefns.xf(r.data[0][0])-r.bar_margin).toFixed(0):e!==r.data[0].length-1?(r.scalefns.xf(r.data[0][e+1])-r.scalefns.xf(t)).toFixed(0):(r.scalefns.xf(r.data[0][1])-r.scalefns.xf(r.data[0][0])).toFixed(0)}).attr("height",function(t){return r.height}).attr("opacity",0).on("mouseover",a.rolloverOn(r)).on("mouseout",a.rolloverOff(r)).on("mousemove",a.rolloverMove(r)),a},this.rolloverOn=function(e){var n=O(e.target);return function(r,a){if(n.selectAll("text").filter(function(t,e){return r===t}).attr("opacity",.3),e.processed.xax_format||MG.time_format(e.utc_time,"%b %e, %Y"),Ee(e),n.selectAll(".mg-bar rect").filter(function(t,e){return e===a}).classed("active",!0),e.linked&&!MG.globals.link&&(MG.globals.link=!0,E.selectAll(".mg-rollover-rects.roll_"+a+" rect").each(function(t){E.select(this).on("mouseover")(t,a)})),e.show_rollover_text){var t=Xt(e,{svg:n}).mouseover_row();t.text("▟ ").elem.classed("hist-symbol",!0),t.text(Se(e,r)),t.text(Le(e,r,e.time_series))}e.mouseover&&(jt(n,e),e.mouseover(r,a))}},this.rolloverOff=function(r){var a=O(r.target);return function(t,e){r.linked&&MG.globals.link&&(MG.globals.link=!1,E.selectAll(".mg-rollover-rects.roll_"+e+" rect").each(function(t){E.select(this).on("mouseout")(t,e)})),a.selectAll(".mg-bar rect").classed("active",!1),Lt(a),r.mouseout&&r.mouseout(t,e)}},this.rolloverMove=function(r){return function(t,e){r.mousemove&&r.mousemove(t,e)}},this.windowListeners=function(){return Qt(a.args),a},this.init(r)},{binned:!1,bins:null,processed_x_accessor:"x",processed_y_accessor:"y",processed_dx_accessor:"dx",bar_margin:1});var we=function(t,e){var r=e.x_accessor,a=e.y_accessor;return t.filter(function(t){return(null===e.min_x||t[r]>=e.min_x)&&(null===e.max_x||t[r]<=e.max_x)&&(null===e.min_y||t[a]>=e.min_y)&&(null===e.max_y||t[a]<=e.max_y)})};MG.register("point",function(a){var o=this;this.init=function(e){if((this.args=e).x_axis_type=s(e,"x"),e.y_axis_type=s(e,"y"),Me(e),Oe(e),Ot(e),"categorical"===e.x_axis_type?(MG.scale_factory(e).namespace("x").categoricalDomainFromData().categoricalRangeBands([0,e.xgroup_height],null===e.xgroup_accessor),e.xgroup_accessor?new MG.scale_factory(e).namespace("xgroup").categoricalDomainFromData().categoricalRangeBands("bottom"):(e.scales.XGROUP=function(){return Y(e)},e.scalefns.xgroupf=function(){return Y(e)}),e.scalefns.xoutf=function(t){return e.scalefns.xf(t)+e.scalefns.xgroupf(t)}):(MG.scale_factory(e).namespace("x").inflateDomain(!0).zeroBottom("categorical"===e.y_axis_type).numericalDomainFromData((e.baselines||[]).map(function(t){return t[e.x_accessor]})).numericalRange("bottom"),e.scalefns.xoutf=e.scalefns.xf),"categorical"===e.y_axis_type)MG.scale_factory(e).namespace("y").zeroBottom(!0).categoricalDomainFromData().categoricalRangeBands([0,e.ygroup_height],!0),e.ygroup_accessor?new MG.scale_factory(e).namespace("ygroup").categoricalDomainFromData().categoricalRangeBands("left"):(e.scales.YGROUP=function(){return b(e)},e.scalefns.ygroupf=function(){return b(e)}),e.scalefns.youtf=function(t){return e.scalefns.yf(t)+e.scalefns.ygroupf(t)};else{var t=(e.baselines||[]).map(function(t){return t[e.y_accessor]});MG.scale_factory(e).namespace("y").inflateDomain(!0).zeroBottom("categorical"===e.x_axis_type).numericalDomainFromData(t).numericalRange("left"),e.scalefns.youtf=function(t){return e.scalefns.yf(t)}}if(null!==e.color_accessor){var r=MG.scale_factory(e).namespace("color");"number"===e.color_type?r.numericalDomainFromData(B(e)).numericalRange(I(e)).clamp(!0):e.color_domain?r.categoricalDomain(e.color_domain).categoricalRange(e.color_range):r.categoricalDomainFromData().categoricalColorRange()}return e.size_accessor&&new MG.scale_factory(e).namespace("size").numericalDomainFromData().numericalRange(it(e)).clamp(!0),new MG.axis_factory(e).namespace("x").type(e.x_axis_type).zeroLine("categorical"===e.y_axis_type).position(e.x_axis_position).rug(nt(e)).label(st).draw(),new MG.axis_factory(e).namespace("y").type(e.y_axis_type).zeroLine("categorical"===e.x_axis_type).position(e.y_axis_position).rug(K(e)).label(et).draw(),this.mainPlot(),this.markers(),this.rollover(),this.windowListeners(),e.brush&&MG.add_brush_function(e),this},this.markers=function(){return St(a),a.least_squares&&ze(a),this},this.mainPlot=function(){var t=O(a.target),e=we(a.data[0],a);t.selectAll(".mg-points").remove();var r=t.append("g").classed("mg-points",!0).selectAll("circle").data(e).enter().append("circle").attr("class",function(t,e){return"path-"+e}).attr("cx",a.scalefns.xoutf).attr("cy",function(t){return a.scalefns.youtf(t)});return null!==a.color_accessor?(r.attr("fill",a.scalefns.colorf),r.attr("stroke",a.scalefns.colorf)):r.classed("mg-points-mono",!0),null!==a.size_accessor?r.attr("r",a.scalefns.sizef):r.attr("r",a.point_size),this},this.rollover=function(){var t=O(a.target);0===t.selectAll(".mg-active-datapoint-container").nodes().length&&S(t,"mg-active-datapoint-container"),t.selectAll(".mg-voronoi").remove();var e=E.voronoi().x(a.scalefns.xoutf).y(a.scalefns.youtf).extent([[a.buffer,a.buffer+(a.title?a.title_y_position:0)],[a.width-a.buffer,a.height-a.buffer]]);return t.append("g").attr("class","mg-voronoi").selectAll("path").data(e.polygons(we(a.data[0],a))).enter().append("path").attr("d",function(t){return null==t?null:"M"+t.join(",")+"Z"}).attr("class",function(t,e){return"path-"+e}).style("fill-opacity",0).on("mouseover",this.rolloverOn(a)).on("mouseout",this.rolloverOff(a)).on("mousemove",this.rolloverMove(a)),1===a.data[0].length&&be(a,t,a.data[0][0]),this},this.rolloverOn=function(a){var n=O(a.target);return function(t,e){n.selectAll(".mg-points circle").classed("selected",!1);var r=n.selectAll(".mg-points circle.path-"+e).classed("selected",!0);a.size_accessor?r.attr("r",function(t){return a.scalefns.sizef(t)+a.active_point_size_increase}):r.attr("r",a.point_size+a.active_point_size_increase),a.linked&&!MG.globals.link&&(MG.globals.link=!0,E.selectAll(".mg-voronoi .path-"+e).each(function(){E.select(o).on("mouseover")(t,e)})),a.show_rollover_text&&be(a,n,t.data),a.mouseover&&a.mouseover(t,e)}},this.rolloverOff=function(a){var n=O(a.target);return function(t,e){a.linked&&MG.globals.link&&(MG.globals.link=!1,E.selectAll(".mg-voronoi .path-"+e).each(function(){E.select(o).on("mouseout")(t,e)}));var r=n.selectAll(".mg-points circle").classed("unselected",!1).classed("selected",!1);a.size_accessor?r.attr("r",a.scalefns.sizef):r.attr("r",a.point_size),1<a.data[0].length&&Lt(n),a.mouseout&&a.mouseout(t,e)}},this.rolloverMove=function(r){return function(t,e){r.mousemove&&r.mousemove(t,e)}},this.update=function(t){return this},this.windowListeners=function(){return Qt(this.args),this},this.init(a)},{y_padding_percentage:.05,y_outer_padding_percentage:.2,ygroup_padding_percentage:0,ygroup_outer_padding_percentage:0,x_padding_percentage:.05,x_outer_padding_percentage:.2,xgroup_padding_percentage:0,xgroup_outer_padding_percentage:0,y_categorical_show_guides:!0,x_categorical_show_guides:!0,buffer:16,ls:!1,lowess:!1,point_size:2.5,label_accessor:null,size_accessor:null,color_accessor:null,size_range:null,color_range:null,size_domain:null,color_domain:null,active_point_size_increase:1,color_type:"number"});MG.register("bar",function(M){var G=this;this.args=M,this.init=function(e){if((G.args=e).x_axis_type=s(e,"x"),e.y_axis_type=s(e,"y"),"categorical"==e.x_axis_type?e.orientation="vertical":"categorical"==e.y_axis_type?e.orientation="horizontal":"categorical"!=e.x_axis_type&&"categorical"!=e.y_axis_type&&(e.orientation="vertical"),Me(e),Oe(e),Ot(e),"categorical"===e.x_axis_type?(MG.scale_factory(e).namespace("x").categoricalDomainFromData().categoricalRangeBands([0,e.xgroup_height],null===e.xgroup_accessor),e.xgroup_accessor?new MG.scale_factory(e).namespace("xgroup").categoricalDomainFromData().categoricalRangeBands("bottom"):(e.scales.XGROUP=function(t){return Y(e)},e.scalefns.xgroupf=function(t){return Y(e)}),e.scalefns.xoutf=function(t){return e.scalefns.xf(t)+e.scalefns.xgroupf(t)}):(MG.scale_factory(e).namespace("x").inflateDomain(!0).zeroBottom("categorical"===e.y_axis_type).numericalDomainFromData((e.baselines||[]).map(function(t){return t[e.x_accessor]})).numericalRange("bottom"),e.scalefns.xoutf=e.scalefns.xf),"categorical"===e.y_axis_type)MG.scale_factory(e).namespace("y").zeroBottom(!0).categoricalDomainFromData().categoricalRangeBands([0,e.ygroup_height],!0),e.ygroup_accessor?new MG.scale_factory(e).namespace("ygroup").categoricalDomainFromData().categoricalRangeBands("left"):(e.scales.YGROUP=function(){return b(e)},e.scalefns.ygroupf=function(t){return b(e)}),e.scalefns.youtf=function(t){return e.scalefns.yf(t)+e.scalefns.ygroupf(t)};else{var t=(e.baselines||[]).map(function(t){return t[e.y_accessor]});MG.scale_factory(e).namespace("y").inflateDomain(!0).zeroBottom("categorical"===e.x_axis_type).numericalDomainFromData(t).numericalRange("left"),e.scalefns.youtf=function(t){return e.scalefns.yf(t)}}return null!==e.ygroup_accessor&&(e.ycolor_accessor=e.y_accessor,MG.scale_factory(e).namespace("ycolor").scaleName("color").categoricalDomainFromData().categoricalColorRange()),null!==e.xgroup_accessor&&(e.xcolor_accessor=e.x_accessor,MG.scale_factory(e).namespace("xcolor").scaleName("color").categoricalDomainFromData().categoricalColorRange()),new MG.axis_factory(e).namespace("x").type(e.x_axis_type).zeroLine("categorical"===e.y_axis_type).position(e.x_axis_position).draw(),new MG.axis_factory(e).namespace("y").type(e.y_axis_type).zeroLine("categorical"===e.x_axis_type).position(e.y_axis_position).draw(),G.mainPlot(),G.markers(),G.rollover(),G.windowListeners(),G},this.mainPlot=function(){var t=O(M.target),e=M.data[0],r=t.select("g.mg-barplot"),a=r.empty(),n=void 0;a&&M.animate_on_load||M.transition_on_update,M.transition_duration,a&&(r=t.append("g").classed("mg-barplot",!0)),n=r.selectAll(".mg-bar").data(e).enter().append("rect").classed("mg-bar",!0).classed("default-bar",!M.scales.hasOwnProperty("COLOR"));var o=void 0,i=void 0,s=void 0,c=void 0,l=void 0,u=void 0,d=void 0,f=void 0,p=void 0,_=void 0,m=void 0,g=void 0,h=void 0,x=void 0,y=void 0,v=void 0;if("vertical"==M.orientation&&(o="height",i="width",s=M.y_axis_type,c=M.x_axis_type,l="y",u="x",d="categorical"==s?M.scalefns.youtf:M.scalefns.yf,f="categorical"==c?M.scalefns.xoutf:M.scalefns.xf,p=M.scales.Y,_=M.scales.X,m=M.y_accessor,g=M.x_accessor,h=function(t){var e=void 0;return e=d(t),t[m]<0&&(e=p(0)),e},x=function(t){return Math.abs(d(t)-p(0))},y=function(t){return Math.abs(p(t[M.reference_accessor])-p(0))},v=function(t){return p(t[M.reference_accessor])}),"horizontal"==M.orientation&&(o="width",i="height",s=M.x_axis_type,c=M.y_axis_type,l="x",u="y",d="categorical"==s?M.scalefns.xoutf:M.scalefns.xf,f="categorical"==c?M.scalefns.youtf:M.scalefns.yf,p=M.scales.X,_=M.scales.Y,m=M.x_accessor,g=M.y_accessor,h=function(t){return p(0)},x=function(t){return Math.abs(d(t)-p(0))},y=function(t){return Math.abs(p(t[M.reference_accessor])-p(0))},v=function(t){return p(0)}),n.attr(l,h),n.attr(u,function(t){var e=void 0;return"categorical"==c?e=f(t):(e=_(0),t[g]<0&&(e=f(t))),e-=M.bar_thickness/2}),M.scales.COLOR&&n.attr("fill",M.scalefns.colorf),n.attr(o,x).attr(i,function(t){return M.bar_thickness}),null!==M.reference_accessor){var b=e.filter(function(t){return t.hasOwnProperty(M.reference_accessor)});r.selectAll(".mg-categorical-reference").data(b).enter().append("rect").attr(l,v).attr(u,function(t){return f(t)-M.reference_thickness/2}).attr(o,y).attr(i,M.reference_thickness)}if(null!==M.comparison_accessor){var w=null;w=null===M.comparison_thickness?M.bar_thickness/2:M.comparison_thickness;var k=e.filter(function(t){return t.hasOwnProperty(M.comparison_accessor)});r.selectAll(".mg-categorical-comparison").data(k).enter().append("line").attr(l+"1",function(t){return p(t[M.comparison_accessor])}).attr(l+"2",function(t){return p(t[M.comparison_accessor])}).attr(u+"1",function(t){return f(t)-w/2}).attr(u+"2",function(t){return f(t)+w/2}).attr("stroke","black").attr("stroke-width",M.comparison_width)}return(M.legend||null!==M.color_accessor&&M.ygroup_accessor!==M.color_accessor)&&(M.legend_target?function(t){var e=t.legend_target,r=t.orientation,a=t.scales;if(e){var n=E.select(e).append("div").classed("mg-bar-target-legend",!0);("horizontal"==r?a.Y.domain():a.X.domain()).forEach(function(t){var e=n.append("span").classed("mg-bar-target-element",!0);e.append("span").classed("mg-bar-target-legend-shape",!0).style("color",a.COLOR(t)).text("◼ "),e.append("span").classed("mg-bar-target-legend-text",!0).text(t)})}}(M):function(t,r){var e=void 0;e="horizontal"==r.orientation?r.scales.Y.domain():r.scales.X.domain();var a=0,n=t.append("g").classed("mg-bar-legend",!0).append("text");n.selectAll("*").remove(),n.attr("width",r.right).attr("height",100).attr("text-anchor","start"),e.forEach(function(t){var e=n.append("tspan").attr("x",P(r)).attr("y",r.height/2).attr("dy",1.1*a+"em");e.append("tspan").text("■ ").attr("fill",r.scales.COLOR(t)).attr("font-size",20),e.append("tspan").text(t).attr("font-weight",300).attr("font-size",10),a++})}(t,M)),G},this.markers=function(){return St(M),G},this.rollover=function(){var t=O(M.target);0===t.selectAll(".mg-active-datapoint-container").nodes().length&&S(t,"mg-active-datapoint-container"),t.selectAll(".mg-rollover-rect").remove(),t.selectAll(".mg-active-datapoint").remove();var e=void 0,r=void 0,a=void 0,n=void 0,o=void 0,i=void 0,s=void 0,c=void 0,l=void 0,u=void 0,d=void 0,f=void 0;"vertical"==M.orientation&&(e="height",r="width",a=M.y_axis_type,n=M.x_axis_type,o="y",i="x","categorical"==a?M.scalefns.youtf:M.scalefns.yf,s="categorical"==n?M.scalefns.xoutf:M.scalefns.xf,c=M.scales.Y,l=M.scales.X,M.y_accessor,u=M.x_accessor,d=function(t){return b(M)},f=function(t){return M.height-M.top-M.bottom-2*M.buffer}),"horizontal"==M.orientation&&(e="width",r="height",a=M.x_axis_type,n=M.y_axis_type,o="x",i="y","categorical"==a?M.scalefns.xoutf:M.scalefns.xf,s="categorical"==n?M.scalefns.youtf:M.scalefns.yf,c=M.scales.X,l=M.scales.Y,M.x_accessor,u=M.y_accessor,d=function(t){return c(0)},f=function(t){return M.width-M.left-M.right-2*M.buffer});var p=void 0,_=void 0;"right"===M.rollover_align?(p=M.width-M.right,_="end"):"left"===M.rollover_align?(p=M.left,_="start"):(p=(M.width-M.left-M.right)/2+M.left,_="middle"),t.append("text").attr("class","mg-active-datapoint").attr("xml:space","preserve").attr("x",p).attr("y",.75*M.top).attr("dy",".35em").attr("text-anchor",_);var m=t.append("g").attr("class","mg-rollover-rect").selectAll(".mg-bar-rollover").data(M.data[0]).enter().append("rect").attr("class","mg-bar-rollover");return m.attr("opacity",0).attr(o,d).attr(i,function(t){var e=void 0;return"categorical"==n?e=s(t):(e=l(0),t[u]<0&&(e=s(t))),e-=M.bar_thickness/2}),m.attr(e,f),m.attr(r,function(t){return M.bar_thickness}),m.on("mouseover",G.rolloverOn(M)).on("mouseout",G.rolloverOff(M)).on("mousemove",G.rolloverMove(M)),G},this.rolloverOn=function(o){var i=O(o.target);return G.is_vertical?o.x_accessor:o.y_accessor,G.is_vertical?o.y_accessor:o.x_accessor,G.is_vertical?o.yax_units:o.xax_units,function(t,r){MG.time_format(o.utc_time,"%b %e, %Y"),Ee(o);var e=i.selectAll("g.mg-barplot .mg-bar").filter(function(t,e){return e===r}).classed("active",!0);if(o.scales.hasOwnProperty("COLOR")?e.attr("fill",E.rgb(o.scalefns.colorf(t)).darker()):e.classed("default-active",!0),o.show_rollover_text){var a=Xt(o,{svg:i}),n=a.mouseover_row();o.ygroup_accessor&&n.text(t[o.ygroup_accessor]+" ").bold(),n.text(Se(o,t)),n.text(o.y_accessor+": "+t[o.y_accessor]),(o.predictor_accessor||o.baseline_accessor)&&(n=a.mouseover_row(),o.predictor_accessor&&n.text(Pe(o,t,null,o.predictor_accessor,!1)),o.baseline_accessor&&n.text(Pe(o,t,null,o.baseline_accessor,!1)))}o.mouseover&&o.mouseover(t,r)}},this.rolloverOff=function(a){var n=O(a.target);return function(t,e){var r=n.selectAll("g.mg-barplot .mg-bar.active").classed("active",!1);a.scales.hasOwnProperty("COLOR")?r.attr("fill",a.scalefns.colorf(t)):r.classed("default-active",!1),n.select(".mg-active-datapoint").text(""),Lt(n),a.mouseout&&a.mouseout(t,e)}},this.rolloverMove=function(r){return function(t,e){r.mousemove&&r.mousemove(t,e)}},this.windowListeners=function(){return Qt(G.args),G},this.init(M)},{y_padding_percentage:.05,y_outer_padding_percentage:.2,ygroup_padding_percentage:0,ygroup_outer_padding_percentage:0,x_padding_percentage:.05,x_outer_padding_percentage:.2,xgroup_padding_percentage:0,xgroup_outer_padding_percentage:0,buffer:16,y_accessor:"factor",x_accessor:"value",reference_accessor:null,comparison_accessor:null,secondary_label_accessor:null,color_accessor:null,color_type:"category",color_domain:null,reference_thickness:1,comparison_width:3,comparison_thickness:null,legend:!1,legend_target:null,mouseover_align:"right",baseline_accessor:null,predictor_accessor:null,predictor_proportion:5,show_bar_zero:!0,binned:!0,truncate_x_labels:!0,truncate_y_labels:!0}),MG.data_table=function(t){return this.args=t,this.args.standard_col={width:150,font_size:12,font_weight:"normal"},this.args.columns=[],this.formatting_options=[["color","color"],["font-weight","font_weight"],["font-style","font_style"],["font-size","font_size"]],this._strip_punctuation=function(t){return t.replace(/[^a-zA-Z0-9 _]+/g,"").replace(/ +?/g,"")},this._format_element=function(a,n,o){this.formatting_options.forEach(function(t){var e=t[0],r=t[1];o[r]&&a.style(e,"string"==typeof o[r]||"number"==typeof o[r]?o[r]:o[r](n))})},this._add_column=function(t,e){var r=this.args.standard_col,a=p(MG.clone(t),MG.clone(r));a.type=e,this.args.columns.push(a)},this.target=function(){var t=arguments[0];return this.args.target=t,this},this.title=function(){return this._add_column(arguments[0],"title"),this},this.text=function(){return this._add_column(arguments[0],"text"),this},this.bullet=function(){return this},this.sparkline=function(){return this},this.number=function(){return this._add_column(arguments[0],"number"),this},this.display=function(){var t=this.args;j(t);var e,r,a,n,o,i,s,c,l,u,d,f,p=t.target,_=E.select(p).append("table").classed("mg-data-table",!0),m=_.append("colgroup"),g=_.append("thead"),h=_.append("tbody");for(a=g.append("tr"),f=0;f<t.columns.length;f++){var x=t.columns[f];i=x.type,c=void 0===(c=x.label)?"":c,n=a.append("th").style("width",x.width).style("text-align","title"===i?"left":"right").text(c),t.show_tooltips&&x.description&&k()&&(n.append("i").classed("fa",!0).classed("fa-question-circle",!0).classed("fa-inverse",!0),$(n.node()).popover({html:!0,animation:!1,content:x.description,trigger:"hover",placement:"top",container:$(n.node())}))}for(f=0;f<t.columns.length;f++)d=m.append("col"),"number"===t.columns[f].type&&d.attr("align","char").attr("char",".");for(var y=0;y<t.data.length;y++){a=h.append("tr");for(var v=0;v<t.columns.length;v++){if(o=(e=t.columns[v]).accessor,s=l=t.data[y][o],"number"===(i=e.type)){if(e.hasOwnProperty("round")&&!e.hasOwnProperty("format")&&(l=E.format("0,."+e.round+"f")(l)),e.hasOwnProperty("value_formatter")&&(l=e.value_formatter(l)),e.hasOwnProperty("format")){e.round&&(l=Math.round(l,e.round));var b,w=e.format;"percentage"===w&&(b=E.format(".0%")),"count"===w&&(b=E.format(",.0f")),"temperature"===w&&(b=function(t){return t+"°"}),l=b(l)}e.hasOwnProperty("currency")&&(l=e.currency+l)}u=a.append("td").classed("table-"+i,!0).classed("table-"+i+"-"+this._strip_punctuation(o),!0).attr("data-value",s).style("width",e.width).style("text-align","title"===i||"text"===i?"left":"right"),this._format_element(u,s,e),"title"===i?(r=u.append("div").text(l),this._format_element(r,l,e),t.columns[v].hasOwnProperty("secondary_accessor")&&u.append("div").text(t.data[y][t.columns[v].secondary_accessor]).classed("secondary-title",!0)):u.text(l)}}return this},this};function ke(t,e){var r,a,n;function o(t){return 1e3===t?3:1e6===t?7:Math.log(t)/Math.LN10}"x"===e?(r=t.x_accessor,a=t.scales.X.ticks(t.xax_count),n=t.processed.max_x):"y"===e&&(r=t.y_accessor,a=t.scales.Y.ticks(t.yax_count),n=t.processed.max_y),("x"===e&&"log"===t.x_scale_type||"y"===e&&"log"===t.y_scale_type)&&(a=a.filter(function(t){return Math.abs(o(t))%1<1e-6||Math.abs(o(t))%1>1-1e-6}));var i=a.length,s=!0;t.data.forEach(function(t,e){t.forEach(function(t,e){if(t[r]%1!=0)return s=!1})}),s&&n<i&&"count"===t.format&&(a=a.filter(function(t){return t%1==0})),"x"===e?t.processed.x_ticks=a:"y"===e&&(t.processed.y_ticks=a)}function Me(r){if(r.data=MG.clone(r.data),r.single_object=!1,r.array_of_objects=!1,r.array_of_arrays=!1,r.nested_array_of_arrays=!1,r.nested_array_of_objects=!1,a(r.data)?(r.nested_array_of_objects=r.data.map(function(t){return e(t)}),r.nested_array_of_arrays=r.data.map(function(t){return a(t)})):(r.array_of_objects=n(r.data),r.array_of_arrays=a(r.data)),"line"===r.chart_type?(r.array_of_objects||r.array_of_arrays)&&(r.data=[r.data]):y(r.data[0])||(r.data=[r.data]),Ge(r,"x_accessor"),Ge(r,"y_accessor"),void 0!==r.color&&(r.colors=r.color),null!==r.colors&&"string"==typeof r.colors&&(r.colors=[r.colors]),"line"===r.chart_type&&!0===r.x_sort)for(var t=0;t<r.data.length;t++)r.data[t].sort(function(t,e){return t[r.x_accessor]-e[r.x_accessor]});return this}function Ge(e,r){y(e[r])&&(e.data=e.data.map(function(t){return e[r].map(function(e){return t.map(function(t){if(void 0!==(t=MG.clone(t))[e])return t["multiline_"+r]=t[e],t}).filter(function(t){return void 0!==t})})})[0],e[r]="multiline_"+r)}function Ae(r){var t,e=0<E.sum(r.data.map(function(t){return 0<t.length&&x(t[0][r.x_accessor])}));if((r.missing_is_zero||r.missing_is_hidden)&&"line"===r.chart_type&&e)for(var a=0;a<r.data.length;a++)if(!(r.data[a].length<=1)){var n=r.data[a][0],o=r.data[a][r.data[a].length-1],i=[],s=MG.clone(n[r.x_accessor]).setDate(n[r.x_accessor].getDate()+1),c=r.min_x?r.min_x:s,l=r.max_x?r.max_x:o[r.x_accessor];if(t=lt((l-c)/1e3),-1!==["four-days","many-days","many-months","years","default"].indexOf(t)&&null===r.missing_is_hidden_accessor)for(var u=new Date(c);u<=l;u.setDate(u.getDate()+1)){var d={};u.setHours(0,0,0,0),Date.parse(u)===Date.parse(new Date(s))&&i.push(MG.clone(r.data[a][0]));var f=null;r.data[a].forEach(function(t,e){if(Date.parse(t[r.x_accessor])===Date.parse(new Date(u)))return f=t,!1}),f?((f[r.missing_is_hidden_accessor]||null===f[r.y_accessor])&&(f._missing=!0),i.push(f)):(d[r.x_accessor]=new Date(u),d[r.y_accessor]=0,d._missing=!0,i.push(d))}else for(var p=0;p<r.data[a].length;p+=1){var _=MG.clone(r.data[a][p]);_._missing=r.data[a][p][r.missing_is_hidden_accessor],i.push(_)}r.data[a]=i}return this}function De(e){var t,r=e.data[0];if(!1===e.binned){if("object"===c(r[0]))t=r.map(function(t){return t[e.x_accessor]});else{if("number"!=typeof r[0])return void console.log("TypeError: expected an array of numbers, found "+c(r[0]));t=r}var a=E.histogram();e.bins&&a.thresholds(e.bins);var n=a(t);e.processed_data=n.map(function(t){return{x:t.x0,y:t.length}})}else{var o,i;e.processed_data=r.map(function(t){return{x:t[e.x_accessor],y:t[e.y_accessor]}});for(var s=0;s<e.processed_data.length;s++)o=e.processed_data[s],s===e.processed_data.length-1?o.dx=e.processed_data[s-1].dx:(i=e.processed_data[s+1],o.dx=i.x-o.x)}return e.processed||(e.processed={}),e.processed.original_data=e.data,e.processed.original_x_accessor=e.x_accessor,e.processed.original_y_accessor=e.y_accessor,e.data=[e.processed_data],e.x_accessor=e.processed_x_accessor,e.y_accessor=e.processed_y_accessor,this}function Oe(e){var t=e.data[0],r=t.map(function(t){return t[e.x_accessor]}),a=t.map(function(t){return t[e.y_accessor]});return e.least_squares&&(e.ls_line=Fe(r,a)),this}function ze(e){var t=O(e.target),r=e.data[0],a=E.min(r,function(t){return t[e.x_accessor]}),n=E.max(r,function(t){return t[e.x_accessor]});E.select(e.target).selectAll(".mg-least-squares-line").remove(),t.append("svg:line").attr("x1",e.scales.X(a)).attr("x2",e.scales.X(n)).attr("y1",e.scales.Y(e.ls_line.fit(a))).attr("y2",e.scales.Y(e.ls_line.fit(n))).attr("class","mg-least-squares-line")}function Fe(t,e){var r,a,n;t.length;r=x(t[0])?t.map(function(t){return t.getTime()}):t,a=x(e[0])?e.map(function(t){return t.getTime()}):e;for(var o=E.mean(r),i=E.mean(a),s=0,c=0,l=0;l<r.length;l++)s+=((n=r[l])-o)*(a[l]-i),c+=(n-o)*(n-o);var u=s/c,d=i-u*o;return{x0:d,beta:u,fit:function(t){return d+t*u}}}function Ce(t,e){return 0<=t&&t<=1?Math.pow(1-Math.pow(t,e),e):0}function Te(t){var e,r,a,n,o,i,s,c=(a=t,n=E.sum(a.map(function(t){return t.w})),{xbar:E.sum(a.map(function(t){return t.w*t.x}))/n,ybar:E.sum(a.map(function(t){return t.w*t.y}))/n}),l=(o=t,i=r=c.xbar,s=e=c.ybar,E.sum(o.map(function(t){return Math.pow(t.w,2)*(t.x-i)*(t.y-s)}))/E.sum(o.map(function(t){return Math.pow(t.w,2)*Math.pow(t.x-i,2)})));return{beta:l,xbar:r,ybar:e,x0:e-l*r}}function Re(t,e,r,a,n){var o=Math.floor(t.length*r),i=t.slice();i.sort(function(t,e){return t<e?-1:e<t?1:0});for(var s,c,l,u,d,f=E.quantile(i,.98),p=E.quantile(i,.02),_=E.zip(t,e,n).sort(),m=Math.abs(f-p)/a,g=p,h=f,x=E.range(g,h,m),y=[],v=0;v<x.length;v+=1){c=x[v],s=_.map(function(t){return[Math.abs(t[0]-c),t[0],t[1],t[2]]}).sort().slice(0,o),d=E.max(s)[0];var b=Te(s=s.map(function(t){return{w:(e=t[0]/d,Ce(e,3)*t[3]),x:t[1],y:t[2]};var e}));u=b.x0,l=b.beta,y.push(u+l*c)}return{x:x,y:y}}function Ee(r){return"count"===r.format?function(t){var e;return e=t%1!=0?E.format(",."+r.decimals+"f"):E.format(",.0f"),r.yax_units_append?e(t)+r.yax_units:r.yax_units+e(t)}:function(t){var e=(r.decimals?"."+r.decimals:"")+"%";return E.format(e)(t)}}MG.register("missing-data",function(t){var R=this;this.init=function(t){yt(R.args=t),vt(t);var e=E.select(t.target);Gt(e,t);var r,a,n,o,i,s,c,l,u,d,f,p,_,m,g,h,x,y,v,b,w,k,M,G,A,D,O,z,F,C=e.selectAll("svg");if(bt(C,t),kt(C=wt(C,t),t),Mt(C,t),r=t.target,E.select(r).selectAll("svg *").remove(),C.classed("mg-missing",!0),(a=t.legend_target)&&E.select(a).html(""),j(t),t.show_missing_background){!function(t){for(var e=[],r=1;r<=50;r++)e.push({x:r,y:Math.random()-.03*r});t.data=e}(t),(G=t).scales.X=E.scaleLinear().domain([0,G.data.length]).range([Y(G),P(G)]),G.scalefns.yf=function(t){var e=t.y;return G.scales.Y(e)},(M=t).scales.Y=E.scaleLinear().domain([-2,2]).range([M.height-M.bottom-2*M.buffer,M.top]),M.scalefns.xf=function(t){var e=t.x;return M.scales.X(e)};var T=S(C,"mg-missing-pane");h=T,y=(x=t).title,v=x.buffer,b=x.title_y_position,w=x.width,k=x.height,h.append("svg:rect").classed("mg-missing-background",!0).attr("x",v).attr("y",v+2*(y?b:0)).attr("width",w-2*v).attr("height",k-2*v-2*(y?b:0)).attr("rx",15).attr("ry",15),d=T,p=(f=t).scalefns,_=f.interpolate,m=f.data,g=E.line().x(p.xf).y(p.yf).curve(_),d.append("path").attr("class","mg-main-line mg-line1-color").attr("d",g(m)),n=T,i=(o=t).scalefns,s=o.scales,c=o.interpolate,l=o.data,u=E.area().x(i.xf).y0(s.Y.range()[0]).y1(i.yf).curve(c),n.append("path").attr("class","mg-main-area mg-area1-color").attr("d",u(l))}return A=C,O=(D=t).missing_text,z=D.width,F=D.height,A.selectAll(".mg-missing-text").data([O]).enter().append("text").attr("class","mg-missing-text").attr("x",z/2).attr("y",F/2).attr("dy",".50em").attr("text-anchor","middle").text(O),R.windowListeners(),R},this.windowListeners=function(){return Qt(R.args),R},this.init(t)},{top:40,bottom:30,right:10,left:0,buffer:8,legend_target:"",width:350,height:220,missing_text:"Data currently missing or unavailable",scalefns:{},scales:{},show_tooltips:!0,show_missing_background:!0}),MG.raw_data_transformation=Me,MG.process_line=Ae,MG.process_histogram=De,MG.process_categorical_variables=function(t){return"vertical"===t.bar_orientation?t.x_accessor:t.y_accessor,"vertical"===t.bar_orientation?t.y_accessor:t.x_accessor,this},MG.process_point=Oe,MG.add_ls=ze,MG.add_lowess=function(e){var t=O(e.target),r=e.lowess_line,a=E.svg.line().x(function(t){return e.scales.X(t.x)}).y(function(t){return e.scales.Y(t.y)}).interpolate(e.interpolate);t.append("path").attr("d",a(r)).attr("class","mg-lowess-line")},MG.lowess_robust=function(t,e,r,a){var n,o,i=[];for(E.mean(e),o=0;o<t.length;o+=1)i.push(1);var s=(n=Re(t,e,r,a,i)).x,c=n.y;for(o=0;o<100;o+=1){i=E.zip(c,e).map(function(t){return Math.abs(t[1]-t[0])});var l=E.quantile(i.sort(),.5);s=(n=Re(t,e,r,a,i=i.map(function(t){return Ce(t/(6*l),2)}))).x,c=n.y}return E.zip(s,c).map(function(t){var e={};return e.x=t[0],e.y=t[1],e})},MG.lowess=function(t,e,r,a){for(var n=[],o=0;o<t.length;o+=1)n.push(1);Re(t,e,r,a,n)},MG.least_squares=Fe;var Ye=function(t,e,r,a){return"string"==typeof t?MG.time_format(a,t)(e[r]):"function"==typeof t?t(e):e[r]},$e=function(t,e,r){return"string"==typeof t?E.format("s")(e[r]):"function"==typeof t?t(e):e[r]};function Pe(t,e,r,a,n){var o,i=function(t){if(t.rollover_time_format)return MG.time_format(t.utc_time,t.rollover_time_format);switch(t.processed.x_time_frame){case"millis":return MG.time_format(t.utc_time,"%b %e, %Y %H:%M:%S.%L");case"seconds":return MG.time_format(t.utc_time,"%b %e, %Y %H:%M:%S");case"less-than-a-day":case"four-days":return MG.time_format(t.utc_time,"%b %e, %Y %I:%M%p")}return MG.time_format(t.utc_time,"%b %e, %Y")}(t);return o="string"==typeof e[a]?function(t){return t}:Ee(t),null!==r?n?Ye(r,e,a,t.utc):$e(r,e,a):n?i(new Date(+e[a]))+" ":(t.time_series?"":a+": ")+o(e[a])+" "}function Se(t,e){return Pe(t,e,t.x_mouseover,t.x_accessor,t.time_series)}function Le(t,e){return Pe(t,e,t.y_mouseover,t.y_accessor,!1)}function je(c,l){return function(){for(var a=this,n=a.cloneNode(),o=a.getTotalLength()||0,i=(n.setAttribute("d",c),n).getTotalLength()||0,t=[0],e=0,r=l/Math.max(o,i);(e+=r)<1;)t.push(e);t.push(1);var s=t.map(function(t){var e=a.getPointAtLength(t*o),r=n.getPointAtLength(t*i);return E.interpolate([e.x,e.y],[r.x,r.y])});return function(e){return e<1?"M"+s.map(function(t){return t(e)}).join("L"):c}}}function Xe(t){console.error("ERROR : ",t.target," : ",t.error),E.select(t.target).select(".mg-chart-title").append("tspan").attr("class","fa fa-x fa-exclamation-circle mg-warning").attr("dx","0.3em").text("")}return MG.format_rollover_number=Ee,MG.path_tween=je,MG.render_markup=function(t){switch("undefined"==typeof window?"undefined":c(window)){case"undefined":return function(t){var e,r=MG.virtual_window,a=E.select(r.document),n=r.document.createElement("div"),o=global.d3,i=global.window,s=global.document;global.d3=a,global.window=r,global.document=r.document;try{t(n)}catch(t){e=t}if(global.d3=o,global.window=i,global.document=s,e)throw e;return a.select(function(){return n}).html()}(t);default:return e=t,r=document.createElement("div"),e(r),E.select(r).html()}var e,r},MG.init_virtual_window=function(t,e){if(!MG.virtual_window||e){var r=t.jsdom({html:"",features:{QuerySelector:!0}});MG.virtual_window=r.defaultView}},MG.error=Xe,MG}); \ No newline at end of file
diff --git a/priv/static/js/metricsgraphics/MG.js b/priv/static/js/metricsgraphics/MG.js
new file mode 100644
index 0000000..7f24a0f
--- /dev/null
+++ b/priv/static/js/metricsgraphics/MG.js
@@ -0,0 +1 @@
+(typeof window === 'undefined' ? global : window).MG = {version: '2.11'};
diff --git a/priv/static/js/metricsgraphics/charts/bar.js b/priv/static/js/metricsgraphics/charts/bar.js
new file mode 100644
index 0000000..b39d59f
--- /dev/null
+++ b/priv/static/js/metricsgraphics/charts/bar.js
@@ -0,0 +1,792 @@
+{
+ // TODO add styles to stylesheet instead
+ function scaffold({target, width, height, top, left, right, buffer}) {
+ const svg = mg_get_svg_child_of(target);
+ // main margins
+ svg.append('line')
+ .attr('x1', 0)
+ .attr('x2', width)
+ .attr('y1', top)
+ .attr('y2', top)
+ .attr('stroke', 'black');
+ svg.append('line')
+ .attr('x1', 0)
+ .attr('x2', width)
+ .attr('y1', height - bottom)
+ .attr('y2', height - bottom)
+ .attr('stroke', 'black');
+
+ svg.append('line')
+ .attr('x1', left)
+ .attr('x2', left)
+ .attr('y1', 0)
+ .attr('y2', height)
+ .attr('stroke', 'black');
+
+ svg.append('line')
+ .attr('x1', width - right)
+ .attr('x2', width - right)
+ .attr('y1', 0)
+ .attr('y2', height)
+ .attr('stroke', 'black');
+
+ // plot area margins
+ svg.append('line')
+ .attr('x1', 0)
+ .attr('x2', width)
+ .attr('y1', height - bottom - buffer)
+ .attr('y2', height - bottom - buffer)
+ .attr('stroke', 'gray');
+
+ svg.append('line')
+ .attr('x1', 0)
+ .attr('x2', width)
+ .attr('y1', top + buffer)
+ .attr('y2', top + buffer)
+ .attr('stroke', 'gray');
+
+ svg.append('line')
+ .attr('x1', left + buffer)
+ .attr('x2', left + buffer)
+ .attr('y1', 0)
+ .attr('y2', args.height)
+ .attr('stroke', 'gray');
+ svg.append('line')
+ .attr('x1', width - right - buffer)
+ .attr('x2', width - right - buffer)
+ .attr('y1', 0)
+ .attr('y2', height)
+ .attr('stroke', 'gray');
+ }
+
+ // barchart re-write.
+ function mg_targeted_legend({legend_target, orientation, scales}) {
+ let labels;
+ const plot = '';
+ if (legend_target) {
+
+ const div = d3.select(legend_target).append('div').classed('mg-bar-target-legend', true);
+
+ if (orientation == 'horizontal') labels = scales.Y.domain();
+ else labels = scales.X.domain();
+
+ labels.forEach(label => {
+ const outer_span = div.append('span').classed('mg-bar-target-element', true);
+ outer_span.append('span')
+ .classed('mg-bar-target-legend-shape', true)
+ .style('color', scales.COLOR(label))
+ .text('\u25FC ');
+ outer_span.append('span')
+ .classed('mg-bar-target-legend-text', true)
+ .text(label);
+ });
+ }
+ }
+
+ function legend_on_graph(svg, args) {
+ // draw each element at the top right
+ // get labels
+
+ let labels;
+ if (args.orientation=='horizontal') labels = args.scales.Y.domain();
+ else labels = args.scales.X.domain();
+
+ let lineCount = 0;
+ const lineHeight = 1.1;
+ const g = svg.append('g').classed("mg-bar-legend", true);
+ const textContainer = g.append('text');
+
+ textContainer
+ .selectAll('*')
+ .remove();
+ textContainer
+ .attr('width', args.right)
+ .attr('height', 100)
+ .attr('text-anchor', 'start');
+
+ labels.forEach(label => {
+ const sub_container = textContainer.append('tspan')
+ .attr('x', mg_get_plot_right(args))
+ .attr('y', args.height / 2)
+ .attr('dy', `${lineCount * lineHeight}em`);
+ sub_container.append('tspan')
+ .text('\u25a0 ')
+ .attr('fill', args.scales.COLOR(label))
+ .attr('font-size', 20);
+ sub_container.append('tspan')
+ .text(label)
+ .attr('font-weight', 300)
+ .attr('font-size', 10);
+ lineCount++;
+ });
+
+ // d.values.forEach(function (datum) {
+ // formatted_y = mg_format_y_rollover(args, num, datum);
+
+ // if (args.y_rollover_format !== null) {
+ // formatted_y = number_rollover_format(args.y_rollover_format, datum, args.y_accessor);
+ // } else {
+ // formatted_y = args.yax_units + num(datum[args.y_accessor]);
+ // }
+
+ // sub_container = textContainer.append('tspan').attr('x', 0).attr('y', (lineCount * lineHeight) + 'em');
+ // formatted_y = mg_format_y_rollover(args, num, datum);
+ // mouseover_tspan(sub_container, '\u2014 ')
+ // .color(args, datum);
+ // mouseover_tspan(sub_container, formatted_x + ' ' + formatted_y);
+
+ // lineCount++;
+ // });
+ }
+
+ function barChart(args) {
+ this.args = args;
+
+ this.init = (args) => {
+ this.args = args;
+ args.x_axis_type = mg_infer_type(args, 'x');
+ args.y_axis_type = mg_infer_type(args, 'y');
+
+ // this is specific to how rects work in svg, let's keep track of the bar orientation to
+ // plot appropriately.
+ if (args.x_axis_type == 'categorical') {
+ args.orientation = 'vertical';
+ } else if (args.y_axis_type == 'categorical') {
+ args.orientation = 'horizontal';
+ } else if (args.x_axis_type != 'categorical' && args.y_axis_type != 'categorical') {
+ // histogram.
+ args.orientation = 'vertical';
+ }
+
+ raw_data_transformation(args);
+
+ process_point(args);
+ init(args);
+
+ let xMaker;
+ let yMaker;
+
+ if (args.x_axis_type === 'categorical') {
+ xMaker = MG.scale_factory(args)
+ .namespace('x')
+ .categoricalDomainFromData()
+ .categoricalRangeBands([0, args.xgroup_height], args.xgroup_accessor === null);
+
+ if (args.xgroup_accessor) {
+ new MG.scale_factory(args)
+ .namespace('xgroup')
+ .categoricalDomainFromData()
+ .categoricalRangeBands('bottom');
+
+ } else {
+ args.scales.XGROUP = d => mg_get_plot_left(args);
+ args.scalefns.xgroupf = d => mg_get_plot_left(args);
+ }
+
+ args.scalefns.xoutf = d => args.scalefns.xf(d) + args.scalefns.xgroupf(d);
+ } else {
+ xMaker = MG.scale_factory(args)
+ .namespace('x')
+ .inflateDomain(true)
+ .zeroBottom(args.y_axis_type === 'categorical')
+ .numericalDomainFromData((args.baselines || []).map(d => d[args.x_accessor]))
+ .numericalRange('bottom');
+
+ args.scalefns.xoutf = args.scalefns.xf;
+ }
+
+ // y-scale generation. This needs to get simplified.
+ if (args.y_axis_type === 'categorical') {
+ yMaker = MG.scale_factory(args)
+ .namespace('y')
+ .zeroBottom(true)
+ .categoricalDomainFromData()
+ .categoricalRangeBands([0, args.ygroup_height], true);
+
+ if (args.ygroup_accessor) {
+
+ new MG.scale_factory(args)
+ .namespace('ygroup')
+ .categoricalDomainFromData()
+ .categoricalRangeBands('left');
+
+ } else {
+ args.scales.YGROUP = () => mg_get_plot_top(args);
+ args.scalefns.ygroupf = d => mg_get_plot_top(args);
+
+ }
+ args.scalefns.youtf = d => args.scalefns.yf(d) + args.scalefns.ygroupf(d);
+
+ } else {
+ const baselines = (args.baselines || []).map(d => d[args.y_accessor]);
+
+ yMaker = MG.scale_factory(args)
+ .namespace('y')
+ .inflateDomain(true)
+ .zeroBottom(args.x_axis_type === 'categorical')
+ .numericalDomainFromData(baselines)
+ .numericalRange('left');
+
+ args.scalefns.youtf = d => args.scalefns.yf(d);
+ }
+
+ if (args.ygroup_accessor !== null) {
+ args.ycolor_accessor = args.y_accessor;
+ MG.scale_factory(args)
+ .namespace('ycolor')
+ .scaleName('color')
+ .categoricalDomainFromData()
+ .categoricalColorRange();
+ }
+
+ if (args.xgroup_accessor !== null) {
+ args.xcolor_accessor = args.x_accessor;
+ MG.scale_factory(args)
+ .namespace('xcolor')
+ .scaleName('color')
+ .categoricalDomainFromData()
+ .categoricalColorRange();
+ }
+
+ // if (args.ygroup_accessor !== null) {
+ // MG.scale_factory(args)
+ // .namespace('ygroup')
+ // .categoricalDomainFromData()
+ // .categoricalColorRange();
+ // }
+
+ new MG.axis_factory(args)
+ .namespace('x')
+ .type(args.x_axis_type)
+ .zeroLine(args.y_axis_type === 'categorical')
+ .position(args.x_axis_position)
+ .draw();
+
+ new MG.axis_factory(args)
+ .namespace('y')
+ .type(args.y_axis_type)
+ .zeroLine(args.x_axis_type === 'categorical')
+ .position(args.y_axis_position)
+ .draw();
+
+ //mg_categorical_group_color_scale(args);
+
+ this.mainPlot();
+ this.markers();
+ this.rollover();
+ this.windowListeners();
+ //scaffold(args)
+
+ return this;
+ };
+
+ this.mainPlot = () => {
+ const svg = mg_get_svg_child_of(args.target);
+ const data = args.data[0];
+ let barplot = svg.select('g.mg-barplot');
+ const fresh_render = barplot.empty();
+
+ let bars, predictor_bars, pp, pp0, baseline_marks;
+
+ const perform_load_animation = fresh_render && args.animate_on_load;
+ const should_transition = perform_load_animation || args.transition_on_update;
+ const transition_duration = args.transition_duration || 1000;
+
+ // draw the plot on first render
+ if (fresh_render) {
+ barplot = svg.append('g')
+ .classed('mg-barplot', true);
+ }
+
+ bars = barplot.selectAll('.mg-bar')
+ .data(data)
+ .enter()
+ .append('rect')
+ .classed('mg-bar', true)
+ .classed('default-bar', args.scales.hasOwnProperty('COLOR') ? false : true);
+
+ // TODO - reimplement
+
+ // reference_accessor {}
+
+ // if (args.predictor_accessor) {
+ // predictor_bars = barplot.selectAll('.mg-bar-prediction')
+ // .data(data.filter(function(d) {
+ // return d.hasOwnProperty(args.predictor_accessor) }));
+
+ // predictor_bars.exit().remove();
+
+ // predictor_bars.enter().append('rect')
+ // .classed('mg-bar-prediction', true);
+ // }
+
+ // if (args.baseline_accessor) {
+ // baseline_marks = barplot.selectAll('.mg-bar-baseline')
+ // .data(data.filter(function(d) {
+ // return d.hasOwnProperty(args.baseline_accessor) }));
+
+ // baseline_marks.exit().remove();
+
+ // baseline_marks.enter().append('line')
+ // .classed('mg-bar-baseline', true);
+ // }
+
+ let appropriate_size;
+
+ // setup transitions
+ // if (should_transition) {
+ // bars = bars.transition()
+ // .duration(transition_duration);
+
+ // if (predictor_bars) {
+ // predictor_bars = predictor_bars.transition()
+ // .duration(transition_duration);
+ // }
+
+ // if (baseline_marks) {
+ // baseline_marks = baseline_marks.transition()
+ // .duration(transition_duration);
+ // }
+ // }
+
+ //appropriate_size = args.scales.Y_ingroup.rangeBand()/1.5;
+ let length, width, length_type, width_type, length_coord, width_coord,
+ length_scalefn, width_scalefn, length_scale, width_scale,
+ length_accessor, width_accessor, length_coord_map, width_coord_map,
+ length_map, width_map;
+
+ let reference_length_map, reference_length_coord_fn;
+
+ if (args.orientation == 'vertical') {
+ length = 'height';
+ width = 'width';
+ length_type = args.y_axis_type;
+ width_type = args.x_axis_type;
+ length_coord = 'y';
+ width_coord = 'x';
+ length_scalefn = length_type == 'categorical' ? args.scalefns.youtf : args.scalefns.yf;
+ width_scalefn = width_type == 'categorical' ? args.scalefns.xoutf : args.scalefns.xf;
+ length_scale = args.scales.Y;
+ width_scale = args.scales.X;
+ length_accessor = args.y_accessor;
+ width_accessor = args.x_accessor;
+
+ length_coord_map = d => {
+ let l;
+ l = length_scalefn(d);
+ if (d[length_accessor] < 0) {
+ l = length_scale(0);
+ }
+ return l;
+ };
+
+ length_map = d => Math.abs(length_scalefn(d) - length_scale(0));
+
+ reference_length_map = d => Math.abs(length_scale(d[args.reference_accessor]) - length_scale(0));
+
+ reference_length_coord_fn = d => length_scale(d[args.reference_accessor]);
+ }
+
+ if (args.orientation == 'horizontal') {
+ length = 'width';
+ width = 'height';
+ length_type = args.x_axis_type;
+ width_type = args.y_axis_type;
+ length_coord = 'x';
+ width_coord = 'y';
+ length_scalefn = length_type == 'categorical' ? args.scalefns.xoutf : args.scalefns.xf;
+ width_scalefn = width_type == 'categorical' ? args.scalefns.youtf : args.scalefns.yf;
+ length_scale = args.scales.X;
+ width_scale = args.scales.Y;
+ length_accessor = args.x_accessor;
+ width_accessor = args.y_accessor;
+
+ length_coord_map = d => {
+ let l;
+ l = length_scale(0);
+ return l;
+ };
+
+ length_map = d => Math.abs(length_scalefn(d) - length_scale(0));
+
+ reference_length_map = d => Math.abs(length_scale(d[args.reference_accessor]) - length_scale(0));
+
+ reference_length_coord_fn = d => length_scale(0);
+ }
+
+ // if (perform_load_animation) {
+ // bars.attr(length, 0);
+
+ // if (predictor_bars) {
+ // predictor_bars.attr(length, 0);
+ // }
+
+ // // if (baseline_marks) {
+ // // baseline_marks.attr({
+ // // x1: args.scales.X(0),
+ // // x2: args.scales.X(0)
+ // // });
+ // // }
+ // }
+
+ bars.attr(length_coord, length_coord_map);
+
+ // bars.attr(length_coord, 40)
+ //bars.attr(width_coord, 70)
+
+ bars.attr(width_coord, d => {
+ let w;
+ if (width_type == 'categorical') {
+ w = width_scalefn(d);
+ } else {
+ w = width_scale(0);
+ if (d[width_accessor] < 0) {
+ w = width_scalefn(d);
+ }
+ }
+ w = w - args.bar_thickness/2;
+ return w;
+ });
+
+ if (args.scales.COLOR) {
+ bars.attr('fill', args.scalefns.colorf);
+ }
+
+ bars
+ .attr(length, length_map)
+ .attr(width, d => args.bar_thickness);
+
+ if (args.reference_accessor !== null) {
+ const reference_data = data.filter(d => d.hasOwnProperty(args.reference_accessor));
+ const reference_bars = barplot.selectAll('.mg-categorical-reference')
+ .data(reference_data)
+ .enter()
+ .append('rect');
+
+ reference_bars
+ .attr(length_coord, reference_length_coord_fn)
+ .attr(width_coord, d => width_scalefn(d) - args.reference_thickness/2)
+ .attr(length, reference_length_map)
+ .attr(width, args.reference_thickness);
+ }
+
+ if (args.comparison_accessor !== null) {
+ let comparison_thickness = null;
+ if (args.comparison_thickness === null) {
+ comparison_thickness = args.bar_thickness/2;
+ } else {
+ comparison_thickness = args.comparison_thickness;
+ }
+
+ const comparison_data = data.filter(d => d.hasOwnProperty(args.comparison_accessor));
+ const comparison_marks = barplot.selectAll('.mg-categorical-comparison')
+ .data(comparison_data)
+ .enter()
+ .append('line');
+
+ comparison_marks
+ .attr(`${length_coord}1`, d => length_scale(d[args.comparison_accessor]))
+ .attr(`${length_coord}2`, d => length_scale(d[args.comparison_accessor]))
+ .attr(`${width_coord}1`, d => width_scalefn(d) - comparison_thickness/2)
+ .attr(`${width_coord}2`, d => width_scalefn(d) + comparison_thickness/2)
+ .attr('stroke', 'black')
+ .attr('stroke-width', args.comparison_width);
+ }
+
+ //bars.attr(width_coord, );
+ // bars.attr('width', 50);
+ // bars.attr('height', 50);
+ // bars.attr('y', function(d){
+ // var y = args.scales.Y(0);
+ // if (d[args.y_accessor] < 0) {
+ // y = args.scalefns.yf(d);
+ // }
+ // return y;
+ // });
+
+ // bars.attr('x', function(d){
+ // return 40;
+ // })
+
+ // bars.attr('width', function(d){
+ // return 100;
+ // });
+
+ // bars.attr('height', 100);
+
+ // bars.attr('fill', 'black');
+ // bars.attr('x', function(d) {
+ // var x = args.scales.X(0);
+ // if (d[args.x_accessor] < 0) {
+ // x = args.scalefns.xf(d);
+ // }
+ // return x;
+ // })
+ // TODO - reimplement.
+ // if (args.predictor_accessor) {
+ // predictor_bars
+ // .attr('x', args.scales.X(0))
+ // .attr('y', function(d) {
+ // return args.scalefns.ygroupf(d) + args.scalefns.yf(d) + args.scales.Y.rangeBand() * (7 / 16) // + pp0 * appropriate_size/(pp*2) + appropriate_size / 2;
+ // })
+ // .attr('height', args.scales.Y.rangeBand() / 8) //appropriate_size / pp)
+ // .attr('width', function(d) {
+ // return args.scales.X(d[args.predictor_accessor]) - args.scales.X(0);
+ // });
+ // }
+
+ // TODO - reimplement.
+ // if (args.baseline_accessor) {
+
+ // baseline_marks
+ // .attr('x1', function(d) {
+ // return args.scales.X(d[args.baseline_accessor]); })
+ // .attr('x2', function(d) {
+ // return args.scales.X(d[args.baseline_accessor]); })
+ // .attr('y1', function(d) {
+ // return args.scalefns.ygroupf(d) + args.scalefns.yf(d) + args.scales.Y.rangeBand() / 4
+ // })
+ // .attr('y2', function(d) {
+ // return args.scalefns.ygroupf(d) + args.scalefns.yf(d) + args.scales.Y.rangeBand() * 3 / 4
+ // });
+ // }
+ if (args.legend || (args.color_accessor !== null && args.ygroup_accessor !== args.color_accessor)) {
+ if (!args.legend_target) legend_on_graph(svg, args);
+ else mg_targeted_legend(args);
+ }
+ return this;
+ };
+
+ this.markers = () => {
+ markers(args);
+ return this;
+ };
+
+ this.rollover = () => {
+ const svg = mg_get_svg_child_of(args.target);
+ let g;
+
+ if (svg.selectAll('.mg-active-datapoint-container').nodes().length === 0) {
+ mg_add_g(svg, 'mg-active-datapoint-container');
+ }
+
+ //remove the old rollovers if they already exist
+ svg.selectAll('.mg-rollover-rect').remove();
+ svg.selectAll('.mg-active-datapoint').remove();
+
+ // get orientation
+ let length, width, length_type, width_type, length_coord, width_coord,
+ length_scalefn, width_scalefn, length_scale, width_scale,
+ length_accessor, width_accessor;
+
+ let length_coord_map, width_coord_map, length_map, width_map;
+
+ if (args.orientation == 'vertical') {
+ length = 'height';
+ width = 'width';
+ length_type = args.y_axis_type;
+ width_type = args.x_axis_type;
+ length_coord = 'y';
+ width_coord = 'x';
+ length_scalefn = length_type == 'categorical' ? args.scalefns.youtf : args.scalefns.yf;
+ width_scalefn = width_type == 'categorical' ? args.scalefns.xoutf : args.scalefns.xf;
+ length_scale = args.scales.Y;
+ width_scale = args.scales.X;
+ length_accessor = args.y_accessor;
+ width_accessor = args.x_accessor;
+
+ length_coord_map = d => mg_get_plot_top(args);
+
+ length_map = d => args.height -args.top-args.bottom-args.buffer*2;
+ }
+
+ if (args.orientation == 'horizontal') {
+ length = 'width';
+ width = 'height';
+ length_type = args.x_axis_type;
+ width_type = args.y_axis_type;
+ length_coord = 'x';
+ width_coord = 'y';
+ length_scalefn = length_type == 'categorical' ? args.scalefns.xoutf : args.scalefns.xf;
+ width_scalefn = width_type == 'categorical' ? args.scalefns.youtf : args.scalefns.yf;
+ length_scale = args.scales.X;
+ width_scale = args.scales.Y;
+ length_accessor = args.x_accessor;
+ width_accessor = args.y_accessor;
+
+ length_coord_map = d => {
+ let l;
+ l = length_scale(0);
+ return l;
+ };
+
+ length_map = d => args.width -args.left-args.right-args.buffer*2;
+ }
+
+ //rollover text
+ let rollover_x, rollover_anchor;
+ if (args.rollover_align === 'right') {
+ rollover_x = args.width - args.right;
+ rollover_anchor = 'end';
+ } else if (args.rollover_align === 'left') {
+ rollover_x = args.left;
+ rollover_anchor = 'start';
+ } else {
+ rollover_x = (args.width - args.left - args.right) / 2 + args.left;
+ rollover_anchor = 'middle';
+ }
+
+ svg.append('text')
+ .attr('class', 'mg-active-datapoint')
+ .attr('xml:space', 'preserve')
+ .attr('x', rollover_x)
+ .attr('y', args.top * 0.75)
+ .attr('dy', '.35em')
+ .attr('text-anchor', rollover_anchor);
+
+ g = svg.append('g')
+ .attr('class', 'mg-rollover-rect');
+
+ //draw rollover bars
+ const bars = g.selectAll(".mg-bar-rollover")
+ .data(args.data[0]).enter()
+ .append("rect")
+ .attr('class', 'mg-bar-rollover');
+
+ bars.attr('opacity', 0)
+ .attr(length_coord, length_coord_map)
+ .attr(width_coord, d => {
+ let w;
+ if (width_type == 'categorical') {
+ w = width_scalefn(d);
+ } else {
+ w = width_scale(0);
+ if (d[width_accessor] < 0) {
+ w = width_scalefn(d);
+ }
+ }
+ w = w - args.bar_thickness/2;
+ return w;
+ });
+
+ bars.attr(length, length_map);
+ bars.attr(width, d => args.bar_thickness);
+
+ bars
+ .on('mouseover', this.rolloverOn(args))
+ .on('mouseout', this.rolloverOff(args))
+ .on('mousemove', this.rolloverMove(args));
+
+ return this;
+ };
+
+ this.rolloverOn = (args) => {
+ const svg = mg_get_svg_child_of(args.target);
+ const label_accessor = this.is_vertical ? args.x_accessor : args.y_accessor;
+ const data_accessor = this.is_vertical ? args.y_accessor : args.x_accessor;
+ const label_units = this.is_vertical ? args.yax_units : args.xax_units;
+
+ return (d, i) => {
+
+ const fmt = MG.time_format(args.utc_time, '%b %e, %Y');
+ const num = format_rollover_number(args);
+
+ //highlight active bar
+ const bar = svg.selectAll('g.mg-barplot .mg-bar')
+ .filter((d, j) => j === i).classed('active', true);
+
+ if (args.scales.hasOwnProperty('COLOR')) {
+ bar.attr('fill', d3.rgb(args.scalefns.colorf(d)).darker());
+ } else {
+ bar.classed('default-active', true);
+ }
+
+ //update rollover text
+ if (args.show_rollover_text) {
+ const mouseover = mg_mouseover_text(args, { svg });
+ let row = mouseover.mouseover_row();
+
+ if (args.ygroup_accessor) row.text(`${d[args.ygroup_accessor]} `).bold();
+
+ row.text(mg_format_x_mouseover(args, d));
+ row.text(`${args.y_accessor}: ${d[args.y_accessor]}`);
+ if (args.predictor_accessor || args.baseline_accessor) {
+ row = mouseover.mouseover_row();
+
+ if (args.predictor_accessor) row.text(mg_format_data_for_mouseover(args, d, null, args.predictor_accessor, false));
+ if (args.baseline_accessor) row.text(mg_format_data_for_mouseover(args, d, null, args.baseline_accessor, false));
+ }
+ }
+ if (args.mouseover) {
+ args.mouseover(d, i);
+ }
+ };
+ };
+
+ this.rolloverOff = (args) => {
+ const svg = mg_get_svg_child_of(args.target);
+
+ return (d, i) => {
+ //reset active bar
+ const bar = svg.selectAll('g.mg-barplot .mg-bar.active').classed('active', false);
+
+ if (args.scales.hasOwnProperty('COLOR')) {
+ bar.attr('fill', args.scalefns.colorf(d));
+ } else {
+ bar.classed('default-active', false);
+ }
+
+ //reset active data point text
+ svg.select('.mg-active-datapoint')
+ .text('');
+
+ mg_clear_mouseover_container(svg);
+
+ if (args.mouseout) {
+ args.mouseout(d, i);
+ }
+ };
+ };
+
+ this.rolloverMove = (args) => (d, i) => {
+ if (args.mousemove) {
+ args.mousemove(d, i);
+ }
+ };
+
+ this.windowListeners = () => {
+ mg_window_listeners(this.args);
+ return this;
+ };
+
+ this.init(args);
+ }
+
+ const options = {
+ buffer: [16, 'number'],
+ y_accessor: ['factor', 'string'],
+ x_accessor: ['value', 'string'],
+ reference_accessor: [null, 'string'],
+ comparison_accessor: [null, 'string'],
+ secondary_label_accessor: [null, 'string'],
+ color_accessor: [null, 'string'],
+ color_type: ['category', ['number', 'category']],
+ color_domain: [null, 'number[]'],
+ reference_thickness: [1, 'number'],
+ comparison_width: [3, 'number'],
+ comparison_thickness: [null, 'number'],
+ legend: [false, 'boolean'],
+ legend_target: [null, 'string'],
+ mouseover_align: ['right', ['right', 'left']],
+ baseline_accessor: [null, 'string'],
+ predictor_accessor: [null, 'string'],
+ predictor_proportion: [5, 'number'],
+ show_bar_zero: [true, 'boolean'],
+ binned: [true, 'boolean'],
+ truncate_x_labels: [true, 'boolean'],
+ truncate_y_labels: [true, 'boolean']
+ };
+
+ MG.register('bar', barChart, options);
+
+}
diff --git a/priv/static/js/metricsgraphics/charts/histogram.js b/priv/static/js/metricsgraphics/charts/histogram.js
new file mode 100644
index 0000000..3fca8e3
--- /dev/null
+++ b/priv/static/js/metricsgraphics/charts/histogram.js
@@ -0,0 +1,222 @@
+{
+ function histogram(args) {
+ this.init = (args) => {
+ this.args = args;
+
+ raw_data_transformation(args);
+ process_histogram(args);
+ init(args);
+
+ new MG.scale_factory(args)
+ .namespace('x')
+ .numericalDomainFromData()
+ .numericalRange('bottom');
+
+ const baselines = (args.baselines || []).map(d => d[args.y_accessor]);
+
+ new MG.scale_factory(args)
+ .namespace('y')
+ .zeroBottom(true)
+ .inflateDomain(true)
+ .numericalDomainFromData(baselines)
+ .numericalRange('left');
+
+ x_axis(args);
+ y_axis(args);
+
+ this.mainPlot();
+ this.markers();
+ this.rollover();
+ this.windowListeners();
+
+ return this;
+ };
+
+ this.mainPlot = () => {
+ const svg = mg_get_svg_child_of(args.target);
+
+ //remove the old histogram, add new one
+ svg.selectAll('.mg-histogram').remove();
+
+ const g = svg.append('g')
+ .attr('class', 'mg-histogram');
+
+ const bar = g.selectAll('.mg-bar')
+ .data(args.data[0])
+ .enter().append('g')
+ .attr('class', 'mg-bar')
+ .attr('transform', d => `translate(${args.scales.X(d[args.x_accessor]).toFixed(2)},${args.scales.Y(d[args.y_accessor]).toFixed(2)})`);
+
+ //draw bars
+ bar.append('rect')
+ .attr('x', 1)
+ .attr('width', (d, i) => {
+ if (args.data[0].length === 1) {
+ return (args.scalefns.xf(args.data[0][0]) - args.bar_margin).toFixed(0);
+ } else if (i !== args.data[0].length - 1) {
+ return (args.scalefns.xf(args.data[0][i + 1]) - args.scalefns.xf(d)).toFixed(0);
+ } else {
+ return (args.scalefns.xf(args.data[0][1]) - args.scalefns.xf(args.data[0][0])).toFixed(0);
+ }
+ })
+ .attr('height', d => {
+ if (d[args.y_accessor] === 0) {
+ return 0;
+ }
+
+ return (args.height - args.bottom - args.buffer - args.scales.Y(d[args.y_accessor])).toFixed(2);
+ });
+
+ return this;
+ };
+
+ this.markers = () => {
+ markers(args);
+ return this;
+ };
+
+ this.rollover = () => {
+ const svg = mg_get_svg_child_of(args.target);
+
+ if (svg.selectAll('.mg-active-datapoint-container').nodes().length === 0) {
+ mg_add_g(svg, 'mg-active-datapoint-container');
+ }
+
+ //remove the old rollovers if they already exist
+ svg.selectAll('.mg-rollover-rect').remove();
+ svg.selectAll('.mg-active-datapoint').remove();
+
+ const g = svg.append('g')
+ .attr('class', 'mg-rollover-rect');
+
+ //draw rollover bars
+ const bar = g.selectAll('.mg-bar')
+ .data(args.data[0])
+ .enter().append('g')
+ .attr('class', (d, i) => {
+ if (args.linked) {
+ return `mg-rollover-rects roll_${i}`;
+ } else {
+ return 'mg-rollover-rects';
+ }
+ })
+ .attr('transform', d => `translate(${args.scales.X(d[args.x_accessor])},${0})`);
+
+ bar.append('rect')
+ .attr('x', 1)
+ .attr('y', args.buffer + (args.title ? args.title_y_position : 0))
+ .attr('width', (d, i) => {
+ //if data set is of length 1
+ if (args.data[0].length === 1) {
+ return (args.scalefns.xf(args.data[0][0]) - args.bar_margin).toFixed(0);
+ } else if (i !== args.data[0].length - 1) {
+ return (args.scalefns.xf(args.data[0][i + 1]) - args.scalefns.xf(d)).toFixed(0);
+ } else {
+ return (args.scalefns.xf(args.data[0][1]) - args.scalefns.xf(args.data[0][0])).toFixed(0);
+ }
+ })
+ .attr('height', d => args.height)
+ .attr('opacity', 0)
+ .on('mouseover', this.rolloverOn(args))
+ .on('mouseout', this.rolloverOff(args))
+ .on('mousemove', this.rolloverMove(args));
+
+ return this;
+ };
+
+ this.rolloverOn = (args) => {
+ const svg = mg_get_svg_child_of(args.target);
+
+ return (d, i) => {
+ svg.selectAll('text')
+ .filter((g, j) => d === g)
+ .attr('opacity', 0.3);
+
+ const fmt = args.processed.xax_format || MG.time_format(args.utc_time, '%b %e, %Y');
+ const num = format_rollover_number(args);
+
+ svg.selectAll('.mg-bar rect')
+ .filter((d, j) => j === i)
+ .classed('active', true);
+
+ //trigger mouseover on all matching bars
+ if (args.linked && !MG.globals.link) {
+ MG.globals.link = true;
+
+ //trigger mouseover on matching bars in .linked charts
+ d3.selectAll(`.mg-rollover-rects.roll_${i} rect`)
+ .each(function(d) { //use existing i
+ d3.select(this).on('mouseover')(d, i);
+ });
+ }
+
+ //update rollover text
+ if (args.show_rollover_text) {
+ const mo = mg_mouseover_text(args, { svg });
+ const row = mo.mouseover_row();
+ row.text('\u259F ').elem
+ .classed('hist-symbol', true);
+
+ row.text(mg_format_x_mouseover(args, d)); // x
+ row.text(mg_format_y_mouseover(args, d, args.time_series === false));
+ }
+
+ if (args.mouseover) {
+ mg_setup_mouseover_container(svg, args);
+ args.mouseover(d, i);
+ }
+ };
+ };
+
+ this.rolloverOff = (args) => {
+ const svg = mg_get_svg_child_of(args.target);
+
+ return (d, i) => {
+ if (args.linked && MG.globals.link) {
+ MG.globals.link = false;
+
+ //trigger mouseout on matching bars in .linked charts
+ d3.selectAll(`.mg-rollover-rects.roll_${i} rect`)
+ .each(function(d) { //use existing i
+ d3.select(this).on('mouseout')(d, i);
+ });
+ }
+
+ //reset active bar
+ svg.selectAll('.mg-bar rect')
+ .classed('active', false);
+
+ //reset active data point text
+ mg_clear_mouseover_container(svg);
+
+ if (args.mouseout) {
+ args.mouseout(d, i);
+ }
+ };
+ };
+
+ this.rolloverMove = (args) => (d, i) => {
+ if (args.mousemove) {
+ args.mousemove(d, i);
+ }
+ };
+
+ this.windowListeners = () => {
+ mg_window_listeners(this.args);
+ return this;
+ };
+
+ this.init(args);
+ }
+
+ const options = {
+ bar_margin: [1, "number"], // the margin between bars
+ binned: [false, "boolean"], // determines whether the data is already binned
+ bins: [null, ['number', 'number[]', 'function']], // the number of bins to use. type: {null, number | thresholds | threshold_function}
+ processed_x_accessor: ['x', 'string'],
+ processed_y_accessor: ['y', 'string'],
+ processed_dx_accessor: ['dx', 'string']
+ };
+
+ MG.register('histogram', histogram, options);
+}
diff --git a/priv/static/js/metricsgraphics/charts/line.js b/priv/static/js/metricsgraphics/charts/line.js
new file mode 100644
index 0000000..f441af3
--- /dev/null
+++ b/priv/static/js/metricsgraphics/charts/line.js
@@ -0,0 +1,922 @@
+{
+ function mg_line_color_text(elem, line_id, {color, colors}) {
+ elem.classed('mg-hover-line-color', color === null)
+ .classed(`mg-hover-line${line_id}-color`, colors === null)
+ .attr('fill', colors === null ? '' : colors[line_id - 1]);
+ }
+
+ function mg_line_graph_generators(args, plot, svg) {
+ mg_add_line_generator(args, plot);
+ mg_add_area_generator(args, plot);
+ mg_add_flat_line_generator(args, plot);
+ mg_add_confidence_band_generator(args, plot, svg);
+ }
+
+ function mg_add_confidence_band_generator(args, plot, svg) {
+ plot.existing_band = svg.selectAll('.mg-confidence-band').nodes();
+ if (args.show_confidence_band) {
+ plot.confidence_area = d3.area()
+ .defined(plot.line.defined())
+ .x(args.scalefns.xf)
+ .y0(d => {
+ const l = args.show_confidence_band[0];
+ if (d[l] != undefined) {
+ return args.scales.Y(d[l]);
+ } else {
+ return args.scales.Y(d[args.y_accessor]);
+ }
+ })
+ .y1(d => {
+ const u = args.show_confidence_band[1];
+ if (d[u] != undefined) {
+ return args.scales.Y(d[u]);
+ } else {
+ return args.scales.Y(d[args.y_accessor]);
+ }
+ })
+ .curve(args.interpolate);
+ }
+ }
+
+ function mg_add_area_generator({scalefns, scales, interpolate, flip_area_under_y_value}, plot) {
+
+ const areaBaselineValue = (Number.isFinite(flip_area_under_y_value)) ? scales.Y(flip_area_under_y_value) : scales.Y.range()[0];
+
+ plot.area = d3.area()
+ .defined(plot.line.defined())
+ .x(scalefns.xf)
+ .y0(() => {
+ return areaBaselineValue;
+ })
+ .y1(scalefns.yf)
+ .curve(interpolate);
+ }
+
+ function mg_add_flat_line_generator({y_accessor, scalefns, scales, interpolate}, plot) {
+ plot.flat_line = d3.line()
+ .defined(d => (d['_missing'] === undefined || d['_missing'] !== true) && d[y_accessor] !== null)
+ .x(scalefns.xf)
+ .y(() => scales.Y(plot.data_median))
+ .curve(interpolate);
+ }
+
+ function mg_add_line_generator({scalefns, interpolate, missing_is_zero, y_accessor}, plot) {
+ plot.line = d3.line()
+ .x(scalefns.xf)
+ .y(scalefns.yf)
+ .curve(interpolate);
+
+ // if missing_is_zero is not set, then hide data points that fall in missing
+ // data ranges or that have been explicitly identified as missing in the
+ // data source.
+ if (!missing_is_zero) {
+ // a line is defined if the _missing attrib is not set to true
+ // and the y-accessor is not null
+ plot.line = plot.line.defined(d => (d['_missing'] === undefined || d['_missing'] !== true) && d[y_accessor] !== null);
+ }
+ }
+
+ function mg_add_confidence_band(
+ {show_confidence_band, transition_on_update, data, target},
+ plot,
+ svg,
+ which_line
+ ) {
+ if (show_confidence_band) {
+ let confidenceBand;
+ if (svg.select(`.mg-confidence-band-${which_line}`).empty()) {
+ svg.append('path')
+ .attr('class', `mg-confidence-band mg-confidence-band-${which_line}`);
+ }
+
+ // transition this line's confidence band
+ confidenceBand = svg.select(`.mg-confidence-band-${which_line}`);
+
+ confidenceBand
+ .transition()
+ .duration(() => (transition_on_update) ? 1000 : 0)
+ .attr('d', plot.confidence_area(data[which_line - 1]))
+ .attr('clip-path', `url(#mg-plot-window-${mg_target_ref(target)})`);
+ }
+ }
+
+ function mg_add_area({data, target, colors}, plot, svg, which_line, line_id) {
+ const areas = svg.selectAll(`.mg-main-area.mg-area${line_id}`);
+ if (plot.display_area) {
+ // if area already exists, transition it
+ if (!areas.empty()) {
+ svg.node().appendChild(areas.node());
+
+ areas.transition()
+ .duration(plot.update_transition_duration)
+ .attr('d', plot.area(data[which_line]))
+ .attr('clip-path', `url(#mg-plot-window-${mg_target_ref(target)})`);
+ } else { // otherwise, add the area
+ svg.append('path')
+ .classed('mg-main-area', true)
+ .classed(`mg-area${line_id}`, true)
+ .classed('mg-area-color', colors === null)
+ .classed(`mg-area${line_id}-color`, colors === null)
+ .attr('d', plot.area(data[which_line]))
+ .attr('fill', colors === null ? '' : colors[line_id - 1])
+ .attr('clip-path', `url(#mg-plot-window-${mg_target_ref(target)})`);
+ }
+ } else if (!areas.empty()) {
+ areas.remove();
+ }
+ }
+
+ function mg_default_color_for_path(this_path, line_id) {
+ this_path.classed('mg-line-color', true)
+ .classed(`mg-line${line_id}-color`, true);
+ }
+
+ function mg_color_line({colors}, this_path, which_line, line_id) {
+ if (colors) {
+ // for now, if args.colors is not an array, then keep moving as if nothing happened.
+ // if args.colors is not long enough, default to the usual line_id color.
+ if (colors.constructor === Array) {
+ this_path.attr('stroke', colors[which_line]);
+ if (colors.length < which_line + 1) {
+ // Go with default coloring.
+ // this_path.classed('mg-line' + (line_id) + '-color', true);
+ mg_default_color_for_path(this_path, line_id);
+ }
+ } else {
+ // this_path.classed('mg-line' + (line_id) + '-color', true);
+ mg_default_color_for_path(this_path, line_id);
+ }
+ } else {
+ // this is the typical workflow
+ // this_path.classed('mg-line' + (line_id) + '-color', true);
+ mg_default_color_for_path(this_path, line_id);
+ }
+ }
+
+ function mg_add_line_element({animate_on_load, data, y_accessor, target}, plot, this_path, which_line) {
+ if (animate_on_load) {
+ plot.data_median = d3.median(data[which_line], d => d[y_accessor]);
+ this_path.attr('d', plot.flat_line(data[which_line]))
+ .transition()
+ .duration(1000)
+ .attr('d', plot.line(data[which_line]))
+ .attr('clip-path', `url(#mg-plot-window-${mg_target_ref(target)})`);
+ } else { // or just add the line
+ this_path.attr('d', plot.line(data[which_line]))
+ .attr('clip-path', `url(#mg-plot-window-${mg_target_ref(target)})`);
+ }
+ }
+
+ function mg_add_line(args, plot, svg, existing_line, which_line, line_id) {
+ if (!existing_line.empty()) {
+ svg.node().appendChild(existing_line.node());
+
+ const lineTransition = existing_line.transition()
+ .duration(plot.update_transition_duration);
+
+ if (!plot.display_area && args.transition_on_update && !args.missing_is_hidden) {
+ lineTransition.attrTween('d', path_tween(plot.line(args.data[which_line]), 4));
+ } else {
+ lineTransition.attr('d', plot.line(args.data[which_line]));
+ }
+ } else { // otherwise...
+ // if we're animating on load, animate the line from its median value
+ const this_path = svg.append('path')
+ .attr('class', `mg-main-line mg-line${line_id}`);
+
+ mg_color_line(args, this_path, which_line, line_id);
+ mg_add_line_element(args, plot, this_path, which_line);
+ }
+ }
+
+ function mg_add_legend_element(args, plot, which_line, line_id) {
+ let this_legend;
+ if (args.legend) {
+ if (is_array(args.legend)) {
+ this_legend = args.legend[which_line];
+ } else if (is_function(args.legend)) {
+ this_legend = args.legend(args.data[which_line]);
+ }
+
+ if (args.legend_target) {
+ if (args.colors && args.colors.constructor === Array) {
+ plot.legend_text = `<span style='color:${args.colors[which_line]}'>&mdash; ${this_legend}&nbsp; </span>${plot.legend_text}`;
+ } else {
+ plot.legend_text = `<span class='mg-line${line_id}-legend-color'>&mdash; ${this_legend}&nbsp; </span>${plot.legend_text}`;
+ }
+ } else {
+ let anchor_point, anchor_orientation, dx;
+
+ if (args.y_axis_position === 'left') {
+ anchor_point = args.data[which_line][args.data[which_line].length - 1];
+ anchor_orientation = 'start';
+ dx = args.buffer;
+ } else {
+ anchor_point = args.data[which_line][0];
+ anchor_orientation = 'end';
+ dx = -args.buffer;
+ }
+ const legend_text = plot.legend_group.append('svg:text')
+ .attr('x', args.scalefns.xf(anchor_point))
+ .attr('dx', dx)
+ .attr('y', args.scalefns.yf(anchor_point))
+ .attr('dy', '.35em')
+ .attr('font-size', 10)
+ .attr('text-anchor', anchor_orientation)
+ .attr('font-weight', '300')
+ .text(this_legend);
+
+ if (args.colors && args.colors.constructor === Array) {
+ if (args.colors.length < which_line + 1) {
+ legend_text.classed(`mg-line${line_id}-legend-color`, true);
+ } else {
+ legend_text.attr('fill', args.colors[which_line]);
+ }
+ } else {
+ legend_text.classed('mg-line-legend-color', true)
+ .classed(`mg-line${line_id}-legend-color`, true);
+ }
+
+ mg_prevent_vertical_overlap(plot.legend_group.selectAll('.mg-line-legend text').nodes(), args);
+ }
+ }
+ }
+
+ function mg_plot_legend_if_legend_target(target, legend) {
+ if (target) d3.select(target).html(legend);
+ }
+
+ function mg_add_legend_group({legend}, plot, svg) {
+ if (legend) plot.legend_group = mg_add_g(svg, 'mg-line-legend');
+ }
+
+ function mg_remove_existing_line_rollover_elements(svg) {
+ // remove the old rollovers if they already exist
+ mg_selectAll_and_remove(svg, '.mg-rollover-rect');
+ mg_selectAll_and_remove(svg, '.mg-voronoi');
+
+ // remove the old rollover text and circle if they already exist
+ mg_selectAll_and_remove(svg, '.mg-active-datapoint');
+ mg_selectAll_and_remove(svg, '.mg-line-rollover-circle');
+ //mg_selectAll_and_remove(svg, '.mg-active-datapoint-container');
+ }
+
+ function mg_add_rollover_circle({data, colors}, svg) {
+ // append circle
+ const circle = svg.selectAll('.mg-line-rollover-circle')
+ .data(data)
+ .enter().append('circle')
+ .attr('cx', 0)
+ .attr('cy', 0)
+ .attr('r', 0);
+
+ if (colors && colors.constructor === Array) {
+ circle
+ .attr('class', ({__line_id__}) => `mg-line${__line_id__}`)
+ .attr('fill', (d, i) => colors[i])
+ .attr('stroke', (d, i) => colors[i]);
+ } else {
+ circle.attr('class', ({__line_id__}, i) => [
+ `mg-line${__line_id__}`,
+ `mg-line${__line_id__}-color`,
+ `mg-area${__line_id__}-color`
+ ].join(' '));
+ }
+ circle.classed('mg-line-rollover-circle', true);
+ }
+
+ function mg_set_unique_line_id_for_each_series({data, custom_line_color_map}) {
+ // update our data by setting a unique line id for each series
+ // increment from 1... unless we have a custom increment series
+
+ for (let i = 0; i < data.length; i++) {
+ data[i].forEach(datum => {
+ datum.__index__ = i + 1;
+ datum.__line_id__ = (custom_line_color_map.length > 0) ? custom_line_color_map[i] : i + 1;
+ });
+ }
+ }
+
+ function mg_nest_data_for_voronoi({data}) {
+ return d3.merge(data);
+ }
+
+ function mg_line_class_string(args) {
+ return d => {
+ let class_string;
+
+ if (args.linked) {
+ const v = d[args.x_accessor];
+ const formatter = MG.time_format(args.utc_time, args.linked_format);
+
+ // only format when x-axis is date
+ const id = (typeof v === 'number') ? (d.__line_id__ - 1) : formatter(v);
+ class_string = `roll_${id} mg-line${d.__line_id__}`;
+
+ if (args.color === null) {
+ class_string += ` mg-line${d.__line_id__}-color`;
+ }
+ return class_string;
+
+ } else {
+ class_string = `mg-line${d.__line_id__}`;
+ if (args.color === null) class_string += ` mg-line${d.__line_id__}-color`;
+ return class_string;
+ }
+ };
+ }
+
+ function mg_add_voronoi_rollover(args, svg, rollover_on, rollover_off, rollover_move, rollover_click) {
+ const voronoi = d3.voronoi()
+ .x(d => args.scales.X(d[args.x_accessor]).toFixed(2))
+ .y(d => args.scales.Y(d[args.y_accessor]).toFixed(2))
+ .extent([
+ [args.buffer, args.buffer + (args.title ? args.title_y_position : 0)],
+ [args.width - args.buffer, args.height - args.buffer]
+ ]);
+
+ const g = mg_add_g(svg, 'mg-voronoi');
+ g.selectAll('path')
+ .data(voronoi.polygons(mg_nest_data_for_voronoi(args)))
+ .enter()
+ .append('path')
+ .filter(d => d !== undefined && d.length > 0)
+ .attr('d', d => d == null ? null : `M${d.join('L')}Z`)
+ .datum(d => d == null ? null : d.data) // because of d3.voronoi, reassign d
+ .attr('class', mg_line_class_string(args))
+ .on('click', rollover_click)
+ .on('mouseover', rollover_on)
+ .on('mouseout', rollover_off)
+ .on('mousemove', rollover_move);
+
+ mg_configure_voronoi_rollover(args, svg);
+ }
+
+ function nest_data_for_aggregate_rollover({x_accessor, data, x_sort}) {
+ const data_nested = d3.nest()
+ .key(d => d[x_accessor])
+ .entries(d3.merge(data));
+ data_nested.forEach(entry => {
+ const datum = entry.values[0];
+ entry.key = datum[x_accessor];
+ });
+
+ if (x_sort) {
+ return data_nested.sort((a, b) => new Date(a.key) - new Date(b.key));
+ } else {
+ return data_nested;
+ }
+ }
+
+ function mg_add_aggregate_rollover(args, svg, rollover_on, rollover_off, rollover_move, rollover_click) {
+ // Undo the keys getting coerced to strings, by setting the keys from the values
+ // This is necessary for when we have X axis keys that are things like
+ const data_nested = nest_data_for_aggregate_rollover(args);
+
+ const xf = data_nested.map(({key}) => args.scales.X(key));
+
+ const g = svg.append('g')
+ .attr('class', 'mg-rollover-rect');
+
+ g.selectAll('.mg-rollover-rects')
+ .data(data_nested).enter()
+ .append('rect')
+ .attr('x', (d, i) => {
+ if (xf.length === 1) return mg_get_plot_left(args);
+ else if (i === 0) return xf[i].toFixed(2);
+ else return ((xf[i - 1] + xf[i]) / 2).toFixed(2);
+ })
+ .attr('y', args.top)
+ .attr('width', (d, i) => {
+ if (xf.length === 1) return mg_get_plot_right(args);
+ else if (i === 0) return ((xf[i + 1] - xf[i]) / 2).toFixed(2);
+ else if (i === xf.length - 1) return ((xf[i] - xf[i - 1]) / 2).toFixed(2);
+ else return ((xf[i + 1] - xf[i - 1]) / 2).toFixed(2);
+ })
+ .attr('class', ({values}) => {
+ let line_classes = values.map(({__line_id__}) => {
+ let lc = mg_line_class(__line_id__);
+ if (args.colors === null) lc += ` ${mg_line_color_class(__line_id__)}`;
+ return lc;
+ }).join(' ');
+ if (args.linked && values.length > 0) {
+ line_classes += ` ${mg_rollover_id_class(mg_rollover_format_id(values[0], args))}`;
+ }
+
+ return line_classes;
+ })
+ .attr('height', args.height - args.bottom - args.top - args.buffer)
+ .attr('opacity', 0)
+ .on('click', rollover_click)
+ .on('mouseover', rollover_on)
+ .on('mouseout', rollover_off)
+ .on('mousemove', rollover_move);
+
+ mg_configure_aggregate_rollover(args, svg);
+ }
+
+ function mg_configure_singleton_rollover({data}, svg) {
+ svg.select('.mg-rollover-rect rect')
+ .on('mouseover')(data[0][0], 0);
+ }
+
+ function mg_configure_voronoi_rollover({data, custom_line_color_map}, svg) {
+ for (let i = 0; i < data.length; i++) {
+ let j = i + 1;
+
+ if (custom_line_color_map.length > 0 &&
+ custom_line_color_map[i] !== undefined) {
+ j = custom_line_color_map[i];
+ }
+
+ if (data[i].length === 1 && !svg.selectAll(`.mg-voronoi .mg-line${j}`).empty()) {
+ svg.selectAll(`.mg-voronoi .mg-line${j}`)
+ .on('mouseover')(data[i][0], 0);
+
+ svg.selectAll(`.mg-voronoi .mg-line${j}`)
+ .on('mouseout')(data[i][0], 0);
+ }
+ }
+ }
+
+ function mg_line_class(line_id) {
+ return `mg-line${line_id}`;
+ }
+
+ function mg_line_color_class(line_id) {
+ return `mg-line${line_id}-color`;
+ }
+
+ function mg_rollover_id_class(id) {
+ return `roll_${id}`;
+ }
+
+ function mg_rollover_format_id(d, {x_accessor, utc_time, linked_format}) {
+ const v = d[x_accessor];
+ const formatter = MG.time_format(utc_time, linked_format);
+ // only format when x-axis is date
+ return (typeof v === 'number') ? v.toString().replace('.', '_') : formatter(v);
+ }
+
+ function mg_add_single_line_rollover(args, svg, rollover_on, rollover_off, rollover_move, rollover_click) {
+ // set to 1 unless we have a custom increment series
+ let line_id = 1;
+ if (args.custom_line_color_map.length > 0) {
+ line_id = args.custom_line_color_map[0];
+ }
+
+ const g = svg.append('g')
+ .attr('class', 'mg-rollover-rect');
+
+ const xf = args.data[0].map(args.scalefns.xf);
+
+ g.selectAll('.mg-rollover-rects')
+ .data(args.data[0]).enter()
+ .append('rect')
+ .attr('class', (d, i) => {
+ let cl = `${mg_line_color_class(line_id)} ${mg_line_class(d.__line_id__)}`;
+ if (args.linked) cl += `${cl} ${mg_rollover_id_class(mg_rollover_format_id(d, args))}`;
+ return cl;
+ })
+ .attr('x', (d, i) => {
+ // if data set is of length 1
+ if (xf.length === 1) return mg_get_plot_left(args);
+ else if (i === 0) return xf[i].toFixed(2);
+ else return ((xf[i - 1] + xf[i]) / 2).toFixed(2);
+ })
+ .attr('y', (d, i) => (args.data.length > 1) ? args.scalefns.yf(d) - 6 // multi-line chart sensitivity
+ : args.top)
+ .attr('width', (d, i) => {
+ // if data set is of length 1
+ if (xf.length === 1) return mg_get_plot_right(args);
+ else if (i === 0) return ((xf[i + 1] - xf[i]) / 2).toFixed(2);
+ else if (i === xf.length - 1) return ((xf[i] - xf[i - 1]) / 2).toFixed(2);
+ else return ((xf[i + 1] - xf[i - 1]) / 2).toFixed(2);
+ })
+ .attr('height', (d, i) => (args.data.length > 1) ? 12 // multi-line chart sensitivity
+ : args.height - args.bottom - args.top - args.buffer)
+ .attr('opacity', 0)
+ .on('click', rollover_click)
+ .on('mouseover', rollover_on)
+ .on('mouseout', rollover_off)
+ .on('mousemove', rollover_move);
+
+ if (mg_is_singleton(args)) {
+ mg_configure_singleton_rollover(args, svg);
+ }
+ }
+
+ function mg_configure_aggregate_rollover({data}, svg) {
+ const rect = svg.selectAll('.mg-rollover-rect rect');
+ const rect_first = rect.nodes()[0][0] || rect.nodes()[0];
+ if (data.filter(({length}) => length === 1).length > 0) {
+ rect.on('mouseover')(rect_first.__data__, 0);
+ }
+ }
+
+ function mg_is_standard_multiline({data, aggregate_rollover}) {
+ return data.length > 1 && !aggregate_rollover;
+ }
+
+ function mg_is_aggregated_rollover({data, aggregate_rollover}) {
+ return data.length > 1 && aggregate_rollover;
+ }
+
+ function mg_is_singleton({data}) {
+ return data.length === 1 && data[0].length === 1;
+ }
+
+ function mg_draw_all_line_elements(args, plot, svg) {
+ mg_remove_dangling_bands(plot, svg);
+
+ // If option activated, remove existing active points if exists
+ if (args.active_point_on_lines) {
+ svg.selectAll('circle.mg-shown-active-point').remove();
+ }
+
+ for (let i = args.data.length - 1; i >= 0; i--) {
+ const this_data = args.data[i];
+
+ // passing the data for the current line
+ MG.call_hook('line.before_each_series', [this_data, args]);
+
+ // override increment if we have a custom increment series
+ let line_id = i + 1;
+ if (args.custom_line_color_map.length > 0) {
+ line_id = args.custom_line_color_map[i];
+ }
+
+ args.data[i].__line_id__ = line_id;
+
+ // If option activated, add active points for each lines
+ if (args.active_point_on_lines) {
+ svg.selectAll('circle-' + line_id)
+ .data(args.data[i])
+ .enter()
+ .filter((d) => {
+ return d[args.active_point_accessor];
+ })
+ .append('circle')
+ .attr('class', 'mg-area' + (line_id) + '-color mg-shown-active-point')
+ .attr('cx', args.scalefns.xf)
+ .attr('cy', args.scalefns.yf)
+ .attr('r', () => {
+ return args.active_point_size;
+ });
+ }
+
+ const existing_line = svg.select(`path.mg-main-line.mg-line${line_id}`);
+ if (this_data.length === 0) {
+ existing_line.remove();
+ continue;
+ }
+
+ mg_add_confidence_band(args, plot, svg, line_id);
+
+ if (Array.isArray(args.area)) {
+ if (args.area[line_id - 1]) {
+ mg_add_area(args, plot, svg, i, line_id);
+ }
+ } else {
+ mg_add_area(args, plot, svg, i, line_id);
+ }
+
+ mg_add_line(args, plot, svg, existing_line, i, line_id);
+ mg_add_legend_element(args, plot, i, line_id);
+
+ // passing the data for the current line
+ MG.call_hook('line.after_each_series', [this_data, existing_line, args]);
+ }
+ }
+
+ function mg_remove_dangling_bands({existing_band}, svg) {
+ if (existing_band[0] && existing_band[0].length > svg.selectAll('.mg-main-line').node().length) {
+ svg.selectAll('.mg-confidence-band').remove();
+ }
+ }
+
+ function mg_line_main_plot(args) {
+ const plot = {};
+ const svg = mg_get_svg_child_of(args.target);
+
+ // remove any old legends if they exist
+ mg_selectAll_and_remove(svg, '.mg-line-legend');
+ mg_add_legend_group(args, plot, svg);
+
+ plot.data_median = 0;
+ plot.update_transition_duration = (args.transition_on_update) ? 1000 : 0;
+ plot.display_area = (args.area && !args.use_data_y_min && args.data.length <= 1 && args.aggregate_rollover === false) || (Array.isArray(args.area) && args.area.length > 0);
+ plot.legend_text = '';
+ mg_line_graph_generators(args, plot, svg);
+ plot.existing_band = svg.selectAll('.mg-confidence-band').nodes();
+
+ // should we continue with the default line render? A `line.all_series` hook should return false to prevent the default.
+ const continueWithDefault = MG.call_hook('line.before_all_series', [args]);
+ if (continueWithDefault !== false) {
+ mg_draw_all_line_elements(args, plot, svg);
+ }
+
+ mg_plot_legend_if_legend_target(args.legend_target, plot.legend_text);
+ }
+
+ function mg_line_rollover_setup(args, graph) {
+ const svg = mg_get_svg_child_of(args.target);
+
+ if (args.showActivePoint && svg.selectAll('.mg-active-datapoint-container').nodes().length === 0) {
+ mg_add_g(svg, 'mg-active-datapoint-container');
+ }
+
+ mg_remove_existing_line_rollover_elements(svg);
+ mg_add_rollover_circle(args, svg);
+ mg_set_unique_line_id_for_each_series(args);
+
+ if (mg_is_standard_multiline(args)) {
+ mg_add_voronoi_rollover(args, svg, graph.rolloverOn(args), graph.rolloverOff(args), graph.rolloverMove(args), graph.rolloverClick(args));
+ } else if (mg_is_aggregated_rollover(args)) {
+ mg_add_aggregate_rollover(args, svg, graph.rolloverOn(args), graph.rolloverOff(args), graph.rolloverMove(args), graph.rolloverClick(args));
+ } else {
+ mg_add_single_line_rollover(args, svg, graph.rolloverOn(args), graph.rolloverOff(args), graph.rolloverMove(args), graph.rolloverClick(args));
+ }
+ }
+
+ function mg_update_rollover_circle(args, svg, d) {
+ if (args.aggregate_rollover && args.data.length > 1) {
+ // hide the circles in case a non-contiguous series is present
+ svg.selectAll('circle.mg-line-rollover-circle')
+ .style('opacity', 0);
+
+ d.values.forEach((datum, index, list) => {
+ if (args.missing_is_hidden && list[index]['_missing']) {
+ return;
+ }
+
+ if (mg_data_in_plot_bounds(datum, args)) mg_update_aggregate_rollover_circle(args, svg, datum);
+ });
+ } else if ((args.missing_is_hidden && d['_missing']) || d[args.y_accessor] === null) {
+ // disable rollovers for hidden parts of the line
+ // recall that hidden parts are missing data ranges and possibly also
+ // data points that have been explicitly identified as missing
+ return;
+ } else {
+ // show circle on mouse-overed rect
+ if (mg_data_in_plot_bounds(d, args)) {
+ mg_update_generic_rollover_circle(args, svg, d);
+ }
+ }
+ }
+
+ function mg_update_aggregate_rollover_circle({scales, x_accessor, y_accessor, point_size}, svg, datum) {
+ svg.select(`circle.mg-line-rollover-circle.mg-line${datum.__line_id__}`)
+ .attr('cx', scales.X(datum[x_accessor]).toFixed(2))
+ .attr('cy', scales.Y(datum[y_accessor]).toFixed(2))
+ .attr('r', point_size)
+ .style('opacity', 1);
+ }
+
+ function mg_update_generic_rollover_circle({scales, x_accessor, y_accessor, point_size}, svg, d) {
+ svg.selectAll(`circle.mg-line-rollover-circle.mg-line${d.__line_id__}`)
+ .classed('mg-line-rollover-circle', true)
+ .attr('cx', () => scales.X(d[x_accessor]).toFixed(2))
+ .attr('cy', () => scales.Y(d[y_accessor]).toFixed(2))
+ .attr('r', point_size)
+ .style('opacity', 1);
+ }
+
+ function mg_trigger_linked_mouseovers(args, d, i) {
+ if (args.linked && !MG.globals.link) {
+ MG.globals.link = true;
+ if (!args.aggregate_rollover || d[args.y_accessor] !== undefined || (d.values && d.values.length > 0)) {
+ const datum = d.values ? d.values[0] : d;
+ const id = mg_rollover_format_id(datum, args);
+ // trigger mouseover on matching line in .linked charts
+ d3.selectAll(`.${mg_line_class(datum.__line_id__)}.${mg_rollover_id_class(id)}`)
+ .each(function(d) {
+ d3.select(this)
+ .on('mouseover')(d, i);
+ });
+ }
+ }
+ }
+
+ function mg_trigger_linked_mouseouts({linked, utc_time, linked_format, x_accessor}, d, i) {
+ if (linked && MG.globals.link) {
+ MG.globals.link = false;
+
+ const formatter = MG.time_format(utc_time, linked_format);
+ const datums = d.values ? d.values : [d];
+ datums.forEach(datum => {
+ const v = datum[x_accessor];
+ const id = (typeof v === 'number') ? i : formatter(v);
+
+ // trigger mouseout on matching line in .linked charts
+ d3.selectAll(`.roll_${id}`)
+ .each(function(d) {
+ d3.select(this)
+ .on('mouseout')(d);
+ });
+ });
+ }
+ }
+
+ function mg_remove_active_data_points_for_aggregate_rollover(args, svg) {
+ svg.selectAll('circle.mg-line-rollover-circle').filter(({length}) => length > 1)
+ .style('opacity', 0);
+ }
+
+ function mg_remove_active_data_points_for_generic_rollover({custom_line_color_map, data}, svg, line_id) {
+ svg.selectAll(`circle.mg-line-rollover-circle.mg-line${line_id}`)
+ .style('opacity', () => {
+ let id = line_id - 1;
+ if (custom_line_color_map.length > 0 &&
+ custom_line_color_map.indexOf(line_id) !== undefined
+ ) {
+ id = custom_line_color_map.indexOf(line_id);
+ }
+
+ if (data[id].length === 1) {
+ return 1;
+ } else {
+ return 0;
+ }
+ });
+ }
+
+ function mg_remove_active_text(svg) {
+ svg.select('.mg-active-datapoint').text('');
+ }
+
+ function lineChart(args) {
+ this.init = function(args) {
+ this.args = args;
+
+ if (!args.data || args.data.length === 0) {
+ args.internal_error = 'No data was supplied';
+ internal_error(args);
+ return this;
+ } else {
+ args.internal_error = undefined;
+ }
+
+ raw_data_transformation(args);
+ process_line(args);
+
+ MG.call_hook('line.before_destroy', this);
+
+ init(args);
+
+ // TODO incorporate markers into calculation of x scales
+ new MG.scale_factory(args)
+ .namespace('x')
+ .numericalDomainFromData()
+ .numericalRange('bottom');
+
+ const baselines = (args.baselines || []).map(d => d[args.y_accessor]);
+
+ new MG.scale_factory(args)
+ .namespace('y')
+ .zeroBottom(true)
+ .inflateDomain(true)
+ .numericalDomainFromData(baselines)
+ .numericalRange('left');
+
+ if (args.x_axis) {
+ new MG.axis_factory(args)
+ .namespace('x')
+ .type('numerical')
+ .position(args.x_axis_position)
+ .rug(x_rug(args))
+ .label(mg_add_x_label)
+ .draw();
+ }
+
+ if (args.y_axis) {
+ new MG.axis_factory(args)
+ .namespace('y')
+ .type('numerical')
+ .position(args.y_axis_position)
+ .rug(y_rug(args))
+ .label(mg_add_y_label)
+ .draw();
+ }
+
+ this.markers();
+ this.mainPlot();
+ this.rollover();
+ this.windowListeners();
+ if (args.brush) MG.add_brush_function(args);
+ MG.call_hook('line.after_init', this);
+
+ return this;
+ };
+
+ this.mainPlot = function() {
+ mg_line_main_plot(args);
+ return this;
+ };
+
+ this.markers = function() {
+ markers(args);
+ return this;
+ };
+
+ this.rollover = function() {
+ mg_line_rollover_setup(args, this);
+ MG.call_hook('line.after_rollover', args);
+
+ return this;
+ };
+
+ this.rolloverClick = args => (d, i) => {
+ if (args.click) {
+ args.click(d, i);
+ }
+ };
+
+ this.rolloverOn = args => {
+ const svg = mg_get_svg_child_of(args.target);
+
+ return (d, i) => {
+ mg_update_rollover_circle(args, svg, d);
+ mg_trigger_linked_mouseovers(args, d, i);
+
+ svg.selectAll('text')
+ .filter((g, j) => d === g)
+ .attr('opacity', 0.3);
+
+ // update rollover text except for missing data points
+ if (args.show_rollover_text &&
+ !((args.missing_is_hidden && d['_missing']) || d[args.y_accessor] === null)
+ ) {
+ const mouseover = mg_mouseover_text(args, { svg });
+ let row = mouseover.mouseover_row();
+ if (args.aggregate_rollover) {
+ row.text((args.aggregate_rollover && args.data.length > 1
+ ? mg_format_x_aggregate_mouseover
+ : mg_format_x_mouseover)(args, d));
+ }
+
+ const pts = args.aggregate_rollover && args.data.length > 1
+ ? d.values
+ : [d];
+
+ pts.forEach(di => {
+ if (args.aggregate_rollover) {
+ row = mouseover.mouseover_row();
+ }
+
+ if (args.legend) {
+ mg_line_color_text(row.text(`${args.legend[di.__index__ - 1]} `).bold(), di.__line_id__, args);
+ }
+
+ mg_line_color_text(row.text('\u2014 ').elem, di.__line_id__, args);
+ if (!args.aggregate_rollover) {
+ row.text(mg_format_x_mouseover(args, di));
+ }
+
+ row.text(mg_format_y_mouseover(args, di, args.time_series === false));
+ });
+ }
+
+ if (args.mouseover) {
+ args.mouseover(d, i);
+ }
+ };
+ };
+
+ this.rolloverOff = args => {
+ const svg = mg_get_svg_child_of(args.target);
+
+ return (d, i) => {
+ mg_trigger_linked_mouseouts(args, d, i);
+ if (args.aggregate_rollover) {
+ mg_remove_active_data_points_for_aggregate_rollover(args, svg);
+ } else {
+ mg_remove_active_data_points_for_generic_rollover(args, svg, d.__line_id__);
+ }
+
+ if (args.data[0].length > 1) {
+ mg_clear_mouseover_container(svg);
+ }
+
+ if (args.mouseout) {
+ args.mouseout(d, i);
+ }
+ };
+ };
+
+ this.rolloverMove = args => (d, i) => {
+ if (args.mousemove) {
+ args.mousemove(d, i);
+ }
+ };
+
+ this.windowListeners = function() {
+ mg_window_listeners(this.args);
+ return this;
+ };
+
+ this.init(args);
+ }
+
+ MG.register('line', lineChart);
+}
diff --git a/priv/static/js/metricsgraphics/charts/missing.js b/priv/static/js/metricsgraphics/charts/missing.js
new file mode 100644
index 0000000..330d5c0
--- /dev/null
+++ b/priv/static/js/metricsgraphics/charts/missing.js
@@ -0,0 +1,144 @@
+{
+ function mg_missing_add_text(svg, {missing_text, width, height}) {
+ svg.selectAll('.mg-missing-text').data([missing_text])
+ .enter().append('text')
+ .attr('class', 'mg-missing-text')
+ .attr('x', width / 2)
+ .attr('y', height / 2)
+ .attr('dy', '.50em')
+ .attr('text-anchor', 'middle')
+ .text(missing_text);
+ }
+
+ function mg_missing_x_scale(args) {
+ args.scales.X = d3.scaleLinear()
+ .domain([0, args.data.length])
+ .range([mg_get_plot_left(args), mg_get_plot_right(args)]);
+ args.scalefns.yf = ({y}) => args.scales.Y(y);
+ }
+
+ function mg_missing_y_scale(args) {
+ args.scales.Y = d3.scaleLinear()
+ .domain([-2, 2])
+ .range([args.height - args.bottom - args.buffer * 2, args.top]);
+ args.scalefns.xf = ({x}) => args.scales.X(x);
+ }
+
+ function mg_make_fake_data(args) {
+ const data = [];
+ for (let x = 1; x <= 50; x++) {
+ data.push({ x, y: Math.random() - (x * 0.03) });
+ }
+ args.data = data;
+ }
+
+ function mg_add_missing_background_rect(g, {title, buffer, title_y_position, width, height}) {
+ g.append('svg:rect')
+ .classed('mg-missing-background', true)
+ .attr('x', buffer)
+ .attr('y', buffer + (title ? title_y_position : 0) * 2)
+ .attr('width', width - buffer * 2)
+ .attr('height', height - buffer * 2 - (title ? title_y_position : 0) * 2)
+ .attr('rx', 15)
+ .attr('ry', 15);
+ }
+
+ function mg_missing_add_line(g, {scalefns, interpolate, data}) {
+ const line = d3.line()
+ .x(scalefns.xf)
+ .y(scalefns.yf)
+ .curve(interpolate);
+
+ g.append('path')
+ .attr('class', 'mg-main-line mg-line1-color')
+ .attr('d', line(data));
+ }
+
+ function mg_missing_add_area(g, {scalefns, scales, interpolate, data}) {
+ const area = d3.area()
+ .x(scalefns.xf)
+ .y0(scales.Y.range()[0])
+ .y1(scalefns.yf)
+ .curve(interpolate);
+
+ g.append('path')
+ .attr('class', 'mg-main-area mg-area1-color')
+ .attr('d', area(data));
+ }
+
+ function mg_remove_all_children({target}) {
+ d3.select(target).selectAll('svg *').remove();
+ }
+
+ function mg_missing_remove_legend({legend_target}) {
+ if (legend_target) {
+ d3.select(legend_target).html('');
+ }
+ }
+
+ function missingData(args) {
+ this.init = (args) => {
+ this.args = args;
+
+ mg_init_compute_width(args);
+ mg_init_compute_height(args);
+
+ // create svg if one doesn't exist
+
+ const container = d3.select(args.target);
+ mg_raise_container_error(container, args);
+ let svg = container.selectAll('svg');
+ mg_remove_svg_if_chart_type_has_changed(svg, args);
+ svg = mg_add_svg_if_it_doesnt_exist(svg, args);
+ mg_adjust_width_and_height_if_changed(svg, args);
+ mg_set_viewbox_for_scaling(svg, args);
+ mg_remove_all_children(args);
+
+ svg.classed('mg-missing', true);
+ mg_missing_remove_legend(args);
+
+ chart_title(args);
+
+ // are we adding a background placeholder
+ if (args.show_missing_background) {
+ mg_make_fake_data(args);
+ mg_missing_x_scale(args);
+ mg_missing_y_scale(args);
+ const g = mg_add_g(svg, 'mg-missing-pane');
+
+ mg_add_missing_background_rect(g, args);
+ mg_missing_add_line(g, args);
+ mg_missing_add_area(g, args);
+ }
+
+ mg_missing_add_text(svg, args);
+
+ this.windowListeners();
+
+ return this;
+ };
+
+ this.windowListeners = () => {
+ mg_window_listeners(this.args);
+ return this;
+ };
+
+ this.init(args);
+ }
+
+ const defaults = {
+ top: [40, 'number'], // the size of the top margin
+ bottom: [30, 'number'], // the size of the bottom margin
+ right: [10, 'number'], // size of the right margin
+ left: [0, 'number'], // size of the left margin
+ buffer: [8, 'number'], // the buffer between the actual chart area and the margins
+ legend_target: ['', 'string'],
+ width: [350, 'number'],
+ height: [220, 'number'],
+ missing_text: ['Data currently missing or unavailable', 'string'],
+ show_tooltips: [true, 'boolean'],
+ show_missing_background: [true, 'boolean']
+ };
+
+ MG.register('missing-data', missingData, defaults);
+}
diff --git a/priv/static/js/metricsgraphics/charts/point.js b/priv/static/js/metricsgraphics/charts/point.js
new file mode 100644
index 0000000..b511f19
--- /dev/null
+++ b/priv/static/js/metricsgraphics/charts/point.js
@@ -0,0 +1,383 @@
+function point_mouseover(args, svg, d) {
+ const mouseover = mg_mouseover_text(args, { svg });
+ const row = mouseover.mouseover_row();
+
+ if (args.color_accessor !== null && args.color_type === 'category') {
+ const label = d[args.color_accessor];
+ row.text(`${label} `).bold().attr('fill', args.scalefns.colorf(d));
+ }
+
+ mg_color_point_mouseover(args, row.text('\u25CF ').elem, d); // point shape
+
+ row.text(mg_format_x_mouseover(args, d)); // x
+ row.text(mg_format_y_mouseover(args, d, args.time_series === false));
+}
+
+function mg_color_point_mouseover({color_accessor, scalefns}, elem, d) {
+ if (color_accessor !== null) {
+ elem.attr('fill', scalefns.colorf(d));
+ elem.attr('stroke', scalefns.colorf(d));
+ } else {
+ elem.classed('mg-points-mono', true);
+ }
+}
+
+
+{
+ function mg_filter_out_plot_bounds(data, args) {
+ // max_x, min_x, max_y, min_y;
+ const x = args.x_accessor;
+ const y = args.y_accessor;
+ const new_data = data.filter(d => (args.min_x === null || d[x] >= args.min_x) &&
+ (args.max_x === null || d[x] <= args.max_x) &&
+ (args.min_y === null || d[y] >= args.min_y) &&
+ (args.max_y === null || d[y] <= args.max_y));
+ return new_data;
+ }
+
+ function pointChart(args) {
+ this.init = function(args) {
+ this.args = args;
+
+ // infer y_axis and x_axis type;
+ args.x_axis_type = mg_infer_type(args, 'x');
+ args.y_axis_type = mg_infer_type(args, 'y');
+
+ raw_data_transformation(args);
+
+ process_point(args);
+ init(args);
+
+ let xMaker, yMaker;
+
+ if (args.x_axis_type === 'categorical') {
+ xMaker = MG.scale_factory(args)
+ .namespace('x')
+ .categoricalDomainFromData()
+ .categoricalRangeBands([0, args.xgroup_height], args.xgroup_accessor === null);
+
+ if (args.xgroup_accessor) {
+ new MG.scale_factory(args)
+ .namespace('xgroup')
+ .categoricalDomainFromData()
+ .categoricalRangeBands('bottom');
+
+ } else {
+ args.scales.XGROUP = () => mg_get_plot_left(args);
+ args.scalefns.xgroupf = () => mg_get_plot_left(args);
+ }
+
+ args.scalefns.xoutf = d => args.scalefns.xf(d) + args.scalefns.xgroupf(d);
+ } else {
+ xMaker = MG.scale_factory(args)
+ .namespace('x')
+ .inflateDomain(true)
+ .zeroBottom(args.y_axis_type === 'categorical')
+ .numericalDomainFromData((args.baselines || []).map(d => d[args.x_accessor]))
+ .numericalRange('bottom');
+
+ args.scalefns.xoutf = args.scalefns.xf;
+ }
+
+ // y-scale generation. This needs to get simplified.
+ if (args.y_axis_type === 'categorical') {
+ yMaker = MG.scale_factory(args)
+ .namespace('y')
+ .zeroBottom(true)
+ .categoricalDomainFromData()
+ .categoricalRangeBands([0, args.ygroup_height], true);
+
+ if (args.ygroup_accessor) {
+
+ new MG.scale_factory(args)
+ .namespace('ygroup')
+ .categoricalDomainFromData()
+ .categoricalRangeBands('left');
+
+ } else {
+ args.scales.YGROUP = () => mg_get_plot_top(args);
+ args.scalefns.ygroupf = () => mg_get_plot_top(args);
+
+ }
+ args.scalefns.youtf = d => args.scalefns.yf(d) + args.scalefns.ygroupf(d);
+
+ } else {
+ const baselines = (args.baselines || []).map(d => d[args.y_accessor]);
+ yMaker = MG.scale_factory(args)
+ .namespace('y')
+ .inflateDomain(true)
+ .zeroBottom(args.x_axis_type === 'categorical')
+ .numericalDomainFromData(baselines)
+ .numericalRange('left');
+
+ args.scalefns.youtf = d => args.scalefns.yf(d);
+ }
+
+ /////// COLOR accessor
+ if (args.color_accessor !== null) {
+ const colorScale = MG.scale_factory(args).namespace('color');
+ if (args.color_type === 'number') {
+ // do the color scale.
+ // etiher get color range, or what.
+ colorScale
+ .numericalDomainFromData(mg_get_color_domain(args))
+ .numericalRange(mg_get_color_range(args))
+ .clamp(true);
+ } else {
+ if (args.color_domain) {
+ colorScale
+ .categoricalDomain(args.color_domain)
+ .categoricalRange(args.color_range);
+ } else {
+ colorScale
+ .categoricalDomainFromData()
+ .categoricalColorRange();
+ }
+ }
+ }
+
+ if (args.size_accessor) {
+ new MG.scale_factory(args).namespace('size')
+ .numericalDomainFromData()
+ .numericalRange(mg_get_size_range(args))
+ .clamp(true);
+ }
+
+ new MG.axis_factory(args)
+ .namespace('x')
+ .type(args.x_axis_type)
+ .zeroLine(args.y_axis_type === 'categorical')
+ .position(args.x_axis_position)
+ .rug(x_rug(args))
+ .label(mg_add_x_label)
+ .draw();
+
+ new MG.axis_factory(args)
+ .namespace('y')
+ .type(args.y_axis_type)
+ .zeroLine(args.x_axis_type === 'categorical')
+ .position(args.y_axis_position)
+ .rug(y_rug(args))
+ .label(mg_add_y_label)
+ .draw();
+
+ this.mainPlot();
+ this.markers();
+ this.rollover();
+ this.windowListeners();
+ if (args.brush) MG.add_brush_function(args);
+ return this;
+ };
+
+ this.markers = function() {
+ markers(args);
+ if (args.least_squares) {
+ add_ls(args);
+ }
+
+ return this;
+ };
+
+ this.mainPlot = function() {
+ const svg = mg_get_svg_child_of(args.target);
+
+ const data = mg_filter_out_plot_bounds(args.data[0], args);
+ //remove the old points, add new one
+ svg.selectAll('.mg-points').remove();
+
+ const g = svg.append('g')
+ .classed('mg-points', true);
+
+ const pts = g.selectAll('circle')
+ .data(data)
+ .enter().append('circle')
+ .attr('class', (d, i) => `path-${i}`)
+ .attr('cx', args.scalefns.xoutf)
+ .attr('cy', d => args.scalefns.youtf(d));
+
+ let highlights;
+ svg.selectAll('.mg-highlight').remove();
+ if (args.highlight && mg_is_function(args.highlight)) {
+ highlights = svg.append('g')
+ .classed('mg-highlight', true)
+ .selectAll('circle')
+ .data(data.filter(args.highlight))
+ .enter().append('circle')
+ .attr('cx', args.scalefns.xoutf)
+ .attr('cy', d => args.scalefns.youtf(d));
+ }
+
+ const elements = [pts].concat(highlights ? [highlights] : []);
+ //are we coloring our points, or just using the default color?
+ if (args.color_accessor !== null) {
+ elements.forEach(e => e.attr('fill', args.scalefns.colorf).attr('stroke', args.scalefns.colorf));
+ } else {
+ elements.forEach(e => e.classed('mg-points-mono', true));
+ }
+
+ pts.attr('r', (args.size_accessor !== null) ? args.scalefns.sizef : args.point_size);
+ if (highlights) {
+ highlights.attr('r', (args.size_accessor !== null) ? (d, i) => args.scalefns.sizef(d, i) + 2 : args.point_size + 2);
+ }
+
+ return this;
+ };
+
+ this.rollover = function() {
+ const svg = mg_get_svg_child_of(args.target);
+
+ if (svg.selectAll('.mg-active-datapoint-container').nodes().length === 0) {
+ mg_add_g(svg, 'mg-active-datapoint-container');
+ }
+
+ //remove the old rollovers if they already exist
+ svg.selectAll('.mg-voronoi').remove();
+
+ //add rollover paths
+ const voronoi = d3.voronoi()
+ .x(args.scalefns.xoutf)
+ .y(args.scalefns.youtf)
+ .extent([
+ [args.buffer, args.buffer + (args.title ? args.title_y_position : 0)],
+ [args.width - args.buffer, args.height - args.buffer]
+ ]);
+
+ const paths = svg.append('g')
+ .attr('class', 'mg-voronoi');
+
+ paths.selectAll('path')
+ .data(voronoi.polygons(mg_filter_out_plot_bounds(args.data[0], args)))
+ .enter().append('path')
+ .attr('d', d => d == null ? null : `M${d.join(',')}Z`)
+ .attr('class', (d, i) => `path-${i}`)
+ .style('fill-opacity', 0)
+ .on('click', this.rolloverClick(args))
+ .on('mouseover', this.rolloverOn(args))
+ .on('mouseout', this.rolloverOff(args))
+ .on('mousemove', this.rolloverMove(args));
+
+ if (args.data[0].length === 1) {
+ point_mouseover(args, svg, args.data[0][0]);
+ }
+
+ return this;
+ };
+
+ this.rolloverClick = args => {
+ return (d, i) => {
+ if (args.click) {
+ args.click(d, i);
+ }
+ };
+ };
+
+ this.rolloverOn = args => {
+ const svg = mg_get_svg_child_of(args.target);
+
+ return (d, i) => {
+ svg.selectAll('.mg-points circle')
+ .classed('selected', false);
+
+ //highlight active point
+ const pts = svg.selectAll(`.mg-points circle.path-${i}`)
+ .classed('selected', true);
+
+ if (args.size_accessor) {
+ pts.attr('r', di => args.scalefns.sizef(di) + args.active_point_size_increase);
+ } else {
+ pts.attr('r', args.point_size + args.active_point_size_increase);
+ }
+
+ //trigger mouseover on all points for this class name in .linked charts
+ if (args.linked && !MG.globals.link) {
+ MG.globals.link = true;
+
+ //trigger mouseover on matching point in .linked charts
+ d3.selectAll(`.mg-voronoi .path-${i}`)
+ .each(() => {
+ d3.select(this).on('mouseover')(d, i);
+ });
+ }
+
+ if (args.show_rollover_text) {
+ point_mouseover(args, svg, d.data);
+ }
+
+ if (args.mouseover) {
+ args.mouseover(d, i);
+ }
+ };
+ };
+
+ this.rolloverOff = args => {
+ const svg = mg_get_svg_child_of(args.target);
+
+ return (d, i) => {
+ if (args.linked && MG.globals.link) {
+ MG.globals.link = false;
+
+ d3.selectAll(`.mg-voronoi .path-${i}`)
+ .each(() => {
+ d3.select(this).on('mouseout')(d, i);
+ });
+ }
+
+ //reset active point
+ const pts = svg.selectAll('.mg-points circle')
+ .classed('unselected', false)
+ .classed('selected', false);
+
+ if (args.size_accessor) {
+ pts.attr('r', args.scalefns.sizef);
+ } else {
+ pts.attr('r', args.point_size);
+ }
+
+ //reset active data point text
+ if (args.data[0].length > 1) mg_clear_mouseover_container(svg);
+
+ if (args.mouseout) {
+ args.mouseout(d, i);
+ }
+ };
+ };
+
+ this.rolloverMove = args => (d, i) => {
+ if (args.mousemove) {
+ args.mousemove(d, i);
+ }
+ };
+
+ this.update = function(args) {
+ return this;
+ };
+
+ this.windowListeners = function() {
+ mg_window_listeners(this.args);
+ return this;
+ };
+
+ this.init(args);
+ }
+
+ const options = {
+ color_accessor: [null, 'string'], // the data element to use to map points to colors
+ color_range: [null, 'array'], // the range used to color different groups of points
+ color_type: ['number', ['number', 'category']], // specifies whether the color scale is quantitative or qualitative
+ point_size: [2.5, 'number'], // the radius of the dots in the scatterplot
+ size_accessor: [null, 'string'], // should point sizes be mapped to data
+ size_range: [null, 'array'], // the range of point sizes
+ lowess: [false, 'boolean'], // specifies whether to show a lowess line of best-fit
+ least_squares: [false, 'boolean'], // specifies whether to show a least-squares line of best-fit
+ y_categorical_show_guides: [true, 'boolean'],
+ x_categorical_show_guides: [true, 'boolean'],
+ buffer: [16, 'string'],
+ label_accessor: [null, 'boolean'],
+ size_domain: [null, 'array'],
+ color_domain: [null, 'array'],
+ active_point_size_increase: [1, 'number'],
+ highlight: [null, 'function'] // if this callback function returns true, the selected point will be highlighted
+ };
+
+ MG.register('point', pointChart, options);
+}
diff --git a/priv/static/js/metricsgraphics/charts/table.js b/priv/static/js/metricsgraphics/charts/table.js
new file mode 100644
index 0000000..3081d8c
--- /dev/null
+++ b/priv/static/js/metricsgraphics/charts/table.js
@@ -0,0 +1,220 @@
+/*
+Data Tables
+
+Along with histograms, bars, lines, and scatters, a simple data table can take you far.
+We often just want to look at numbers, organized as a table, where columns are variables,
+and rows are data points. Sometimes we want a cell to have a small graphic as the main
+column element, in which case we want small multiples. sometimes we want to
+
+var table = New data_table(data)
+ .target('div#data-table')
+ .title({accessor: 'point_name', align: 'left'})
+ .description({accessor: 'description'})
+ .number({accessor: ''})
+
+*/
+
+MG.data_table = function(args) {
+ 'use strict';
+ this.args = args;
+ this.args.standard_col = { width: 150, font_size: 12, font_weight: 'normal' };
+ this.args.columns = [];
+ this.formatting_options = [
+ ['color', 'color'],
+ ['font-weight', 'font_weight'],
+ ['font-style', 'font_style'],
+ ['font-size', 'font_size']
+ ];
+
+ this._strip_punctuation = function(s) {
+ var punctuationless = s.replace(/[^a-zA-Z0-9 _]+/g, '');
+ var finalString = punctuationless.replace(/ +?/g, '');
+ return finalString;
+ };
+
+ this._format_element = function(element, value, args) {
+ this.formatting_options.forEach(function(fo) {
+ var attr = fo[0];
+ var key = fo[1];
+ if (args[key]) element.style(attr,
+ typeof args[key] === 'string' ||
+ typeof args[key] === 'number' ?
+ args[key] : args[key](value));
+ });
+ };
+
+ this._add_column = function(_args, arg_type) {
+ var standard_column = this.args.standard_col;
+ var args = merge_with_defaults(MG.clone(_args), MG.clone(standard_column));
+ args.type = arg_type;
+ this.args.columns.push(args);
+ };
+
+ this.target = function() {
+ var target = arguments[0];
+ this.args.target = target;
+ return this;
+ };
+
+ this.title = function() {
+ this._add_column(arguments[0], 'title');
+ return this;
+ };
+
+ this.text = function() {
+ this._add_column(arguments[0], 'text');
+ return this;
+ };
+
+ this.bullet = function() {
+ /*
+ text label
+ main value
+ comparative measure
+ any number of ranges
+
+ additional args:
+ no title
+ xmin, xmax
+ format: percentage
+ xax_formatter
+ */
+ return this;
+ };
+
+ this.sparkline = function() {
+ return this;
+ };
+
+ this.number = function() {
+ this._add_column(arguments[0], 'number');
+ return this;
+ };
+
+ this.display = function() {
+ var args = this.args;
+
+ chart_title(args);
+
+ var target = args.target;
+ var table = d3.select(target).append('table').classed('mg-data-table', true);
+ var colgroup = table.append('colgroup');
+ var thead = table.append('thead');
+ var tbody = table.append('tbody');
+ var this_column;
+ var this_title;
+
+ var tr, th, td_accessor, td_type, td_value, th_text, td_text, td;
+ var col;
+ var h;
+
+ tr = thead.append('tr');
+
+ for (h = 0; h < args.columns.length; h++) {
+ var this_col = args.columns[h];
+ td_type = this_col.type;
+ th_text = this_col.label;
+ th_text = th_text === undefined ? '' : th_text;
+ th = tr.append('th')
+ .style('width', this_col.width)
+ .style('text-align', td_type === 'title' ? 'left' : 'right')
+ .text(th_text);
+
+ if (args.show_tooltips && this_col.description && mg_jquery_exists()) {
+ th.append('i')
+ .classed('fa', true)
+ .classed('fa-question-circle', true)
+ .classed('fa-inverse', true);
+
+ $(th.node()).popover({
+ html: true,
+ animation: false,
+ content: this_col.description,
+ trigger: 'hover',
+ placement: 'top',
+ container: $(th.node())
+ });
+ }
+ }
+
+ for (h = 0; h < args.columns.length; h++) {
+ col = colgroup.append('col');
+ if (args.columns[h].type === 'number') {
+ col.attr('align', 'char').attr('char', '.');
+ }
+ }
+
+ for (var i = 0; i < args.data.length; i++) {
+ tr = tbody.append('tr');
+ for (var j = 0; j < args.columns.length; j++) {
+ this_column = args.columns[j];
+ td_accessor = this_column.accessor;
+ td_value = td_text = args.data[i][td_accessor];
+ td_type = this_column.type;
+
+ if (td_type === 'number') {
+ //td_text may need to be rounded
+ if (this_column.hasOwnProperty('round') && !this_column.hasOwnProperty('format')) {
+ // round according to the number value in this_column.round
+ td_text = d3.format('0,.' + this_column.round + 'f')(td_text);
+ }
+
+ if (this_column.hasOwnProperty('value_formatter')) {
+ // provide a function that formats the text according to the function this_column.format.
+ td_text = this_column.value_formatter(td_text);
+ }
+
+ if (this_column.hasOwnProperty('format')) {
+ // this is a shorthand for percentage formatting, and others if need be.
+ // supported: 'percentage', 'count', 'temperature'
+
+ if (this_column.round) {
+ td_text = Math.round(td_text, this_column.round);
+ }
+
+ var this_format = this_column.format;
+ var formatter;
+
+ if (this_format === 'percentage') formatter = d3.format('.0%');
+ if (this_format === 'count') formatter = d3.format(',.0f');
+ if (this_format === 'temperature') formatter = function(t) {
+ return t + '°'; };
+
+ td_text = formatter(td_text);
+ }
+
+ if (this_column.hasOwnProperty('currency')) {
+ // this is another shorthand for formatting according to a currency amount, which gets appended to front of number
+ td_text = this_column.currency + td_text;
+ }
+ }
+
+ td = tr.append('td')
+ .classed('table-' + td_type, true)
+ .classed('table-' + td_type + '-' + this._strip_punctuation(td_accessor), true)
+ .attr('data-value', td_value)
+ .style('width', this_column.width)
+ .style('text-align', td_type === 'title' || td_type === 'text' ? 'left' : 'right');
+
+ this._format_element(td, td_value, this_column);
+
+ if (td_type === 'title') {
+ this_title = td.append('div').text(td_text);
+ this._format_element(this_title, td_text, this_column);
+
+ if (args.columns[j].hasOwnProperty('secondary_accessor')) {
+ td.append('div')
+ .text(args.data[i][args.columns[j].secondary_accessor])
+ .classed("secondary-title", true);
+ }
+ } else {
+ td.text(td_text);
+ }
+ }
+ }
+
+ return this;
+ };
+
+ return this;
+};
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;
+
+}
diff --git a/priv/static/js/metricsgraphics/layout/bootstrap_dropdown.js b/priv/static/js/metricsgraphics/layout/bootstrap_dropdown.js
new file mode 100755
index 0000000..41dbfc5
--- /dev/null
+++ b/priv/static/js/metricsgraphics/layout/bootstrap_dropdown.js
@@ -0,0 +1,177 @@
+if (mg_jquery_exists()) {
+ /*!
+ * Bootstrap v3.3.1 (http://getbootstrap.com)
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+
+ /*!
+ * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=c3834cc5b59ef727da53)
+ * Config saved to config.json and https://gist.github.com/c3834cc5b59ef727da53
+ */
+
+ /* ========================================================================
+ * Bootstrap: dropdown.js v3.3.1
+ * http://getbootstrap.com/javascript/#dropdowns
+ * ========================================================================
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * ======================================================================== */
+
+
+ +function ($) {
+ 'use strict';
+
+ if(typeof $().dropdown == 'function')
+ return true;
+
+ // DROPDOWN CLASS DEFINITION
+ // =========================
+
+ var backdrop = '.dropdown-backdrop';
+ var toggle = '[data-toggle="dropdown"]';
+ var Dropdown = function (element) {
+ $(element).on('click.bs.dropdown', this.toggle);
+ };
+
+ Dropdown.VERSION = '3.3.1';
+
+ Dropdown.prototype.toggle = function (e) {
+ var $this = $(this);
+
+ if ($this.is('.disabled, :disabled')) return;
+
+ var $parent = getParent($this);
+ var isActive = $parent.hasClass('open');
+
+ clearMenus();
+
+ if (!isActive) {
+ if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) {
+ // if mobile we use a backdrop because click events don't delegate
+ $('<div class="dropdown-backdrop"/>').insertAfter($(this)).on('click', clearMenus);
+ }
+
+ var relatedTarget = { relatedTarget: this };
+ $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget));
+
+ if (e.isDefaultPrevented()) return;
+
+ $this
+ .trigger('focus')
+ .attr('aria-expanded', 'true');
+
+ $parent
+ .toggleClass('open')
+ .trigger('shown.bs.dropdown', relatedTarget);
+ }
+
+ return false;
+ };
+
+ Dropdown.prototype.keydown = function (e) {
+ if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return;
+
+ var $this = $(this);
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ if ($this.is('.disabled, :disabled')) return;
+
+ var $parent = getParent($this);
+ var isActive = $parent.hasClass('open');
+
+ if ((!isActive && e.which != 27) || (isActive && e.which == 27)) {
+ if (e.which == 27) $parent.find(toggle).trigger('focus');
+ return $this.trigger('click');
+ }
+
+ var desc = ' li:not(.divider):visible a';
+ var $items = $parent.find('[role="menu"]' + desc + ', [role="listbox"]' + desc);
+
+ if (!$items.length) return;
+
+ var index = $items.index(e.target);
+
+ if (e.which == 38 && index > 0) index--; // up
+ if (e.which == 40 && index < $items.length - 1) index++; // down
+ if (!~index) index = 0;
+
+ $items.eq(index).trigger('focus');
+ };
+
+ function clearMenus(e) {
+ if (e && e.which === 3) return;
+ $(backdrop).remove();
+ $(toggle).each(function () {
+ var $this = $(this);
+ var $parent = getParent($this);
+ var relatedTarget = { relatedTarget: this };
+
+ if (!$parent.hasClass('open')) return;
+
+ $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget));
+
+ if (e.isDefaultPrevented()) return;
+
+ $this.attr('aria-expanded', 'false');
+ $parent.removeClass('open').trigger('hidden.bs.dropdown', relatedTarget);
+ });
+ }
+
+ function getParent($this) {
+ var selector = $this.attr('data-target');
+
+ if (!selector) {
+ selector = $this.attr('href');
+ selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, ''); // strip for ie7
+ }
+
+ var $parent = selector && $(selector);
+
+ return $parent && $parent.length ? $parent : $this.parent();
+ }
+
+
+ // DROPDOWN PLUGIN DEFINITION
+ // ==========================
+
+ function Plugin(option) {
+ return this.each(function () {
+ var $this = $(this);
+ var data = $this.data('bs.dropdown');
+
+ if (!data) $this.data('bs.dropdown', (data = new Dropdown(this)));
+ if (typeof option == 'string') data[option].call($this);
+ });
+ }
+
+ var old = $.fn.dropdown;
+
+ $.fn.dropdown = Plugin;
+ $.fn.dropdown.Constructor = Dropdown;
+
+
+ // DROPDOWN NO CONFLICT
+ // ====================
+
+ $.fn.dropdown.noConflict = function () {
+ $.fn.dropdown = old;
+ return this;
+ };
+
+
+ // APPLY TO STANDARD DROPDOWN ELEMENTS
+ // ===================================
+
+ $(document)
+ .on('click.bs.dropdown.data-api', clearMenus)
+ .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation(); })
+ .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle)
+ .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown)
+ .on('keydown.bs.dropdown.data-api', '[role="menu"]', Dropdown.prototype.keydown)
+ .on('keydown.bs.dropdown.data-api', '[role="listbox"]', Dropdown.prototype.keydown);
+
+ }(jQuery);
+}
diff --git a/priv/static/js/metricsgraphics/layout/button.js b/priv/static/js/metricsgraphics/layout/button.js
new file mode 100644
index 0000000..b122273
--- /dev/null
+++ b/priv/static/js/metricsgraphics/layout/button.js
@@ -0,0 +1,126 @@
+MG.button_layout = function(target) {
+ 'use strict';
+ this.target = target;
+ this.feature_set = {};
+ this.public_name = {};
+ this.sorters = {};
+ this.manual = [];
+ this.manual_map = {};
+ this.manual_callback = {};
+
+ this._strip_punctuation = function(s) {
+ var punctuationless = s.replace(/[^a-zA-Z0-9 _]+/g, '');
+ var finalString = punctuationless.replace(/ +?/g, '');
+ return finalString;
+ };
+
+ this.data = function(data) {
+ this._data = data;
+ return this;
+ };
+
+ this.manual_button = function(feature, feature_set, callback) {
+ this.feature_set[feature] = feature_set;
+ this.manual_map[this._strip_punctuation(feature)] = feature;
+ this.manual_callback[feature] = callback; // the default is going to be the first feature.
+ return this;
+ };
+
+ this.button = function(feature) {
+ if (arguments.length > 1) {
+ this.public_name[feature] = arguments[1];
+ }
+
+ if (arguments.length > 2) {
+ this.sorters[feature] = arguments[2];
+ }
+
+ this.feature_set[feature] = [];
+ return this;
+ };
+
+ this.callback = function(callback) {
+ this._callback = callback;
+ return this;
+ };
+
+ this.display = function() {
+ var callback = this._callback;
+ var manual_callback = this.manual_callback;
+ var manual_map = this.manual_map;
+
+ var d, f, features, feat;
+ features = Object.keys(this.feature_set);
+
+ var mapDtoF = function(f) {
+ return d[f]; };
+
+ var i;
+
+ // build out this.feature_set with this.data
+ for (i = 0; i < this._data.length; i++) {
+ d = this._data[i];
+ f = features.map(mapDtoF);
+ for (var j = 0; j < features.length; j++) {
+ feat = features[j];
+ if (this.feature_set[feat].indexOf(f[j]) === -1) {
+ this.feature_set[feat].push(f[j]);
+ }
+ }
+ }
+
+ for (feat in this.feature_set) {
+ if (this.sorters.hasOwnProperty(feat)) {
+ this.feature_set[feat].sort(this.sorters[feat]);
+ }
+ }
+
+ $(this.target).empty();
+
+ $(this.target).append("<div class='col-lg-12 segments text-center'></div>");
+
+ var dropdownLiAClick = function() {
+ var k = $(this).data('key');
+ var feature = $(this).data('feature');
+ var manual_feature;
+ $('.' + feature + '-btns button.btn span.title').html(k);
+ if (!manual_map.hasOwnProperty(feature)) {
+ callback(feature, k);
+ } else {
+ manual_feature = manual_map[feature];
+ manual_callback[manual_feature](k);
+ }
+
+ return false;
+ };
+
+ for (var feature in this.feature_set) {
+ features = this.feature_set[feature];
+ $(this.target + ' div.segments').append(
+ '<div class="btn-group ' + this._strip_punctuation(feature) + '-btns text-left">' + // This never changes.
+ '<button type="button" class="btn btn-default btn-lg dropdown-toggle" data-toggle="dropdown">' +
+ "<span class='which-button'>" + (this.public_name.hasOwnProperty(feature) ? this.public_name[feature] : feature) + "</span>" +
+ "<span class='title'>" + (this.manual_callback.hasOwnProperty(feature) ? this.feature_set[feature][0] : 'all') + "</span>" + // if a manual button, don't default to all in label.
+ '<span class="caret"></span>' +
+ '</button>' +
+ '<ul class="dropdown-menu" role="menu">' +
+ (!this.manual_callback.hasOwnProperty(feature) ? '<li><a href="#" data-feature="' + feature + '" data-key="all">All</a></li>' : "") +
+ (!this.manual_callback.hasOwnProperty(feature) ? '<li class="divider"></li>' : "") +
+ '</ul>' + '</div>');
+
+ for (i = 0; i < features.length; i++) {
+ if (features[i] !== 'all' && features[i] !== undefined) { // strange bug with undefined being added to manual buttons.
+ $(this.target + ' div.' + this._strip_punctuation(feature) + '-btns ul.dropdown-menu').append(
+ '<li><a href="#" data-feature="' + this._strip_punctuation(feature) + '" data-key="' + features[i] + '">' + features[i] + '</a></li>'
+ );
+ }
+ }
+
+ $('.' + this._strip_punctuation(feature) + '-btns .dropdown-menu li a').on('click', dropdownLiAClick);
+ }
+
+ return this;
+ };
+
+ return this;
+};
diff --git a/priv/static/js/metricsgraphics/misc/error.js b/priv/static/js/metricsgraphics/misc/error.js
new file mode 100644
index 0000000..cf2eee3
--- /dev/null
+++ b/priv/static/js/metricsgraphics/misc/error.js
@@ -0,0 +1,16 @@
+// call this to add a warning icon to a graph and log an error to the console
+function error(args) {
+ console.error('ERROR : ', args.target, ' : ', args.error);
+
+ d3.select(args.target).select('.mg-chart-title')
+ .append('tspan')
+ .attr('class', 'fa fa-x fa-exclamation-circle mg-warning')
+ .attr('dx', '0.3em')
+ .text('\uf06a');
+}
+
+function internal_error(args) {
+ console.error('INTERNAL ERROR : ', args.target, ' : ', args.internal_error);
+}
+
+MG.error = error;
diff --git a/priv/static/js/metricsgraphics/misc/formatters.js b/priv/static/js/metricsgraphics/misc/formatters.js
new file mode 100644
index 0000000..1533fd5
--- /dev/null
+++ b/priv/static/js/metricsgraphics/misc/formatters.js
@@ -0,0 +1,147 @@
+function format_rollover_number(args) {
+ var num;
+ if (args.format === 'count') {
+ num = function(d) {
+ var is_float = d % 1 !== 0;
+ var pf;
+
+ if (is_float) {
+ pf = d3.format(',.' + args.decimals + 'f');
+ } else {
+ pf = d3.format(',.0f');
+ }
+
+ // 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 {
+ num = function(d_) {
+ var fmt_string = (isNumeric(args.decimals) ? '.' + args.decimals : '') + '%';
+ var pf = d3.format(fmt_string);
+ return pf(d_);
+ };
+ }
+ return num;
+}
+
+var time_rollover_format = function(f, d, accessor, utc) {
+ var fd;
+ if (typeof f === 'string') {
+ fd = MG.time_format(utc, f)(d[accessor]);
+ } else if (typeof f === 'function') {
+ fd = f(d);
+ } else {
+ fd = d[accessor];
+ }
+ return fd;
+};
+
+// define our rollover format for numbers
+var number_rollover_format = function(f, d, accessor) {
+ var fd;
+ if (typeof f === 'string') {
+ fd = d3.format('s')(d[accessor]);
+ } else if (typeof f === 'function') {
+ fd = f(d);
+ } else {
+ fd = d[accessor];
+ }
+ return fd;
+};
+
+function mg_format_y_rollover(args, num, d) {
+ var formatted_y;
+ if (args.y_mouseover !== null) {
+ if (args.aggregate_rollover) {
+ formatted_y = number_rollover_format(args.y_mouseover, d, args.y_accessor);
+ } else {
+ formatted_y = number_rollover_format(args.y_mouseover, d, args.y_accessor);
+ }
+ } else {
+ if (args.time_series) {
+ if (args.aggregate_rollover) {
+ formatted_y = num(d[args.y_accessor]);
+ } else {
+ formatted_y = args.yax_units + num(d[args.y_accessor]);
+ }
+ } else {
+ formatted_y = args.y_accessor + ': ' + args.yax_units + num(d[args.y_accessor]);
+ }
+ }
+ return formatted_y;
+}
+
+function mg_format_x_rollover(args, fmt, d) {
+ var formatted_x;
+ if (args.x_mouseover !== null) {
+ if (args.time_series) {
+ if (args.aggregate_rollover) {
+ formatted_x = time_rollover_format(args.x_mouseover, d, 'key', args.utc);
+ } else {
+ formatted_x = time_rollover_format(args.x_mouseover, d, args.x_accessor, args.utc);
+ }
+ } else {
+ formatted_x = number_rollover_format(args.x_mouseover, d, args.x_accessor);
+ }
+ } else {
+ if (args.time_series) {
+ var date;
+
+ if (args.aggregate_rollover && args.data.length > 1) {
+ date = new Date(d.key);
+ } else {
+ date = new Date(+d[args.x_accessor]);
+ date.setDate(date.getDate());
+ }
+
+ formatted_x = fmt(date) + ' ';
+ } else {
+ formatted_x = args.x_accessor + ': ' + d[args.x_accessor] + ' ';
+ }
+ }
+ return formatted_x;
+}
+
+function mg_format_data_for_mouseover(args, d, mouseover_fcn, accessor, check_time) {
+ var formatted_data, formatter;
+ var time_fmt = mg_get_rollover_time_format(args);
+ if (typeof d[accessor] === 'string') {
+ formatter = function(d) {
+ return d;
+ };
+ } else {
+ formatter = format_rollover_number(args);
+ }
+
+ if (mouseover_fcn !== null) {
+ if (check_time) formatted_data = time_rollover_format(mouseover_fcn, d, accessor, args.utc);
+ else formatted_data = number_rollover_format(mouseover_fcn, d, accessor);
+
+ } else {
+ if (check_time) formatted_data = time_fmt(new Date(+d[accessor])) + ' ';
+ else formatted_data = (args.time_series ? '' : accessor + ': ') + formatter(d[accessor]) + ' ';
+ }
+ return formatted_data;
+}
+
+function mg_format_number_mouseover(args, d) {
+ return mg_format_data_for_mouseover(args, d, args.x_mouseover, args.x_accessor, false);
+}
+
+function mg_format_x_mouseover(args, d) {
+ return mg_format_data_for_mouseover(args, d, args.x_mouseover, args.x_accessor, args.time_series);
+}
+
+function mg_format_y_mouseover(args, d) {
+ return mg_format_data_for_mouseover(args, d, args.y_mouseover, args.y_accessor, false);
+}
+
+function mg_format_x_aggregate_mouseover(args, d) {
+ return mg_format_data_for_mouseover(args, d, args.x_mouseover, 'key', args.time_series);
+}
+
+MG.format_rollover_number = format_rollover_number;
diff --git a/priv/static/js/metricsgraphics/misc/markup.js b/priv/static/js/metricsgraphics/misc/markup.js
new file mode 100644
index 0000000..c49f12a
--- /dev/null
+++ b/priv/static/js/metricsgraphics/misc/markup.js
@@ -0,0 +1,66 @@
+// influenced by https://bl.ocks.org/tomgp/c99a699587b5c5465228
+
+function render_markup_for_server(callback) {
+ var virtual_window = MG.virtual_window;
+ var virtual_d3 = d3.select(virtual_window.document);
+ var target = virtual_window.document.createElement('div');
+
+ var original_d3 = global.d3;
+ var original_window = global.window;
+ var original_document = global.document;
+ global.d3 = virtual_d3;
+ global.window = virtual_window;
+ global.document = virtual_window.document;
+
+ var error;
+ try {
+ callback(target);
+ } catch(e) {
+ error = e;
+ }
+
+ global.d3 = original_d3;
+ global.window = original_window;
+ global.document = original_document;
+
+ if (error) {
+ throw error;
+ }
+
+ /* for some reason d3.select parses jsdom elements incorrectly
+ * but it works if we wrap the element in a function.
+ */
+ return virtual_d3.select(function targetFn() {
+ return target;
+ }).html();
+}
+
+function render_markup_for_client(callback) {
+ var target = document.createElement('div');
+ callback(target);
+ return d3.select(target).html();
+}
+
+function render_markup(callback) {
+ switch(typeof window) {
+ case 'undefined':
+ return render_markup_for_server(callback);
+ default:
+ return render_markup_for_client(callback);
+ }
+}
+
+function init_virtual_window(jsdom, force) {
+ if (MG.virtual_window && !force) {
+ return;
+ }
+
+ var doc = jsdom.jsdom({
+ html: '',
+ features: { QuerySelector: true }
+ });
+ MG.virtual_window = doc.defaultView;
+}
+
+MG.render_markup = render_markup;
+MG.init_virtual_window = init_virtual_window;
diff --git a/priv/static/js/metricsgraphics/misc/process.js b/priv/static/js/metricsgraphics/misc/process.js
new file mode 100644
index 0000000..d0d312d
--- /dev/null
+++ b/priv/static/js/metricsgraphics/misc/process.js
@@ -0,0 +1,368 @@
+function mg_process_scale_ticks(args, axis) {
+ var accessor;
+ var scale_ticks;
+ var max;
+
+ if (axis === 'x') {
+ accessor = args.x_accessor;
+ scale_ticks = args.scales.X.ticks(args.xax_count);
+ max = args.processed.max_x;
+ } else if (axis === 'y') {
+ accessor = args.y_accessor;
+ scale_ticks = args.scales.Y.ticks(args.yax_count);
+ max = args.processed.max_y;
+ }
+
+ function log10(val) {
+ if (val === 1000) {
+ return 3;
+ }
+ if (val === 1000000) {
+ return 7;
+ }
+ return Math.log(val) / Math.LN10;
+ }
+
+ if ((axis === 'x' && args.x_scale_type === 'log') || (axis === 'y' && args.y_scale_type === 'log')) {
+ // get out only whole logs
+ scale_ticks = scale_ticks.filter(function(d) {
+ return Math.abs(log10(d)) % 1 < 1e-6 || Math.abs(log10(d)) % 1 > 1 - 1e-6;
+ });
+ }
+
+ // filter out fraction ticks if our data is ints and if 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;
+ });
+ }
+
+ if (axis === 'x') {
+ args.processed.x_ticks = scale_ticks;
+ } else if (axis === 'y') {
+ args.processed.y_ticks = scale_ticks;
+ }
+}
+
+function raw_data_transformation(args) {
+ 'use strict';
+
+ // dupe our data so we can modify it without adverse effect
+ args.data = MG.clone(args.data);
+
+ // we need to account for a few data format cases:
+ // #0 {bar1:___, bar2:___} // single object (for, say, bar charts)
+ // #1 [{key:__, value:__}, ...] // unnested obj-arrays
+ // #2 [[{key:__, value:__}, ...], [{key:__, value:__}, ...]] // nested obj-arrays
+ // #3 [[4323, 2343],..] // unnested 2d array
+ // #4 [[[4323, 2343],..] , [[4323, 2343],..]] // nested 2d array
+ args.single_object = false; // for bar charts.
+ args.array_of_objects = false;
+ args.array_of_arrays = false;
+ args.nested_array_of_arrays = false;
+ args.nested_array_of_objects = false;
+
+ // is the data object a nested array?
+
+ if (is_array_of_arrays(args.data)) {
+ args.nested_array_of_objects = args.data.map(function(d) {
+ return is_array_of_objects_or_empty(d);
+ }); // Case #2
+ args.nested_array_of_arrays = args.data.map(function(d) {
+ return is_array_of_arrays(d);
+ }); // Case #4
+ } else {
+ args.array_of_objects = is_array_of_objects(args.data); // Case #1
+ args.array_of_arrays = is_array_of_arrays(args.data); // Case #3
+ }
+
+ if (args.chart_type === 'line') {
+ if (args.array_of_objects || args.array_of_arrays) {
+ args.data = [args.data];
+ }
+ } else {
+ if (!(mg_is_array(args.data[0]))) {
+ args.data = [args.data];
+ }
+ }
+ // if the y_accessor is an array, break it up and store the result in args.data
+ mg_process_multiple_x_accessors(args);
+ mg_process_multiple_y_accessors(args);
+
+ // if user supplies keyword in args.color, change to arg.colors.
+ // this is so that the API remains fairly sensible and legible.
+ if (args.color !== undefined) {
+ args.colors = args.color;
+ }
+
+ // if user has supplied args.colors, and that value is a string, turn it into an array.
+ if (args.colors !== null && typeof args.colors === 'string') {
+ args.colors = [args.colors];
+ }
+
+ // sort x-axis data
+ if (args.chart_type === 'line' && args.x_sort === true) {
+ for (var i = 0; i < args.data.length; i++) {
+ args.data[i].sort(function(a, b) {
+ return a[args.x_accessor] - b[args.x_accessor];
+ });
+ }
+ }
+
+ return this;
+}
+
+function mg_process_multiple_accessors(args, which_accessor) {
+ // turns an array of accessors into ...
+ if (mg_is_array(args[which_accessor])) {
+ args.data = args.data.map(function(_d) {
+ return args[which_accessor].map(function(ya) {
+ return _d.map(function(di) {
+ di = MG.clone(di);
+
+ if (di[ya] === undefined) {
+ return undefined;
+ }
+
+ di['multiline_' + which_accessor] = di[ya];
+ return di;
+ }).filter(function(di) {
+ return di !== undefined;
+ });
+ });
+ })[0];
+ args[which_accessor] = 'multiline_' + which_accessor;
+ }
+}
+
+function mg_process_multiple_x_accessors(args) {
+ mg_process_multiple_accessors(args, 'x_accessor');
+}
+
+function mg_process_multiple_y_accessors(args) {
+ mg_process_multiple_accessors(args, 'y_accessor');
+}
+
+MG.raw_data_transformation = raw_data_transformation;
+
+function process_line(args) {
+ 'use strict';
+
+ var time_frame;
+
+ // do we have a time-series?
+ var is_time_series = d3.sum(args.data.map(function(series) {
+ return series.length > 0 && mg_is_date(series[0][args.x_accessor]);
+ })) > 0;
+
+ // are we replacing missing y values with zeros?
+ if ((args.missing_is_zero || args.missing_is_hidden) && args.chart_type === 'line' && is_time_series) {
+ for (var i = 0; i < args.data.length; i++) {
+ // we need to have a dataset of length > 2, so if it's less than that, skip
+ if (args.data[i].length <= 1) {
+ continue;
+ }
+
+ var first = args.data[i][0];
+ var last = args.data[i][args.data[i].length - 1];
+
+ // initialize our new array for storing the processed data
+ var processed_data = [];
+
+ // we'll be starting from the day after our first date
+ var start_date = MG.clone(first[args.x_accessor]).setDate(first[args.x_accessor].getDate() + 1);
+
+ // if we've set a max_x, add data points up to there
+ var from = (args.min_x) ? args.min_x : start_date;
+ var upto = (args.max_x) ? args.max_x : last[args.x_accessor];
+
+ time_frame = mg_get_time_frame((upto - from) / 1000);
+
+ if (['four-days', 'many-days', 'many-months', 'years', 'default'].indexOf(time_frame) !== -1 && args.missing_is_hidden_accessor === null) {
+ for (var d = new Date(from); d <= upto; d.setDate(d.getDate() + 1)) {
+ var o = {};
+ d.setHours(0, 0, 0, 0);
+
+ // add the first date item, we'll be starting from the day after our first date
+ if (Date.parse(d) === Date.parse(new Date(start_date))) {
+ processed_data.push(MG.clone(args.data[i][0]));
+ }
+
+ // check to see if we already have this date in our data object
+ var existing_o = null;
+ args.data[i].forEach(function(val, i) {
+ if (Date.parse(val[args.x_accessor]) === Date.parse(new Date(d))) {
+ existing_o = val;
+
+ return false;
+ }
+ });
+
+ // if we don't have this date in our data object, add it and set it to zero
+ if (!existing_o) {
+ o[args.x_accessor] = new Date(d);
+ o[args.y_accessor] = 0;
+ o['_missing'] = true; //we want to distinguish between zero-value and missing observations
+ processed_data.push(o);
+ }
+
+ // if the data point has, say, a 'missing' attribute set or if its
+ // y-value is null identify it internally as missing
+ else if (existing_o[args.missing_is_hidden_accessor] || existing_o[args.y_accessor] === null) {
+ existing_o['_missing'] = true;
+ processed_data.push(existing_o);
+ }
+
+ //otherwise, use the existing object for that date
+ else {
+ processed_data.push(existing_o);
+ }
+ }
+ } else {
+ for (var j = 0; j < args.data[i].length; j += 1) {
+ var obj = MG.clone(args.data[i][j]);
+ obj['_missing'] = args.data[i][j][args.missing_is_hidden_accessor];
+ processed_data.push(obj);
+ }
+ }
+
+ // update our date object
+ args.data[i] = processed_data;
+ }
+ }
+
+ return this;
+}
+
+MG.process_line = process_line;
+
+function process_histogram(args) {
+ 'use strict';
+
+ // if args.binned == false, then we need to bin the data appropriately.
+ // if args.binned == true, then we need to make sure to compute the relevant computed data.
+ // the outcome of either of these should be something in args.computed_data.
+ // the histogram plotting function will be looking there for the data to plot.
+
+ // we need to compute an array of objects.
+ // each object has an x, y, and dx.
+
+ // histogram data is always single dimension
+ var our_data = args.data[0];
+
+ var extracted_data;
+ if (args.binned === false) {
+ // use d3's built-in layout.histogram functionality to compute what you need.
+
+ if (typeof(our_data[0]) === 'object') {
+ // we are dealing with an array of objects. Extract the data value of interest.
+ extracted_data = our_data
+ .map(function(d) {
+ return d[args.x_accessor];
+ });
+ } else if (typeof(our_data[0]) === 'number') {
+ // we are dealing with a simple array of numbers. No extraction needed.
+ extracted_data = our_data;
+ } else {
+ console.log('TypeError: expected an array of numbers, found ' + typeof(our_data[0]));
+ return;
+ }
+
+ var hist = d3.histogram();
+ if (args.bins) {
+ hist.thresholds(args.bins);
+ }
+
+ var bins = hist(extracted_data);
+ args.processed_data = bins.map(function(d) {
+ return { 'x': d.x0, 'y': d.length };
+ });
+ } else {
+ // here, we just need to reconstruct the array of objects
+ // take the x accessor and y accessor.
+ // pull the data as x and y. y is count.
+
+ args.processed_data = our_data.map(function(d) {
+ return { 'x': d[args.x_accessor], 'y': d[args.y_accessor] };
+ });
+
+ var this_pt;
+ var next_pt;
+
+ // we still need to compute the dx component for each data point
+ for (var i = 0; i < args.processed_data.length; i++) {
+ this_pt = args.processed_data[i];
+ if (i === args.processed_data.length - 1) {
+ this_pt.dx = args.processed_data[i - 1].dx;
+ } else {
+ next_pt = args.processed_data[i + 1];
+ this_pt.dx = next_pt.x - this_pt.x;
+ }
+ }
+ }
+
+ // capture the original data and accessors before replacing args.data
+ if (!args.processed) {
+ args.processed = {};
+ }
+ args.processed.original_data = args.data;
+ args.processed.original_x_accessor = args.x_accessor;
+ args.processed.original_y_accessor = args.y_accessor;
+
+ args.data = [args.processed_data];
+ args.x_accessor = args.processed_x_accessor;
+ args.y_accessor = args.processed_y_accessor;
+
+ return this;
+}
+
+MG.process_histogram = process_histogram;
+
+// for use with bar charts, etc.
+function process_categorical_variables(args) {
+ 'use strict';
+
+ var extracted_data, processed_data = {},
+ pd = [];
+ //var our_data = args.data[0];
+ var label_accessor = args.bar_orientation === 'vertical' ? args.x_accessor : args.y_accessor;
+ var data_accessor = args.bar_orientation === 'vertical' ? args.y_accessor : args.x_accessor;
+
+ return this;
+}
+
+MG.process_categorical_variables = process_categorical_variables;
+
+function process_point(args) {
+ 'use strict';
+
+ var data = args.data[0];
+ var x = data.map(function(d) {
+ return d[args.x_accessor];
+ });
+ var y = data.map(function(d) {
+ return d[args.y_accessor];
+ });
+
+ if (args.least_squares) {
+ args.ls_line = least_squares(x, y);
+ }
+
+ return this;
+}
+
+MG.process_point = process_point;
diff --git a/priv/static/js/metricsgraphics/misc/smoothers.js b/priv/static/js/metricsgraphics/misc/smoothers.js
new file mode 100644
index 0000000..aab8a00
--- /dev/null
+++ b/priv/static/js/metricsgraphics/misc/smoothers.js
@@ -0,0 +1,280 @@
+function add_ls(args) {
+ var svg = mg_get_svg_child_of(args.target);
+ var data = args.data[0];
+ var min_x = d3.min(data, function(d) {
+ return d[args.x_accessor]; });
+ var max_x = d3.max(data, function(d) {
+ return d[args.x_accessor]; });
+
+ d3.select(args.target).selectAll('.mg-least-squares-line').remove();
+
+ svg.append('svg:line')
+ .attr('x1', args.scales.X(min_x))
+ .attr('x2', args.scales.X(max_x))
+ .attr('y1', args.scales.Y(args.ls_line.fit(min_x)))
+ .attr('y2', args.scales.Y(args.ls_line.fit(max_x)))
+ .attr('class', 'mg-least-squares-line');
+}
+
+MG.add_ls = add_ls;
+
+function add_lowess(args) {
+ var svg = mg_get_svg_child_of(args.target);
+ var lowess = args.lowess_line;
+
+ var line = d3.svg.line()
+ .x(function(d) {
+ return args.scales.X(d.x); })
+ .y(function(d) {
+ return args.scales.Y(d.y); })
+ .interpolate(args.interpolate);
+
+ svg.append('path')
+ .attr('d', line(lowess))
+ .attr('class', 'mg-lowess-line');
+}
+
+MG.add_lowess = add_lowess;
+
+function lowess_robust(x, y, alpha, inc) {
+ // Used http://www.unc.edu/courses/2007spring/biol/145/001/docs/lectures/Oct27.html
+ // for the clear explanation of robust lowess.
+
+ // calculate the the first pass.
+ var _l;
+ var r = [];
+ var yhat = d3.mean(y);
+ var i;
+ for (i = 0; i < x.length; i += 1) { r.push(1); }
+ _l = _calculate_lowess_fit(x, y, alpha, inc, r);
+ var x_proto = _l.x;
+ var y_proto = _l.y;
+
+ // Now, take the fit, recalculate the weights, and re-run LOWESS using r*w instead of w.
+
+ for (i = 0; i < 100; i += 1) {
+ r = d3.zip(y_proto, y).map(function(yi) {
+ return Math.abs(yi[1] - yi[0]);
+ });
+
+ var q = d3.quantile(r.sort(), 0.5);
+
+ r = r.map(function(ri) {
+ return _bisquare_weight(ri / (6 * q));
+ });
+
+ _l = _calculate_lowess_fit(x, y, alpha, inc, r);
+ x_proto = _l.x;
+ y_proto = _l.y;
+ }
+
+ return d3.zip(x_proto, y_proto).map(function(d) {
+ var p = {};
+ p.x = d[0];
+ p.y = d[1];
+ return p;
+ });
+}
+
+MG.lowess_robust = lowess_robust;
+
+function lowess(x, y, alpha, inc) {
+ var r = [];
+ for (var i = 0; i < x.length; i += 1) { r.push(1); }
+ var _l = _calculate_lowess_fit(x, y, alpha, inc, r);
+}
+
+MG.lowess = lowess;
+
+function least_squares(x_, y_) {
+ var x, y, xi, yi,
+ _x = 0,
+ _y = 0,
+ _xy = 0,
+ _xx = 0;
+
+ var n = x_.length;
+ if (mg_is_date(x_[0])) {
+ x = x_.map(function(d) {
+ return d.getTime();
+ });
+ } else {
+ x = x_;
+ }
+
+ if (mg_is_date(y_[0])) {
+ y = y_.map(function(d) {
+ return d.getTime();
+ });
+ } else {
+ y = y_;
+ }
+
+ var xhat = d3.mean(x);
+ var yhat = d3.mean(y);
+ var numerator = 0,
+ denominator = 0;
+
+ for (var i = 0; i < x.length; i++) {
+ xi = x[i];
+ yi = y[i];
+ numerator += (xi - xhat) * (yi - yhat);
+ denominator += (xi - xhat) * (xi - xhat);
+ }
+
+ var beta = numerator / denominator;
+ var x0 = yhat - beta * xhat;
+
+ return {
+ x0: x0,
+ beta: beta,
+ fit: function(x) {
+ return x0 + x * beta;
+ }
+ };
+}
+
+MG.least_squares = least_squares;
+
+function _pow_weight(u, w) {
+ if (u >= 0 && u <= 1) {
+ return Math.pow(1 - Math.pow(u, w), w);
+ } else {
+ return 0;
+ }
+}
+
+function _bisquare_weight(u) {
+ return _pow_weight(u, 2);
+}
+
+function _tricube_weight(u) {
+ return _pow_weight(u, 3);
+}
+
+function _neighborhood_width(x0, xis) {
+ return Array.max(xis.map(function(xi) {
+ return Math.abs(x0 - xi);
+ }));
+}
+
+function _manhattan(x1, x2) {
+ return Math.abs(x1 - x2);
+}
+
+function _weighted_means(wxy) {
+ var wsum = d3.sum(wxy.map(function(wxyi) {
+ return wxyi.w; }));
+
+ return {
+ xbar: d3.sum(wxy.map(function(wxyi) {
+ return wxyi.w * wxyi.x;
+ })) / wsum,
+ ybar: d3.sum(wxy.map(function(wxyi) {
+ return wxyi.w * wxyi.y;
+ })) / wsum
+ };
+}
+
+function _weighted_beta(wxy, xbar, ybar) {
+ var num = d3.sum(wxy.map(function(wxyi) {
+ return Math.pow(wxyi.w, 2) * (wxyi.x - xbar) * (wxyi.y - ybar);
+ }));
+
+ var denom = d3.sum(wxy.map(function(wxyi) {
+ return Math.pow(wxyi.w, 2) * Math.pow(wxyi.x - xbar, 2);
+ }));
+
+ return num / denom;
+}
+
+function _weighted_least_squares(wxy) {
+ var ybar, xbar, beta_i, x0;
+
+ var _wm = _weighted_means(wxy);
+
+ xbar = _wm.xbar;
+ ybar = _wm.ybar;
+
+ var beta = _weighted_beta(wxy, xbar, ybar);
+
+ return {
+ beta: beta,
+ xbar: xbar,
+ ybar: ybar,
+ x0: ybar - beta * xbar
+
+ };
+}
+
+function _calculate_lowess_fit(x, y, alpha, inc, residuals) {
+ // alpha - smoothing factor. 0 < alpha < 1/
+ //
+ //
+ var k = Math.floor(x.length * alpha);
+
+ var sorted_x = x.slice();
+
+ sorted_x.sort(function(a, b) {
+ if (a < b) {
+ return -1; } else if (a > b) {
+ return 1; }
+
+ return 0;
+ });
+
+ var x_max = d3.quantile(sorted_x, 0.98);
+ var x_min = d3.quantile(sorted_x, 0.02);
+
+ var xy = d3.zip(x, y, residuals).sort();
+
+ var size = Math.abs(x_max - x_min) / inc;
+
+ var smallest = x_min;
+ var largest = x_max;
+ var x_proto = d3.range(smallest, largest, size);
+
+ var xi_neighbors;
+ var x_i, beta_i, x0_i, delta_i, xbar, ybar;
+
+ // for each prototype, find its fit.
+ var y_proto = [];
+
+ for (var i = 0; i < x_proto.length; i += 1) {
+ x_i = x_proto[i];
+
+ // get k closest neighbors.
+ xi_neighbors = xy.map(function(xyi) {
+ return [
+ Math.abs(xyi[0] - x_i),
+ xyi[0],
+ xyi[1],
+ xyi[2]
+ ];
+ }).sort().slice(0, k);
+
+ // Get the largest distance in the neighbor set.
+ delta_i = d3.max(xi_neighbors)[0];
+
+ // Prepare the weights for mean calculation and WLS.
+
+ xi_neighbors = xi_neighbors.map(function(wxy) {
+ return {
+ w: _tricube_weight(wxy[0] / delta_i) * wxy[3],
+ x: wxy[1],
+ y: wxy[2]
+ };
+ });
+
+ // Find the weighted least squares, obviously.
+ var _output = _weighted_least_squares(xi_neighbors);
+
+ x0_i = _output.x0;
+ beta_i = _output.beta;
+
+ //
+ y_proto.push(x0_i + beta_i * x_i);
+ }
+
+ return { x: x_proto, y: y_proto };
+}
diff --git a/priv/static/js/metricsgraphics/misc/transitions.js b/priv/static/js/metricsgraphics/misc/transitions.js
new file mode 100644
index 0000000..da1e503
--- /dev/null
+++ b/priv/static/js/metricsgraphics/misc/transitions.js
@@ -0,0 +1,31 @@
+// http://bl.ocks.org/mbostock/3916621
+function path_tween(d1, precision) {
+ return function() {
+ var path0 = this,
+ path1 = path0.cloneNode(),
+ n0 = path0.getTotalLength() || 0,
+ n1 = (path1.setAttribute("d", d1), path1).getTotalLength() || 0;
+
+ // Uniform sampling of distance based on specified precision.
+ var distances = [0],
+ i = 0,
+ dt = precision / Math.max(n0, n1);
+ while ((i += dt) < 1) distances.push(i);
+ distances.push(1);
+
+ // Compute point-interpolators at each distance.
+ var points = distances.map(function(t) {
+ var p0 = path0.getPointAtLength(t * n0),
+ p1 = path1.getPointAtLength(t * n1);
+ return d3.interpolate([p0.x, p0.y], [p1.x, p1.y]);
+ });
+
+ return function(t) {
+ return t < 1 ? "M" + points.map(function(p) {
+ return p(t);
+ }).join("L") : d1;
+ };
+ };
+}
+
+MG.path_tween = path_tween;
diff --git a/priv/static/js/metricsgraphics/misc/utility.js b/priv/static/js/metricsgraphics/misc/utility.js
new file mode 100644
index 0000000..f68327b
--- /dev/null
+++ b/priv/static/js/metricsgraphics/misc/utility.js
@@ -0,0 +1,619 @@
+//a set of helper functions, some that we've written, others that we've borrowed
+
+MG.convert = {};
+
+MG.convert.date = function(data, accessor, time_format) {
+ time_format = (typeof time_format === "undefined") ? '%Y-%m-%d' : time_format;
+ var parse_time = d3.timeParse(time_format);
+ data = data.map(function(d) {
+ d[accessor] = parse_time(d[accessor].trim());
+ return d;
+ });
+
+ return data;
+};
+
+MG.convert.number = function(data, accessor) {
+ data = data.map(function(d) {
+ d[accessor] = Number(d[accessor]);
+ return d;
+ });
+
+ return data;
+};
+
+MG.time_format = function(utc, specifier) {
+ return utc ? d3.utcFormat(specifier) : d3.timeFormat(specifier);
+};
+
+function mg_jquery_exists() {
+ if (typeof jQuery !== 'undefined' || typeof $ !== 'undefined') {
+ return true;
+ } else {
+ return false;
+ }
+}
+
+function mg_get_rollover_time_format(args) {
+ // if a rollover time format is defined, use that
+ if (args.rollover_time_format) {
+ return MG.time_format(args.utc_time, args.rollover_time_format);
+ }
+
+ switch (args.processed.x_time_frame) {
+ case 'millis':
+ return MG.time_format(args.utc_time, '%b %e, %Y %H:%M:%S.%L');
+ case 'seconds':
+ return MG.time_format(args.utc_time, '%b %e, %Y %H:%M:%S');
+ case 'less-than-a-day':
+ return MG.time_format(args.utc_time, '%b %e, %Y %I:%M%p');
+ case 'four-days':
+ return MG.time_format(args.utc_time, '%b %e, %Y %I:%M%p');
+ }
+
+ // default
+ return MG.time_format(args.utc_time, '%b %e, %Y');
+}
+
+function mg_data_in_plot_bounds(datum, args) {
+ return datum[args.x_accessor] >= args.processed.min_x &&
+ datum[args.x_accessor] <= args.processed.max_x &&
+ datum[args.y_accessor] >= args.processed.min_y &&
+ datum[args.y_accessor] <= args.processed.max_y;
+}
+
+function is_array(thing) {
+ return Object.prototype.toString.call(thing) === '[object Array]';
+}
+
+function is_function(thing) {
+ return Object.prototype.toString.call(thing) === '[object Function]';
+}
+
+function is_empty_array(thing) {
+ return is_array(thing) && thing.length === 0;
+}
+
+function is_object(thing) {
+ return Object.prototype.toString.call(thing) === '[object Object]';
+}
+
+function is_array_of_arrays(data) {
+ var all_elements = data.map(function(d) {
+ return is_array(d) === true && d.length > 0;
+ });
+
+ return d3.sum(all_elements) === data.length;
+}
+
+function is_array_of_objects(data) {
+ // is every element of data an object?
+ var all_elements = data.map(function(d) {
+ return is_object(d) === true;
+ });
+
+ return d3.sum(all_elements) === data.length;
+}
+
+function is_array_of_objects_or_empty(data) {
+ return is_empty_array(data) || is_array_of_objects(data);
+}
+
+function pluck(arr, accessor) {
+ return arr.map(function(d) {
+ return d[accessor]; });
+}
+
+function count_array_elements(arr) {
+ return arr.reduce(function(a, b) { a[b] = a[b] + 1 || 1;
+ return a; }, {});
+}
+
+function mg_get_bottom(args) {
+ return args.height - args.bottom;
+}
+
+function mg_get_plot_bottom(args) {
+ // returns the pixel location of the bottom side of the plot area.
+ return mg_get_bottom(args) - args.buffer;
+}
+
+function mg_get_top(args) {
+ return args.top;
+}
+
+function mg_get_plot_top(args) {
+ // returns the pixel location of the top side of the plot area.
+ return mg_get_top(args) + args.buffer;
+}
+
+function mg_get_left(args) {
+ return args.left;
+}
+
+function mg_get_plot_left(args) {
+ // returns the pixel location of the left side of the plot area.
+ return mg_get_left(args) + args.buffer;
+}
+
+function mg_get_right(args) {
+ return args.width - args.right;
+}
+
+function mg_get_plot_right(args) {
+ // returns the pixel location of the right side of the plot area.
+ return mg_get_right(args) - args.buffer;
+}
+
+//////// adding elements, removing elements /////////////
+
+function mg_exit_and_remove(elem) {
+ elem.exit().remove();
+}
+
+function mg_selectAll_and_remove(svg, cl) {
+ svg.selectAll(cl).remove();
+}
+
+function mg_add_g(svg, cl) {
+ return svg.append('g').classed(cl, true);
+}
+
+function mg_remove_element(svg, elem) {
+ svg.select(elem).remove();
+}
+
+//////// axis helper functions ////////////
+
+function mg_make_rug(args, rug_class) {
+ var svg = mg_get_svg_child_of(args.target);
+ var all_data = mg_flatten_array(args.data);
+ var rug = svg.selectAll('line.' + rug_class).data(all_data);
+
+ rug.enter()
+ .append('line')
+ .attr('class', rug_class)
+ .attr('opacity', 0.3);
+
+ //remove rug elements that are no longer in use
+ mg_exit_and_remove(rug);
+
+ //set coordinates of new rug elements
+ mg_exit_and_remove(rug);
+ return rug;
+}
+
+function mg_add_color_accessor_to_rug(rug, args, rug_mono_class) {
+ if (args.color_accessor) {
+ rug.attr('stroke', args.scalefns.colorf);
+ rug.classed(rug_mono_class, false);
+ } else {
+ rug.attr('stroke', null);
+ rug.classed(rug_mono_class, true);
+ }
+}
+
+function mg_rotate_labels(labels, rotation_degree) {
+ if (rotation_degree) {
+ labels.attr({
+ dy: 0,
+ transform: function() {
+ var elem = d3.select(this);
+ return 'rotate(' + rotation_degree + ' ' + elem.attr('x') + ',' + elem.attr('y') + ')';
+ }
+ });
+ }
+}
+
+//////////////////////////////////////////////////
+
+function mg_elements_are_overlapping(labels) {
+ labels = labels.node();
+ if (!labels) {
+ return false;
+ }
+
+ for (var i = 0; i < labels.length; i++) {
+ if (mg_is_horizontally_overlapping(labels[i], labels)) return true;
+ }
+
+ return false;
+}
+
+function mg_prevent_horizontal_overlap(labels, args) {
+ if (!labels || labels.length == 1) {
+ return;
+ }
+
+ //see if each of our labels overlaps any of the other labels
+ for (var i = 0; i < labels.length; i++) {
+ //if so, nudge it up a bit, if the label it intersects hasn't already been nudged
+ if (mg_is_horizontally_overlapping(labels[i], labels)) {
+ var node = d3.select(labels[i]);
+ var newY = +node.attr('y');
+ if (newY + 8 >= args.top) {
+ newY = args.top - 16;
+ }
+ node.attr('y', newY);
+ }
+ }
+}
+
+function mg_prevent_vertical_overlap(labels, args) {
+ if (!labels || labels.length == 1) {
+ return;
+ }
+
+ labels.sort(function(b, a) {
+ return d3.select(a).attr('y') - d3.select(b).attr('y');
+ });
+
+ labels.reverse();
+
+ var overlap_amount, label_i, label_j;
+
+ //see if each of our labels overlaps any of the other labels
+ for (var i = 0; i < labels.length; i++) {
+ //if so, nudge it up a bit, if the label it intersects hasn't already been nudged
+ label_i = d3.select(labels[i]).text();
+
+ for (var j = 0; j < labels.length; j++) {
+ label_j = d3.select(labels[j]).text();
+ overlap_amount = mg_is_vertically_overlapping(labels[i], labels[j]);
+
+ if (overlap_amount !== false && label_i !== label_j) {
+ var node = d3.select(labels[i]);
+ var newY = +node.attr('y');
+ newY = newY + overlap_amount;
+ node.attr('y', newY);
+ }
+ }
+ }
+}
+
+function mg_is_vertically_overlapping(element, sibling) {
+ var element_bbox = element.getBoundingClientRect();
+ var sibling_bbox = sibling.getBoundingClientRect();
+
+ if (element_bbox.top <= sibling_bbox.bottom && element_bbox.top >= sibling_bbox.top) {
+ return sibling_bbox.bottom - element_bbox.top;
+ }
+
+ return false;
+}
+
+function mg_is_horiz_overlap(element, sibling) {
+ var element_bbox = element.getBoundingClientRect();
+ var sibling_bbox = sibling.getBoundingClientRect();
+
+ if (element_bbox.right >= sibling_bbox.left || element_bbox.top >= sibling_bbox.top) {
+ return sibling_bbox.bottom - element_bbox.top;
+ }
+ return false;
+}
+
+function mg_is_horizontally_overlapping(element, labels) {
+ var element_bbox = element.getBoundingClientRect();
+
+ for (var i = 0; i < labels.length; i++) {
+ if (labels[i] == element) {
+ continue;
+ }
+
+ //check to see if this label overlaps with any of the other labels
+ var sibling_bbox = labels[i].getBoundingClientRect();
+ if (element_bbox.top === sibling_bbox.top &&
+ !(sibling_bbox.left > element_bbox.right || sibling_bbox.right < element_bbox.left)
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+function mg_infer_type(args, ns) {
+ // must return categorical or numerical.
+ var testPoint = mg_flatten_array(args.data);
+
+ testPoint = testPoint[0][args[ns + '_accessor']];
+ return typeof testPoint === 'string' ? 'categorical' : 'numerical';
+ }
+
+function mg_get_svg_child_of(selector_or_node) {
+ return d3.select(selector_or_node).select('svg');
+}
+
+function mg_flatten_array(arr) {
+ var flat_data = [];
+ return flat_data.concat.apply(flat_data, arr);
+}
+
+function mg_next_id() {
+ if (typeof MG._next_elem_id === 'undefined') {
+ MG._next_elem_id = 0;
+ }
+
+ return 'mg-' + (MG._next_elem_id++);
+}
+
+function mg_target_ref(target) {
+ if (typeof target === 'string') {
+ return mg_normalize(target);
+
+ } else if (target instanceof window.HTMLElement) {
+ var target_ref = target.getAttribute('data-mg-uid');
+ if (!target_ref) {
+ target_ref = mg_next_id();
+ target.setAttribute('data-mg-uid', target_ref);
+ }
+
+ return target_ref;
+
+ } else {
+ console.warn('The specified target should be a string or an HTMLElement.', target);
+ return mg_normalize(target);
+ }
+}
+
+function mg_normalize(string) {
+ return string
+ .replace(/[^a-zA-Z0-9 _-]+/g, '')
+ .replace(/ +?/g, '');
+}
+
+function get_pixel_dimension(target, dimension) {
+ return Number(d3.select(target).style(dimension).replace(/px/g, ''));
+}
+
+function get_width(target) {
+ return get_pixel_dimension(target, 'width');
+}
+
+function get_height(target) {
+ return get_pixel_dimension(target, 'height');
+}
+
+function isNumeric(n) {
+ return !isNaN(parseFloat(n)) && isFinite(n);
+}
+
+var each = function(obj, iterator, context) {
+ // yanked out of underscore
+ var breaker = {};
+ if (obj === null) return obj;
+ if (Array.prototype.forEach && obj.forEach === Array.prototype.forEach) {
+ obj.forEach(iterator, context);
+ } else if (obj.length === +obj.length) {
+ for (var i = 0, length = obj.length; i < length; i++) {
+ if (iterator.call(context, obj[i], i, obj) === breaker) return;
+ }
+ } else {
+ for (var k in obj) {
+ if (iterator.call(context, obj[k], k, obj) === breaker) return;
+ }
+ }
+
+ return obj;
+};
+
+function merge_with_defaults(obj) {
+ // taken from underscore
+ each(Array.prototype.slice.call(arguments, 1), function(source) {
+ if (source) {
+ for (var prop in source) {
+ if (obj[prop] === void 0) obj[prop] = source[prop];
+ }
+ }
+ });
+
+ return obj;
+}
+
+MG.merge_with_defaults = merge_with_defaults;
+
+function options_to_defaults(obj) {
+ return Object.keys(obj).reduce((r, k) => {
+ r[k] = obj[k][0];
+ return r;
+ }, {});
+}
+
+function compare_type(type, value) {
+ if (value == null) return true; // allow null or undefined
+ if (typeof type === 'string') {
+ if (type.substr(-2) === '[]') {
+ if (!is_array(value)) return false;
+ return value.every(i => compare_type(type.slice(0, -2), i));
+ }
+ return typeof value === type
+ || value === type
+ || type.length === 0
+ || type === 'array' && is_array(value);
+ }
+ if (typeof type === 'function') return value === type || value instanceof type;
+ return is_array(type) && !!~type.findIndex(i => compare_type(i, value));
+}
+
+function mg_validate_option(key, value) {
+ if (!is_array(MG.options[key])) return false; // non-existent option
+ const typeDef = MG.options[key][1];
+ if (!typeDef) return true; // not restricted type
+ return compare_type(typeDef, value);
+}
+
+function number_of_values(data, accessor, value) {
+ var values = data.filter(function(d) {
+ return d[accessor] === value;
+ });
+
+ return values.length;
+}
+
+function has_values_below(data, accessor, value) {
+ var values = data.filter(function(d) {
+ return d[accessor] <= value;
+ });
+
+ return values.length > 0;
+}
+
+function has_too_many_zeros(data, accessor, zero_count) {
+ return number_of_values(data, accessor, 0) >= zero_count;
+}
+
+function mg_is_date(obj) {
+ return Object.prototype.toString.call(obj) === '[object Date]';
+}
+
+function mg_is_object(obj) {
+ return Object.prototype.toString.call(obj) === '[object Object]';
+}
+
+function mg_is_array(obj) {
+ if (Array.isArray) {
+ return Array.isArray(obj);
+ }
+
+ return Object.prototype.toString.call(obj) === '[object Array]';
+}
+
+function mg_is_function(obj) {
+ return Object.prototype.toString.call(obj) === '[object Function]';
+}
+
+// deep copy
+// http://stackoverflow.com/questions/728360/most-elegant-way-to-clone-a-javascript-object
+MG.clone = function(obj) {
+ var copy;
+
+ // Handle the 3 simple types, and null or undefined
+ if (null === obj || "object" !== typeof obj) return obj;
+
+ // Handle Date
+ if (mg_is_date(obj)) {
+ copy = new Date();
+ copy.setTime(obj.getTime());
+ return copy;
+ }
+
+ // Handle Array
+ if (mg_is_array(obj)) {
+ copy = [];
+ for (var i = 0, len = obj.length; i < len; i++) {
+ copy[i] = MG.clone(obj[i]);
+ }
+ return copy;
+ }
+
+ // Handle Object
+ if (mg_is_object(obj)) {
+ copy = {};
+ for (var attr in obj) {
+ if (obj.hasOwnProperty(attr)) copy[attr] = MG.clone(obj[attr]);
+ }
+ return copy;
+ }
+
+ throw new Error("Unable to copy obj! Its type isn't supported.");
+};
+
+// give us the difference of two int arrays
+// http://radu.cotescu.com/javascript-diff-function/
+function arr_diff(a, b) {
+ var seen = [],
+ diff = [],
+ i;
+ for (i = 0; i < b.length; i++)
+ seen[b[i]] = true;
+ for (i = 0; i < a.length; i++)
+ if (!seen[a[i]])
+ diff.push(a[i]);
+ return diff;
+}
+
+MG.arr_diff = arr_diff;
+
+/**
+ Print warning message to the console when a feature has been scheduled for removal
+
+ @author Dan de Havilland (github.com/dandehavilland)
+ @date 2014-12
+*/
+function warn_deprecation(message, untilVersion) {
+ console.warn('Deprecation: ' + message + (untilVersion ? '. This feature will be removed in ' + untilVersion + '.' : ' the near future.'));
+ console.trace();
+}
+
+MG.warn_deprecation = warn_deprecation;
+
+/**
+ Truncate a string to fit within an SVG text node
+ CSS text-overlow doesn't apply to SVG <= 1.2
+
+ @author Dan de Havilland (github.com/dandehavilland)
+ @date 2014-12-02
+*/
+function truncate_text(textObj, textString, width) {
+ var bbox,
+ position = 0;
+
+ textObj.textContent = textString;
+ bbox = textObj.getBBox();
+
+ while (bbox.width > width) {
+ textObj.textContent = textString.slice(0, --position) + '...';
+ bbox = textObj.getBBox();
+
+ if (textObj.textContent === '...') {
+ break;
+ }
+ }
+}
+
+MG.truncate_text = truncate_text;
+
+/**
+ Wrap the contents of a text node to a specific width
+
+ Adapted from bl.ocks.org/mbostock/7555321
+
+ @author Mike Bostock
+ @author Dan de Havilland
+ @date 2015-01-14
+*/
+function wrap_text(text, width, token, tspanAttrs) {
+ text.each(function() {
+ var text = d3.select(this),
+ words = text.text().split(token || /\s+/).reverse(),
+ word,
+ line = [],
+ lineNumber = 0,
+ lineHeight = 1.1, // ems
+ y = text.attr("y"),
+ dy = 0,
+ tspan = text.text(null)
+ .append("tspan")
+ .attr("x", 0)
+ .attr("y", dy + "em")
+ .attr(tspanAttrs || {});
+
+ while (!!(word = words.pop())) {
+ line.push(word);
+ tspan.text(line.join(" "));
+ if (width === null || tspan.node().getComputedTextLength() > width) {
+ line.pop();
+ tspan.text(line.join(" "));
+ line = [word];
+ tspan = text
+ .append("tspan")
+ .attr("x", 0)
+ .attr("y", ++lineNumber * lineHeight + dy + "em")
+ .attr(tspanAttrs || {})
+ .text(word);
+ }
+ }
+ });
+}
+
+MG.wrap_text = wrap_text;