on the whole page.
+ left += chartContainer.offsetLeft + svgOffset.left - 2*chartContainer.scrollLeft;
+ top += chartContainer.offsetTop + svgOffset.top - 2*chartContainer.scrollTop;
+
+ if (snapDistance && snapDistance > 0) {
+ top = Math.floor(top/snapDistance) * snapDistance;
+ }
+ calcTooltipPosition([left,top]);
+ });
+ } else {
+ calcTooltipPosition([left,top]);
+ }
+ });
+
+ return nvtooltip;
+ }
+
+ nvtooltip.nvPointerEventsClass = nvPointerEventsClass;
+ nvtooltip.options = nv.utils.optionsFunc.bind(nvtooltip);
+
+ nvtooltip._options = Object.create({}, {
+ // simple read/write options
+ duration: {get: function(){return duration;}, set: function(_){duration=_;}},
+ gravity: {get: function(){return gravity;}, set: function(_){gravity=_;}},
+ distance: {get: function(){return distance;}, set: function(_){distance=_;}},
+ snapDistance: {get: function(){return snapDistance;}, set: function(_){snapDistance=_;}},
+ classes: {get: function(){return classes;}, set: function(_){classes=_;}},
+ chartContainer: {get: function(){return chartContainer;}, set: function(_){chartContainer=_;}},
+ fixedTop: {get: function(){return fixedTop;}, set: function(_){fixedTop=_;}},
+ enabled: {get: function(){return enabled;}, set: function(_){enabled=_;}},
+ hideDelay: {get: function(){return hideDelay;}, set: function(_){hideDelay=_;}},
+ contentGenerator: {get: function(){return contentGenerator;}, set: function(_){contentGenerator=_;}},
+ valueFormatter: {get: function(){return valueFormatter;}, set: function(_){valueFormatter=_;}},
+ headerFormatter: {get: function(){return headerFormatter;}, set: function(_){headerFormatter=_;}},
+ keyFormatter: {get: function(){return keyFormatter;}, set: function(_){keyFormatter=_;}},
+ headerEnabled: {get: function(){return headerEnabled;}, set: function(_){headerEnabled=_;}},
+
+ // internal use only, set by interactive layer to adjust position.
+ _isInteractiveLayer: {get: function(){return isInteractiveLayer;}, set: function(_){isInteractiveLayer=!!_;}},
+
+ // options with extra logic
+ position: {get: function(){return position;}, set: function(_){
+ position.left = _.left !== undefined ? _.left : position.left;
+ position.top = _.top !== undefined ? _.top : position.top;
+ }},
+ offset: {get: function(){return offset;}, set: function(_){
+ offset.left = _.left !== undefined ? _.left : offset.left;
+ offset.top = _.top !== undefined ? _.top : offset.top;
+ }},
+ hidden: {get: function(){return hidden;}, set: function(_){
+ if (hidden != _) {
+ hidden = !!_;
+ nvtooltip();
+ }
+ }},
+ data: {get: function(){return data;}, set: function(_){
+ // if showing a single data point, adjust data format with that
+ if (_.point) {
+ _.value = _.point.x;
+ _.series = _.series || {};
+ _.series.value = _.point.y;
+ _.series.color = _.point.color || _.series.color;
+ }
+ data = _;
+ }},
+
+ // read only properties
+ tooltipElem: {get: function(){return tooltipElem;}, set: function(_){}},
+ id: {get: function(){return id;}, set: function(_){}}
+ });
+
+ nv.utils.initOptions(nvtooltip);
+ return nvtooltip;
+ };
+
+ })();
+
+
+ /*
+ Gets the browser window size
+
+ Returns object with height and width properties
+ */
+ nv.utils.windowSize = function() {
+ // Sane defaults
+ var size = {width: 640, height: 480};
+
+ // Most recent browsers use
+ if (window.innerWidth && window.innerHeight) {
+ size.width = window.innerWidth;
+ size.height = window.innerHeight;
+ return (size);
+ }
+
+ // IE can use depending on mode it is in
+ if (document.compatMode=='CSS1Compat' &&
+ document.documentElement &&
+ document.documentElement.offsetWidth ) {
+
+ size.width = document.documentElement.offsetWidth;
+ size.height = document.documentElement.offsetHeight;
+ return (size);
+ }
+
+ // Earlier IE uses Doc.body
+ if (document.body && document.body.offsetWidth) {
+ size.width = document.body.offsetWidth;
+ size.height = document.body.offsetHeight;
+ return (size);
+ }
+
+ return (size);
+ };
+
+ /*
+ Binds callback function to run when window is resized
+ */
+ nv.utils.windowResize = function(handler) {
+ if (window.addEventListener) {
+ window.addEventListener('resize', handler);
+ } else {
+ nv.log("ERROR: Failed to bind to window.resize with: ", handler);
+ }
+ // return object with clear function to remove the single added callback.
+ return {
+ callback: handler,
+ clear: function() {
+ window.removeEventListener('resize', handler);
+ }
+ }
+ };
+
+
+ /*
+ Backwards compatible way to implement more d3-like coloring of graphs.
+ Can take in nothing, an array, or a function/scale
+ To use a normal scale, get the range and pass that because we must be able
+ to take two arguments and use the index to keep backward compatibility
+ */
+ nv.utils.getColor = function(color) {
+ //if you pass in nothing, get default colors back
+ if (color === undefined) {
+ return nv.utils.defaultColor();
+
+ //if passed an array, turn it into a color scale
+ // use isArray, instanceof fails if d3 range is created in an iframe
+ } else if(Array.isArray(color)) {
+ var color_scale = d3.scale.ordinal().range(color);
+ return function(d, i) {
+ var key = i === undefined ? d : i;
+ return d.color || color_scale(key);
+ };
+
+ //if passed a function or scale, return it, or whatever it may be
+ //external libs, such as angularjs-nvd3-directives use this
+ } else {
+ //can't really help it if someone passes rubbish as color
+ return color;
+ }
+ };
+
+
+ /*
+ Default color chooser uses a color scale of 20 colors from D3
+ https://github.com/mbostock/d3/wiki/Ordinal-Scales#categorical-colors
+ */
+ nv.utils.defaultColor = function() {
+ // get range of the scale so we'll turn it into our own function.
+ return nv.utils.getColor(d3.scale.category20().range());
+ };
+
+
+ /*
+ Returns a color function that takes the result of 'getKey' for each series and
+ looks for a corresponding color from the dictionary
+ */
+ nv.utils.customTheme = function(dictionary, getKey, defaultColors) {
+ // use default series.key if getKey is undefined
+ getKey = getKey || function(series) { return series.key };
+ defaultColors = defaultColors || d3.scale.category20().range();
+
+ // start at end of default color list and walk back to index 0
+ var defIndex = defaultColors.length;
+
+ return function(series, index) {
+ var key = getKey(series);
+ if (typeof dictionary[key] === 'function') {
+ return dictionary[key]();
+ } else if (dictionary[key] !== undefined) {
+ return dictionary[key];
+ } else {
+ // no match in dictionary, use a default color
+ if (!defIndex) {
+ // used all the default colors, start over
+ defIndex = defaultColors.length;
+ }
+ defIndex = defIndex - 1;
+ return defaultColors[defIndex];
+ }
+ };
+ };
+
+
+ /*
+ From the PJAX example on d3js.org, while this is not really directly needed
+ it's a very cool method for doing pjax, I may expand upon it a little bit,
+ open to suggestions on anything that may be useful
+ */
+ nv.utils.pjax = function(links, content) {
+
+ var load = function(href) {
+ d3.html(href, function(fragment) {
+ var target = d3.select(content).node();
+ target.parentNode.replaceChild(
+ d3.select(fragment).select(content).node(),
+ target);
+ nv.utils.pjax(links, content);
+ });
+ };
+
+ d3.selectAll(links).on("click", function() {
+ history.pushState(this.href, this.textContent, this.href);
+ load(this.href);
+ d3.event.preventDefault();
+ });
+
+ d3.select(window).on("popstate", function() {
+ if (d3.event.state) {
+ load(d3.event.state);
+ }
+ });
+ };
+
+
+ /*
+ For when we want to approximate the width in pixels for an SVG:text element.
+ Most common instance is when the element is in a display:none; container.
+ Forumla is : text.length * font-size * constant_factor
+ */
+ nv.utils.calcApproxTextWidth = function (svgTextElem) {
+ if (typeof svgTextElem.style === 'function'
+ && typeof svgTextElem.text === 'function') {
+
+ var fontSize = parseInt(svgTextElem.style("font-size").replace("px",""), 10);
+ var textLength = svgTextElem.text().length;
+ return textLength * fontSize * 0.5;
+ }
+ return 0;
+ };
+
+
+ /*
+ Numbers that are undefined, null or NaN, convert them to zeros.
+ */
+ nv.utils.NaNtoZero = function(n) {
+ if (typeof n !== 'number'
+ || isNaN(n)
+ || n === null
+ || n === Infinity
+ || n === -Infinity) {
+
+ return 0;
+ }
+ return n;
+ };
+
+ /*
+ Add a way to watch for d3 transition ends to d3
+ */
+ d3.selection.prototype.watchTransition = function(renderWatch){
+ var args = [this].concat([].slice.call(arguments, 1));
+ return renderWatch.transition.apply(renderWatch, args);
+ };
+
+
+ /*
+ Helper object to watch when d3 has rendered something
+ */
+ nv.utils.renderWatch = function(dispatch, duration) {
+ if (!(this instanceof nv.utils.renderWatch)) {
+ return new nv.utils.renderWatch(dispatch, duration);
+ }
+
+ var _duration = duration !== undefined ? duration : 250;
+ var renderStack = [];
+ var self = this;
+
+ this.models = function(models) {
+ models = [].slice.call(arguments, 0);
+ models.forEach(function(model){
+ model.__rendered = false;
+ (function(m){
+ m.dispatch.on('renderEnd', function(arg){
+ m.__rendered = true;
+ self.renderEnd('model');
+ });
+ })(model);
+
+ if (renderStack.indexOf(model) < 0) {
+ renderStack.push(model);
+ }
+ });
+ return this;
+ };
+
+ this.reset = function(duration) {
+ if (duration !== undefined) {
+ _duration = duration;
+ }
+ renderStack = [];
+ };
+
+ this.transition = function(selection, args, duration) {
+ args = arguments.length > 1 ? [].slice.call(arguments, 1) : [];
+
+ if (args.length > 1) {
+ duration = args.pop();
+ } else {
+ duration = _duration !== undefined ? _duration : 250;
+ }
+ selection.__rendered = false;
+
+ if (renderStack.indexOf(selection) < 0) {
+ renderStack.push(selection);
+ }
+
+ if (duration === 0) {
+ selection.__rendered = true;
+ selection.delay = function() { return this; };
+ selection.duration = function() { return this; };
+ return selection;
+ } else {
+ if (selection.length === 0) {
+ selection.__rendered = true;
+ } else if (selection.every( function(d){ return !d.length; } )) {
+ selection.__rendered = true;
+ } else {
+ selection.__rendered = false;
+ }
+
+ var n = 0;
+ return selection
+ .transition()
+ .duration(duration)
+ .each(function(){ ++n; })
+ .each('end', function(d, i) {
+ if (--n === 0) {
+ selection.__rendered = true;
+ self.renderEnd.apply(this, args);
+ }
+ });
+ }
+ };
+
+ this.renderEnd = function() {
+ if (renderStack.every( function(d){ return d.__rendered; } )) {
+ renderStack.forEach( function(d){ d.__rendered = false; });
+ dispatch.renderEnd.apply(this, arguments);
+ }
+ }
+
+ };
+
+
+ /*
+ Takes multiple objects and combines them into the first one (dst)
+ example: nv.utils.deepExtend({a: 1}, {a: 2, b: 3}, {c: 4});
+ gives: {a: 2, b: 3, c: 4}
+ */
+ nv.utils.deepExtend = function(dst){
+ var sources = arguments.length > 1 ? [].slice.call(arguments, 1) : [];
+ sources.forEach(function(source) {
+ for (var key in source) {
+ var isArray = dst[key] instanceof Array;
+ var isObject = typeof dst[key] === 'object';
+ var srcObj = typeof source[key] === 'object';
+
+ if (isObject && !isArray && srcObj) {
+ nv.utils.deepExtend(dst[key], source[key]);
+ } else {
+ dst[key] = source[key];
+ }
+ }
+ });
+ };
+
+
+ /*
+ state utility object, used to track d3 states in the models
+ */
+ nv.utils.state = function(){
+ if (!(this instanceof nv.utils.state)) {
+ return new nv.utils.state();
+ }
+ var state = {};
+ var _self = this;
+ var _setState = function(){};
+ var _getState = function(){ return {}; };
+ var init = null;
+ var changed = null;
+
+ this.dispatch = d3.dispatch('change', 'set');
+
+ this.dispatch.on('set', function(state){
+ _setState(state, true);
+ });
+
+ this.getter = function(fn){
+ _getState = fn;
+ return this;
+ };
+
+ this.setter = function(fn, callback) {
+ if (!callback) {
+ callback = function(){};
+ }
+ _setState = function(state, update){
+ fn(state);
+ if (update) {
+ callback();
+ }
+ };
+ return this;
+ };
+
+ this.init = function(state){
+ init = init || {};
+ nv.utils.deepExtend(init, state);
+ };
+
+ var _set = function(){
+ var settings = _getState();
+
+ if (JSON.stringify(settings) === JSON.stringify(state)) {
+ return false;
+ }
+
+ for (var key in settings) {
+ if (state[key] === undefined) {
+ state[key] = {};
+ }
+ state[key] = settings[key];
+ changed = true;
+ }
+ return true;
+ };
+
+ this.update = function(){
+ if (init) {
+ _setState(init, false);
+ init = null;
+ }
+ if (_set.call(this)) {
+ this.dispatch.change(state);
+ }
+ };
+
+ };
+
+
+ /*
+ Snippet of code you can insert into each nv.models.* to give you the ability to
+ do things like:
+ chart.options({
+ showXAxis: true,
+ tooltips: true
+ });
+
+ To enable in the chart:
+ chart.options = nv.utils.optionsFunc.bind(chart);
+ */
+ nv.utils.optionsFunc = function(args) {
+ if (args) {
+ d3.map(args).forEach((function(key,value) {
+ if (typeof this[key] === "function") {
+ this[key](value);
+ }
+ }).bind(this));
+ }
+ return this;
+ };
+
+
+ /*
+ numTicks: requested number of ticks
+ data: the chart data
+
+ returns the number of ticks to actually use on X axis, based on chart data
+ to avoid duplicate ticks with the same value
+ */
+ nv.utils.calcTicksX = function(numTicks, data) {
+ // find max number of values from all data streams
+ var numValues = 1;
+ var i = 0;
+ for (i; i < data.length; i += 1) {
+ var stream_len = data[i] && data[i].values ? data[i].values.length : 0;
+ numValues = stream_len > numValues ? stream_len : numValues;
+ }
+ nv.log("Requested number of ticks: ", numTicks);
+ nv.log("Calculated max values to be: ", numValues);
+ // make sure we don't have more ticks than values to avoid duplicates
+ numTicks = numTicks > numValues ? numTicks = numValues - 1 : numTicks;
+ // make sure we have at least one tick
+ numTicks = numTicks < 1 ? 1 : numTicks;
+ // make sure it's an integer
+ numTicks = Math.floor(numTicks);
+ nv.log("Calculating tick count as: ", numTicks);
+ return numTicks;
+ };
+
+
+ /*
+ returns number of ticks to actually use on Y axis, based on chart data
+ */
+ nv.utils.calcTicksY = function(numTicks, data) {
+ // currently uses the same logic but we can adjust here if needed later
+ return nv.utils.calcTicksX(numTicks, data);
+ };
+
+
+ /*
+ Add a particular option from an options object onto chart
+ Options exposed on a chart are a getter/setter function that returns chart
+ on set to mimic typical d3 option chaining, e.g. svg.option1('a').option2('b');
+
+ option objects should be generated via Object.create() to provide
+ the option of manipulating data via get/set functions.
+ */
+ nv.utils.initOption = function(chart, name) {
+ // if it's a call option, just call it directly, otherwise do get/set
+ if (chart._calls && chart._calls[name]) {
+ chart[name] = chart._calls[name];
+ } else {
+ chart[name] = function (_) {
+ if (!arguments.length) return chart._options[name];
+ chart._overrides[name] = true;
+ chart._options[name] = _;
+ return chart;
+ };
+ // calling the option as _option will ignore if set by option already
+ // so nvd3 can set options internally but the stop if set manually
+ chart['_' + name] = function(_) {
+ if (!arguments.length) return chart._options[name];
+ if (!chart._overrides[name]) {
+ chart._options[name] = _;
+ }
+ return chart;
+ }
+ }
+ };
+
+
+ /*
+ Add all options in an options object to the chart
+ */
+ nv.utils.initOptions = function(chart) {
+ chart._overrides = chart._overrides || {};
+ var ops = Object.getOwnPropertyNames(chart._options || {});
+ var calls = Object.getOwnPropertyNames(chart._calls || {});
+ ops = ops.concat(calls);
+ for (var i in ops) {
+ nv.utils.initOption(chart, ops[i]);
+ }
+ };
+
+
+ /*
+ Inherit options from a D3 object
+ d3.rebind makes calling the function on target actually call it on source
+ Also use _d3options so we can track what we inherit for documentation and chained inheritance
+ */
+ nv.utils.inheritOptionsD3 = function(target, d3_source, oplist) {
+ target._d3options = oplist.concat(target._d3options || []);
+ oplist.unshift(d3_source);
+ oplist.unshift(target);
+ d3.rebind.apply(this, oplist);
+ };
+
+
+ /*
+ Remove duplicates from an array
+ */
+ nv.utils.arrayUnique = function(a) {
+ return a.sort().filter(function(item, pos) {
+ return !pos || item != a[pos - 1];
+ });
+ };
+
+
+ /*
+ Keeps a list of custom symbols to draw from in addition to d3.svg.symbol
+ Necessary since d3 doesn't let you extend its list -_-
+ Add new symbols by doing nv.utils.symbols.set('name', function(size){...});
+ */
+ nv.utils.symbolMap = d3.map();
+
+
+ /*
+ Replaces d3.svg.symbol so that we can look both there and our own map
+ */
+ nv.utils.symbol = function() {
+ var type,
+ size = 64;
+ function symbol(d,i) {
+ var t = type.call(this,d,i);
+ var s = size.call(this,d,i);
+ if (d3.svg.symbolTypes.indexOf(t) !== -1) {
+ return d3.svg.symbol().type(t).size(s)();
+ } else {
+ return nv.utils.symbolMap.get(t)(s);
+ }
+ }
+ symbol.type = function(_) {
+ if (!arguments.length) return type;
+ type = d3.functor(_);
+ return symbol;
+ };
+ symbol.size = function(_) {
+ if (!arguments.length) return size;
+ size = d3.functor(_);
+ return symbol;
+ };
+ return symbol;
+ };
+
+
+ /*
+ Inherit option getter/setter functions from source to target
+ d3.rebind makes calling the function on target actually call it on source
+ Also track via _inherited and _d3options so we can track what we inherit
+ for documentation generation purposes and chained inheritance
+ */
+ nv.utils.inheritOptions = function(target, source) {
+ // inherit all the things
+ var ops = Object.getOwnPropertyNames(source._options || {});
+ var calls = Object.getOwnPropertyNames(source._calls || {});
+ var inherited = source._inherited || [];
+ var d3ops = source._d3options || [];
+ var args = ops.concat(calls).concat(inherited).concat(d3ops);
+ args.unshift(source);
+ args.unshift(target);
+ d3.rebind.apply(this, args);
+ // pass along the lists to keep track of them, don't allow duplicates
+ target._inherited = nv.utils.arrayUnique(ops.concat(calls).concat(inherited).concat(ops).concat(target._inherited || []));
+ target._d3options = nv.utils.arrayUnique(d3ops.concat(target._d3options || []));
+ };
+
+
+ /*
+ Runs common initialize code on the svg before the chart builds
+ */
+ nv.utils.initSVG = function(svg) {
+ svg.classed({'nvd3-svg':true});
+ };
+
+
+ /*
+ Sanitize and provide default for the container height.
+ */
+ nv.utils.sanitizeHeight = function(height, container) {
+ return (height || parseInt(container.style('height'), 10) || 400);
+ };
+
+
+ /*
+ Sanitize and provide default for the container width.
+ */
+ nv.utils.sanitizeWidth = function(width, container) {
+ return (width || parseInt(container.style('width'), 10) || 960);
+ };
+
+
+ /*
+ Calculate the available height for a chart.
+ */
+ nv.utils.availableHeight = function(height, container, margin) {
+ return nv.utils.sanitizeHeight(height, container) - margin.top - margin.bottom;
+ };
+
+ /*
+ Calculate the available width for a chart.
+ */
+ nv.utils.availableWidth = function(width, container, margin) {
+ return nv.utils.sanitizeWidth(width, container) - margin.left - margin.right;
+ };
+
+ /*
+ Clear any rendered chart components and display a chart's 'noData' message
+ */
+ nv.utils.noData = function(chart, container) {
+ var opt = chart.options(),
+ margin = opt.margin(),
+ noData = opt.noData(),
+ data = (noData == null) ? ["No Data Available."] : [noData],
+ height = nv.utils.availableHeight(opt.height(), container, margin),
+ width = nv.utils.availableWidth(opt.width(), container, margin),
+ x = margin.left + width/2,
+ y = margin.top + height/2;
+
+ //Remove any previously created chart components
+ container.selectAll('g').remove();
+
+ var noDataText = container.selectAll('.nv-noData').data(data);
+
+ noDataText.enter().append('text')
+ .attr('class', 'nvd3 nv-noData')
+ .attr('dy', '-.7em')
+ .style('text-anchor', 'middle');
+
+ noDataText
+ .attr('x', x)
+ .attr('y', y)
+ .text(function(t){ return t; });
+ };
+
+ nv.models.axis = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var axis = d3.svg.axis();
+ var scale = d3.scale.linear();
+
+ var margin = {top: 0, right: 0, bottom: 0, left: 0}
+ , width = 75 //only used for tickLabel currently
+ , height = 60 //only used for tickLabel currently
+ , axisLabelText = null
+ , showMaxMin = true //TODO: showMaxMin should be disabled on all ordinal scaled axes
+ , rotateLabels = 0
+ , rotateYLabel = true
+ , staggerLabels = false
+ , isOrdinal = false
+ , ticks = null
+ , axisLabelDistance = 0
+ , duration = 250
+ , dispatch = d3.dispatch('renderEnd')
+ ;
+ axis
+ .scale(scale)
+ .orient('bottom')
+ .tickFormat(function(d) { return d })
+ ;
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ var scale0;
+ var renderWatch = nv.utils.renderWatch(dispatch, duration);
+
+ function chart(selection) {
+ renderWatch.reset();
+ selection.each(function(data) {
+ var container = d3.select(this);
+ nv.utils.initSVG(container);
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-axis').data([data]);
+ var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-axis');
+ var gEnter = wrapEnter.append('g');
+ var g = wrap.select('g');
+
+ if (ticks !== null)
+ axis.ticks(ticks);
+ else if (axis.orient() == 'top' || axis.orient() == 'bottom')
+ axis.ticks(Math.abs(scale.range()[1] - scale.range()[0]) / 100);
+
+ //TODO: consider calculating width/height based on whether or not label is added, for reference in charts using this component
+ g.watchTransition(renderWatch, 'axis').call(axis);
+
+ scale0 = scale0 || axis.scale();
+
+ var fmt = axis.tickFormat();
+ if (fmt == null) {
+ fmt = scale0.tickFormat();
+ }
+
+ var axisLabel = g.selectAll('text.nv-axislabel')
+ .data([axisLabelText || null]);
+ axisLabel.exit().remove();
+
+ var xLabelMargin;
+ var axisMaxMin;
+ var w;
+ switch (axis.orient()) {
+ case 'top':
+ axisLabel.enter().append('text').attr('class', 'nv-axislabel');
+ if (scale.range().length < 2) {
+ w = 0;
+ } else if (scale.range().length === 2) {
+ w = scale.range()[1];
+ } else {
+ w = scale.range()[scale.range().length-1]+(scale.range()[1]-scale.range()[0]);
+ }
+ axisLabel
+ .attr('text-anchor', 'middle')
+ .attr('y', 0)
+ .attr('x', w/2);
+ if (showMaxMin) {
+ axisMaxMin = wrap.selectAll('g.nv-axisMaxMin')
+ .data(scale.domain());
+ axisMaxMin.enter().append('g').attr('class',function(d,i){
+ return ['nv-axisMaxMin','nv-axisMaxMin-x',(i == 0 ? 'nv-axisMin-x':'nv-axisMax-x')].join(' ')
+ }).append('text');
+ axisMaxMin.exit().remove();
+ axisMaxMin
+ .attr('transform', function(d,i) {
+ return 'translate(' + nv.utils.NaNtoZero(scale(d)) + ',0)'
+ })
+ .select('text')
+ .attr('dy', '-0.5em')
+ .attr('y', -axis.tickPadding())
+ .attr('text-anchor', 'middle')
+ .text(function(d,i) {
+ var v = fmt(d);
+ return ('' + v).match('NaN') ? '' : v;
+ });
+ axisMaxMin.watchTransition(renderWatch, 'min-max top')
+ .attr('transform', function(d,i) {
+ return 'translate(' + nv.utils.NaNtoZero(scale.range()[i]) + ',0)'
+ });
+ }
+ break;
+ case 'bottom':
+ xLabelMargin = axisLabelDistance + 36;
+ var maxTextWidth = 30;
+ var textHeight = 0;
+ var xTicks = g.selectAll('g').select("text");
+ var rotateLabelsRule = '';
+ if (rotateLabels%360) {
+ //Calculate the longest xTick width
+ xTicks.each(function(d,i){
+ var box = this.getBoundingClientRect();
+ var width = box.width;
+ textHeight = box.height;
+ if(width > maxTextWidth) maxTextWidth = width;
+ });
+ rotateLabelsRule = 'rotate(' + rotateLabels + ' 0,' + (textHeight/2 + axis.tickPadding()) + ')';
+ //Convert to radians before calculating sin. Add 30 to margin for healthy padding.
+ var sin = Math.abs(Math.sin(rotateLabels*Math.PI/180));
+ xLabelMargin = (sin ? sin*maxTextWidth : maxTextWidth)+30;
+ //Rotate all xTicks
+ xTicks
+ .attr('transform', rotateLabelsRule)
+ .style('text-anchor', rotateLabels%360 > 0 ? 'start' : 'end');
+ }
+ axisLabel.enter().append('text').attr('class', 'nv-axislabel');
+ if (scale.range().length < 2) {
+ w = 0;
+ } else if (scale.range().length === 2) {
+ w = scale.range()[1];
+ } else {
+ w = scale.range()[scale.range().length-1]+(scale.range()[1]-scale.range()[0]);
+ }
+ axisLabel
+ .attr('text-anchor', 'middle')
+ .attr('y', xLabelMargin)
+ .attr('x', w/2);
+ if (showMaxMin) {
+ //if (showMaxMin && !isOrdinal) {
+ axisMaxMin = wrap.selectAll('g.nv-axisMaxMin')
+ //.data(scale.domain())
+ .data([scale.domain()[0], scale.domain()[scale.domain().length - 1]]);
+ axisMaxMin.enter().append('g').attr('class',function(d,i){
+ return ['nv-axisMaxMin','nv-axisMaxMin-x',(i == 0 ? 'nv-axisMin-x':'nv-axisMax-x')].join(' ')
+ }).append('text');
+ axisMaxMin.exit().remove();
+ axisMaxMin
+ .attr('transform', function(d,i) {
+ return 'translate(' + nv.utils.NaNtoZero((scale(d) + (isOrdinal ? scale.rangeBand() / 2 : 0))) + ',0)'
+ })
+ .select('text')
+ .attr('dy', '.71em')
+ .attr('y', axis.tickPadding())
+ .attr('transform', rotateLabelsRule)
+ .style('text-anchor', rotateLabels ? (rotateLabels%360 > 0 ? 'start' : 'end') : 'middle')
+ .text(function(d,i) {
+ var v = fmt(d);
+ return ('' + v).match('NaN') ? '' : v;
+ });
+ axisMaxMin.watchTransition(renderWatch, 'min-max bottom')
+ .attr('transform', function(d,i) {
+ return 'translate(' + nv.utils.NaNtoZero((scale(d) + (isOrdinal ? scale.rangeBand() / 2 : 0))) + ',0)'
+ });
+ }
+ if (staggerLabels)
+ xTicks
+ .attr('transform', function(d,i) {
+ return 'translate(0,' + (i % 2 == 0 ? '0' : '12') + ')'
+ });
+
+ break;
+ case 'right':
+ axisLabel.enter().append('text').attr('class', 'nv-axislabel');
+ axisLabel
+ .style('text-anchor', rotateYLabel ? 'middle' : 'begin')
+ .attr('transform', rotateYLabel ? 'rotate(90)' : '')
+ .attr('y', rotateYLabel ? (-Math.max(margin.right, width) + 12) : -10) //TODO: consider calculating this based on largest tick width... OR at least expose this on chart
+ .attr('x', rotateYLabel ? (d3.max(scale.range()) / 2) : axis.tickPadding());
+ if (showMaxMin) {
+ axisMaxMin = wrap.selectAll('g.nv-axisMaxMin')
+ .data(scale.domain());
+ axisMaxMin.enter().append('g').attr('class',function(d,i){
+ return ['nv-axisMaxMin','nv-axisMaxMin-y',(i == 0 ? 'nv-axisMin-y':'nv-axisMax-y')].join(' ')
+ }).append('text')
+ .style('opacity', 0);
+ axisMaxMin.exit().remove();
+ axisMaxMin
+ .attr('transform', function(d,i) {
+ return 'translate(0,' + nv.utils.NaNtoZero(scale(d)) + ')'
+ })
+ .select('text')
+ .attr('dy', '.32em')
+ .attr('y', 0)
+ .attr('x', axis.tickPadding())
+ .style('text-anchor', 'start')
+ .text(function(d, i) {
+ var v = fmt(d);
+ return ('' + v).match('NaN') ? '' : v;
+ });
+ axisMaxMin.watchTransition(renderWatch, 'min-max right')
+ .attr('transform', function(d,i) {
+ return 'translate(0,' + nv.utils.NaNtoZero(scale.range()[i]) + ')'
+ })
+ .select('text')
+ .style('opacity', 1);
+ }
+ break;
+ case 'left':
+ /*
+ //For dynamically placing the label. Can be used with dynamically-sized chart axis margins
+ var yTicks = g.selectAll('g').select("text");
+ yTicks.each(function(d,i){
+ var labelPadding = this.getBoundingClientRect().width + axis.tickPadding() + 16;
+ if(labelPadding > width) width = labelPadding;
+ });
+ */
+ axisLabel.enter().append('text').attr('class', 'nv-axislabel');
+ axisLabel
+ .style('text-anchor', rotateYLabel ? 'middle' : 'end')
+ .attr('transform', rotateYLabel ? 'rotate(-90)' : '')
+ .attr('y', rotateYLabel ? (-Math.max(margin.left, width) + 25 - (axisLabelDistance || 0)) : -10)
+ .attr('x', rotateYLabel ? (-d3.max(scale.range()) / 2) : -axis.tickPadding());
+ if (showMaxMin) {
+ axisMaxMin = wrap.selectAll('g.nv-axisMaxMin')
+ .data(scale.domain());
+ axisMaxMin.enter().append('g').attr('class',function(d,i){
+ return ['nv-axisMaxMin','nv-axisMaxMin-y',(i == 0 ? 'nv-axisMin-y':'nv-axisMax-y')].join(' ')
+ }).append('text')
+ .style('opacity', 0);
+ axisMaxMin.exit().remove();
+ axisMaxMin
+ .attr('transform', function(d,i) {
+ return 'translate(0,' + nv.utils.NaNtoZero(scale0(d)) + ')'
+ })
+ .select('text')
+ .attr('dy', '.32em')
+ .attr('y', 0)
+ .attr('x', -axis.tickPadding())
+ .attr('text-anchor', 'end')
+ .text(function(d,i) {
+ var v = fmt(d);
+ return ('' + v).match('NaN') ? '' : v;
+ });
+ axisMaxMin.watchTransition(renderWatch, 'min-max right')
+ .attr('transform', function(d,i) {
+ return 'translate(0,' + nv.utils.NaNtoZero(scale.range()[i]) + ')'
+ })
+ .select('text')
+ .style('opacity', 1);
+ }
+ break;
+ }
+ axisLabel.text(function(d) { return d });
+
+ if (showMaxMin && (axis.orient() === 'left' || axis.orient() === 'right')) {
+ //check if max and min overlap other values, if so, hide the values that overlap
+ g.selectAll('g') // the g's wrapping each tick
+ .each(function(d,i) {
+ d3.select(this).select('text').attr('opacity', 1);
+ if (scale(d) < scale.range()[1] + 10 || scale(d) > scale.range()[0] - 10) { // 10 is assuming text height is 16... if d is 0, leave it!
+ if (d > 1e-10 || d < -1e-10) // accounts for minor floating point errors... though could be problematic if the scale is EXTREMELY SMALL
+ d3.select(this).attr('opacity', 0);
+
+ d3.select(this).select('text').attr('opacity', 0); // Don't remove the ZERO line!!
+ }
+ });
+
+ //if Max and Min = 0 only show min, Issue #281
+ if (scale.domain()[0] == scale.domain()[1] && scale.domain()[0] == 0) {
+ wrap.selectAll('g.nv-axisMaxMin').style('opacity', function (d, i) {
+ return !i ? 1 : 0
+ });
+ }
+ }
+
+ if (showMaxMin && (axis.orient() === 'top' || axis.orient() === 'bottom')) {
+ var maxMinRange = [];
+ wrap.selectAll('g.nv-axisMaxMin')
+ .each(function(d,i) {
+ try {
+ if (i) // i== 1, max position
+ maxMinRange.push(scale(d) - this.getBoundingClientRect().width - 4); //assuming the max and min labels are as wide as the next tick (with an extra 4 pixels just in case)
+ else // i==0, min position
+ maxMinRange.push(scale(d) + this.getBoundingClientRect().width + 4)
+ }catch (err) {
+ if (i) // i== 1, max position
+ maxMinRange.push(scale(d) - 4); //assuming the max and min labels are as wide as the next tick (with an extra 4 pixels just in case)
+ else // i==0, min position
+ maxMinRange.push(scale(d) + 4);
+ }
+ });
+ // the g's wrapping each tick
+ g.selectAll('g').each(function(d, i) {
+ if (scale(d) < maxMinRange[0] || scale(d) > maxMinRange[1]) {
+ if (d > 1e-10 || d < -1e-10) // accounts for minor floating point errors... though could be problematic if the scale is EXTREMELY SMALL
+ d3.select(this).remove();
+ else
+ d3.select(this).select('text').remove(); // Don't remove the ZERO line!!
+ }
+ });
+ }
+
+ //Highlight zero tick line
+ g.selectAll('.tick')
+ .filter(function (d) {
+ /*
+ The filter needs to return only ticks at or near zero.
+ Numbers like 0.00001 need to count as zero as well,
+ and the arithmetic trick below solves that.
+ */
+ return !parseFloat(Math.round(d * 100000) / 1000000) && (d !== undefined)
+ })
+ .classed('zero', true);
+
+ //store old scales for use in transitions on update
+ scale0 = scale.copy();
+
+ });
+
+ renderWatch.renderEnd('axis immediate');
+ return chart;
+ }
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ // expose chart's sub-components
+ chart.axis = axis;
+ chart.dispatch = dispatch;
+
+ chart.options = nv.utils.optionsFunc.bind(chart);
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ axisLabelDistance: {get: function(){return axisLabelDistance;}, set: function(_){axisLabelDistance=_;}},
+ staggerLabels: {get: function(){return staggerLabels;}, set: function(_){staggerLabels=_;}},
+ rotateLabels: {get: function(){return rotateLabels;}, set: function(_){rotateLabels=_;}},
+ rotateYLabel: {get: function(){return rotateYLabel;}, set: function(_){rotateYLabel=_;}},
+ showMaxMin: {get: function(){return showMaxMin;}, set: function(_){showMaxMin=_;}},
+ axisLabel: {get: function(){return axisLabelText;}, set: function(_){axisLabelText=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ ticks: {get: function(){return ticks;}, set: function(_){ticks=_;}},
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ duration: {get: function(){return duration;}, set: function(_){
+ duration=_;
+ renderWatch.reset(duration);
+ }},
+ scale: {get: function(){return scale;}, set: function(_){
+ scale = _;
+ axis.scale(scale);
+ isOrdinal = typeof scale.rangeBands === 'function';
+ nv.utils.inheritOptionsD3(chart, scale, ['domain', 'range', 'rangeBand', 'rangeBands']);
+ }}
+ });
+
+ nv.utils.initOptions(chart);
+ nv.utils.inheritOptionsD3(chart, axis, ['orient', 'tickValues', 'tickSubdivide', 'tickSize', 'tickPadding', 'tickFormat']);
+ nv.utils.inheritOptionsD3(chart, scale, ['domain', 'range', 'rangeBand', 'rangeBands']);
+
+ return chart;
+ };
+ nv.models.boxPlot = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var margin = {top: 0, right: 0, bottom: 0, left: 0}
+ , width = 960
+ , height = 500
+ , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one
+ , x = d3.scale.ordinal()
+ , y = d3.scale.linear()
+ , getX = function(d) { return d.x }
+ , getY = function(d) { return d.y }
+ , color = nv.utils.defaultColor()
+ , container = null
+ , xDomain
+ , yDomain
+ , xRange
+ , yRange
+ , dispatch = d3.dispatch('elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd')
+ , duration = 250
+ , maxBoxWidth = null
+ ;
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ var x0, y0;
+ var renderWatch = nv.utils.renderWatch(dispatch, duration);
+
+ function chart(selection) {
+ renderWatch.reset();
+ selection.each(function(data) {
+ var availableWidth = width - margin.left - margin.right,
+ availableHeight = height - margin.top - margin.bottom;
+
+ container = d3.select(this);
+ nv.utils.initSVG(container);
+
+ // Setup Scales
+ x .domain(xDomain || data.map(function(d,i) { return getX(d,i); }))
+ .rangeBands(xRange || [0, availableWidth], .1);
+
+ // if we know yDomain, no need to calculate
+ var yData = []
+ if (!yDomain) {
+ // (y-range is based on quartiles, whiskers and outliers)
+
+ // lower values
+ var yMin = d3.min(data.map(function(d) {
+ var min_arr = [];
+
+ min_arr.push(d.values.Q1);
+ if (d.values.hasOwnProperty('whisker_low') && d.values.whisker_low !== null) { min_arr.push(d.values.whisker_low); }
+ if (d.values.hasOwnProperty('outliers') && d.values.outliers !== null) { min_arr = min_arr.concat(d.values.outliers); }
+
+ return d3.min(min_arr);
+ }));
+
+ // upper values
+ var yMax = d3.max(data.map(function(d) {
+ var max_arr = [];
+
+ max_arr.push(d.values.Q3);
+ if (d.values.hasOwnProperty('whisker_high') && d.values.whisker_high !== null) { max_arr.push(d.values.whisker_high); }
+ if (d.values.hasOwnProperty('outliers') && d.values.outliers !== null) { max_arr = max_arr.concat(d.values.outliers); }
+
+ return d3.max(max_arr);
+ }));
+
+ yData = [ yMin, yMax ] ;
+ }
+
+ y.domain(yDomain || yData);
+ y.range(yRange || [availableHeight, 0]);
+
+ //store old scales if they exist
+ x0 = x0 || x;
+ y0 = y0 || y.copy().range([y(0),y(0)]);
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap').data([data]);
+ var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap');
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ var boxplots = wrap.selectAll('.nv-boxplot').data(function(d) { return d });
+ var boxEnter = boxplots.enter().append('g').style('stroke-opacity', 1e-6).style('fill-opacity', 1e-6);
+ boxplots
+ .attr('class', 'nv-boxplot')
+ .attr('transform', function(d,i,j) { return 'translate(' + (x(getX(d,i)) + x.rangeBand() * .05) + ', 0)'; })
+ .classed('hover', function(d) { return d.hover });
+ boxplots
+ .watchTransition(renderWatch, 'nv-boxplot: boxplots')
+ .style('stroke-opacity', 1)
+ .style('fill-opacity', .75)
+ .delay(function(d,i) { return i * duration / data.length })
+ .attr('transform', function(d,i) {
+ return 'translate(' + (x(getX(d,i)) + x.rangeBand() * .05) + ', 0)';
+ });
+ boxplots.exit().remove();
+
+ // ----- add the SVG elements for each boxPlot -----
+
+ // conditionally append whisker lines
+ boxEnter.each(function(d,i) {
+ var box = d3.select(this);
+
+ ['low', 'high'].forEach(function(key) {
+ if (d.values.hasOwnProperty('whisker_' + key) && d.values['whisker_' + key] !== null) {
+ box.append('line')
+ .style('stroke', (d.color) ? d.color : color(d,i))
+ .attr('class', 'nv-boxplot-whisker nv-boxplot-' + key);
+
+ box.append('line')
+ .style('stroke', (d.color) ? d.color : color(d,i))
+ .attr('class', 'nv-boxplot-tick nv-boxplot-' + key);
+ }
+ });
+ });
+
+ // outliers
+ // TODO: support custom colors here
+ var outliers = boxplots.selectAll('.nv-boxplot-outlier').data(function(d) {
+ if (d.values.hasOwnProperty('outliers') && d.values.outliers !== null) { return d.values.outliers; }
+ else { return []; }
+ });
+ outliers.enter().append('circle')
+ .style('fill', function(d,i,j) { return color(d,j) }).style('stroke', function(d,i,j) { return color(d,j) })
+ .on('mouseover', function(d,i,j) {
+ d3.select(this).classed('hover', true);
+ dispatch.elementMouseover({
+ series: { key: d, color: color(d,j) },
+ e: d3.event
+ });
+ })
+ .on('mouseout', function(d,i,j) {
+ d3.select(this).classed('hover', false);
+ dispatch.elementMouseout({
+ series: { key: d, color: color(d,j) },
+ e: d3.event
+ });
+ })
+ .on('mousemove', function(d,i) {
+ dispatch.elementMousemove({e: d3.event});
+ });
+
+ outliers.attr('class', 'nv-boxplot-outlier');
+ outliers
+ .watchTransition(renderWatch, 'nv-boxplot: nv-boxplot-outlier')
+ .attr('cx', x.rangeBand() * .45)
+ .attr('cy', function(d,i,j) { return y(d); })
+ .attr('r', '3');
+ outliers.exit().remove();
+
+ var box_width = function() { return (maxBoxWidth === null ? x.rangeBand() * .9 : Math.min(75, x.rangeBand() * .9)); };
+ var box_left = function() { return x.rangeBand() * .45 - box_width()/2; };
+ var box_right = function() { return x.rangeBand() * .45 + box_width()/2; };
+
+ // update whisker lines and ticks
+ ['low', 'high'].forEach(function(key) {
+ var endpoint = (key === 'low') ? 'Q1' : 'Q3';
+
+ boxplots.select('line.nv-boxplot-whisker.nv-boxplot-' + key)
+ .watchTransition(renderWatch, 'nv-boxplot: boxplots')
+ .attr('x1', x.rangeBand() * .45 )
+ .attr('y1', function(d,i) { return y(d.values['whisker_' + key]); })
+ .attr('x2', x.rangeBand() * .45 )
+ .attr('y2', function(d,i) { return y(d.values[endpoint]); });
+
+ boxplots.select('line.nv-boxplot-tick.nv-boxplot-' + key)
+ .watchTransition(renderWatch, 'nv-boxplot: boxplots')
+ .attr('x1', box_left )
+ .attr('y1', function(d,i) { return y(d.values['whisker_' + key]); })
+ .attr('x2', box_right )
+ .attr('y2', function(d,i) { return y(d.values['whisker_' + key]); });
+ });
+
+ ['low', 'high'].forEach(function(key) {
+ boxEnter.selectAll('.nv-boxplot-' + key)
+ .on('mouseover', function(d,i,j) {
+ d3.select(this).classed('hover', true);
+ dispatch.elementMouseover({
+ series: { key: d.values['whisker_' + key], color: color(d,j) },
+ e: d3.event
+ });
+ })
+ .on('mouseout', function(d,i,j) {
+ d3.select(this).classed('hover', false);
+ dispatch.elementMouseout({
+ series: { key: d.values['whisker_' + key], color: color(d,j) },
+ e: d3.event
+ });
+ })
+ .on('mousemove', function(d,i) {
+ dispatch.elementMousemove({e: d3.event});
+ });
+ });
+
+ // boxes
+ boxEnter.append('rect')
+ .attr('class', 'nv-boxplot-box')
+ // tooltip events
+ .on('mouseover', function(d,i) {
+ d3.select(this).classed('hover', true);
+ dispatch.elementMouseover({
+ key: d.label,
+ value: d.label,
+ series: [
+ { key: 'Q3', value: d.values.Q3, color: d.color || color(d,i) },
+ { key: 'Q2', value: d.values.Q2, color: d.color || color(d,i) },
+ { key: 'Q1', value: d.values.Q1, color: d.color || color(d,i) }
+ ],
+ data: d,
+ index: i,
+ e: d3.event
+ });
+ })
+ .on('mouseout', function(d,i) {
+ d3.select(this).classed('hover', false);
+ dispatch.elementMouseout({
+ key: d.label,
+ value: d.label,
+ series: [
+ { key: 'Q3', value: d.values.Q3, color: d.color || color(d,i) },
+ { key: 'Q2', value: d.values.Q2, color: d.color || color(d,i) },
+ { key: 'Q1', value: d.values.Q1, color: d.color || color(d,i) }
+ ],
+ data: d,
+ index: i,
+ e: d3.event
+ });
+ })
+ .on('mousemove', function(d,i) {
+ dispatch.elementMousemove({e: d3.event});
+ });
+
+ // box transitions
+ boxplots.select('rect.nv-boxplot-box')
+ .watchTransition(renderWatch, 'nv-boxplot: boxes')
+ .attr('y', function(d,i) { return y(d.values.Q3); })
+ .attr('width', box_width)
+ .attr('x', box_left )
+
+ .attr('height', function(d,i) { return Math.abs(y(d.values.Q3) - y(d.values.Q1)) || 1 })
+ .style('fill', function(d,i) { return d.color || color(d,i) })
+ .style('stroke', function(d,i) { return d.color || color(d,i) });
+
+ // median line
+ boxEnter.append('line').attr('class', 'nv-boxplot-median');
+
+ boxplots.select('line.nv-boxplot-median')
+ .watchTransition(renderWatch, 'nv-boxplot: boxplots line')
+ .attr('x1', box_left)
+ .attr('y1', function(d,i) { return y(d.values.Q2); })
+ .attr('x2', box_right)
+ .attr('y2', function(d,i) { return y(d.values.Q2); });
+
+ //store old scales for use in transitions on update
+ x0 = x.copy();
+ y0 = y.copy();
+ });
+
+ renderWatch.renderEnd('nv-boxplot immediate');
+ return chart;
+ }
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ chart.dispatch = dispatch;
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ maxBoxWidth: {get: function(){return maxBoxWidth;}, set: function(_){maxBoxWidth=_;}},
+ x: {get: function(){return getX;}, set: function(_){getX=_;}},
+ y: {get: function(){return getY;}, set: function(_){getY=_;}},
+ xScale: {get: function(){return x;}, set: function(_){x=_;}},
+ yScale: {get: function(){return y;}, set: function(_){y=_;}},
+ xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}},
+ yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}},
+ xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}},
+ yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}},
+ id: {get: function(){return id;}, set: function(_){id=_;}},
+ // rectClass: {get: function(){return rectClass;}, set: function(_){rectClass=_;}},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ }},
+ duration: {get: function(){return duration;}, set: function(_){
+ duration = _;
+ renderWatch.reset(duration);
+ }}
+ });
+
+ nv.utils.initOptions(chart);
+
+ return chart;
+ };
+ nv.models.boxPlotChart = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var boxplot = nv.models.boxPlot()
+ , xAxis = nv.models.axis()
+ , yAxis = nv.models.axis()
+ ;
+
+ var margin = {top: 15, right: 10, bottom: 50, left: 60}
+ , width = null
+ , height = null
+ , color = nv.utils.getColor()
+ , showXAxis = true
+ , showYAxis = true
+ , rightAlignYAxis = false
+ , staggerLabels = false
+ , tooltip = nv.models.tooltip()
+ , x
+ , y
+ , noData = "No Data Available."
+ , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'beforeUpdate', 'renderEnd')
+ , duration = 250
+ ;
+
+ xAxis
+ .orient('bottom')
+ .showMaxMin(false)
+ .tickFormat(function(d) { return d })
+ ;
+ yAxis
+ .orient((rightAlignYAxis) ? 'right' : 'left')
+ .tickFormat(d3.format(',.1f'))
+ ;
+
+ tooltip.duration(0);
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ var renderWatch = nv.utils.renderWatch(dispatch, duration);
+
+ function chart(selection) {
+ renderWatch.reset();
+ renderWatch.models(boxplot);
+ if (showXAxis) renderWatch.models(xAxis);
+ if (showYAxis) renderWatch.models(yAxis);
+
+ selection.each(function(data) {
+ var container = d3.select(this),
+ that = this;
+ nv.utils.initSVG(container);
+ var availableWidth = (width || parseInt(container.style('width')) || 960)
+ - margin.left - margin.right,
+ availableHeight = (height || parseInt(container.style('height')) || 400)
+ - margin.top - margin.bottom;
+
+ chart.update = function() {
+ dispatch.beforeUpdate();
+ container.transition().duration(duration).call(chart);
+ };
+ chart.container = this;
+
+ // Display No Data message if there's nothing to show. (quartiles required at minimum)
+ if (!data || !data.length ||
+ !data.filter(function(d) { return d.values.hasOwnProperty("Q1") && d.values.hasOwnProperty("Q2") && d.values.hasOwnProperty("Q3"); }).length) {
+ var noDataText = container.selectAll('.nv-noData').data([noData]);
+
+ noDataText.enter().append('text')
+ .attr('class', 'nvd3 nv-noData')
+ .attr('dy', '-.7em')
+ .style('text-anchor', 'middle');
+
+ noDataText
+ .attr('x', margin.left + availableWidth / 2)
+ .attr('y', margin.top + availableHeight / 2)
+ .text(function(d) { return d });
+
+ return chart;
+ } else {
+ container.selectAll('.nv-noData').remove();
+ }
+
+ // Setup Scales
+ x = boxplot.xScale();
+ y = boxplot.yScale().clamp(true);
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-boxPlotWithAxes').data([data]);
+ var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-boxPlotWithAxes').append('g');
+ var defsEnter = gEnter.append('defs');
+ var g = wrap.select('g');
+
+ gEnter.append('g').attr('class', 'nv-x nv-axis');
+ gEnter.append('g').attr('class', 'nv-y nv-axis')
+ .append('g').attr('class', 'nv-zeroLine')
+ .append('line');
+
+ gEnter.append('g').attr('class', 'nv-barsWrap');
+
+ g.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ if (rightAlignYAxis) {
+ g.select(".nv-y.nv-axis")
+ .attr("transform", "translate(" + availableWidth + ",0)");
+ }
+
+ // Main Chart Component(s)
+ boxplot
+ .width(availableWidth)
+ .height(availableHeight);
+
+ var barsWrap = g.select('.nv-barsWrap')
+ .datum(data.filter(function(d) { return !d.disabled }))
+
+ barsWrap.transition().call(boxplot);
+
+
+ defsEnter.append('clipPath')
+ .attr('id', 'nv-x-label-clip-' + boxplot.id())
+ .append('rect');
+
+ g.select('#nv-x-label-clip-' + boxplot.id() + ' rect')
+ .attr('width', x.rangeBand() * (staggerLabels ? 2 : 1))
+ .attr('height', 16)
+ .attr('x', -x.rangeBand() / (staggerLabels ? 1 : 2 ));
+
+ // Setup Axes
+ if (showXAxis) {
+ xAxis
+ .scale(x)
+ .ticks( nv.utils.calcTicksX(availableWidth/100, data) )
+ .tickSize(-availableHeight, 0);
+
+ g.select('.nv-x.nv-axis').attr('transform', 'translate(0,' + y.range()[0] + ')');
+ g.select('.nv-x.nv-axis').call(xAxis);
+
+ var xTicks = g.select('.nv-x.nv-axis').selectAll('g');
+ if (staggerLabels) {
+ xTicks
+ .selectAll('text')
+ .attr('transform', function(d,i,j) { return 'translate(0,' + (j % 2 == 0 ? '5' : '17') + ')' })
+ }
+ }
+
+ if (showYAxis) {
+ yAxis
+ .scale(y)
+ .ticks( Math.floor(availableHeight/36) ) // can't use nv.utils.calcTicksY with Object data
+ .tickSize( -availableWidth, 0);
+
+ g.select('.nv-y.nv-axis').call(yAxis);
+ }
+
+ // Zero line
+ g.select(".nv-zeroLine line")
+ .attr("x1",0)
+ .attr("x2",availableWidth)
+ .attr("y1", y(0))
+ .attr("y2", y(0))
+ ;
+
+ //============================================================
+ // Event Handling/Dispatching (in chart's scope)
+ //------------------------------------------------------------
+ });
+
+ renderWatch.renderEnd('nv-boxplot chart immediate');
+ return chart;
+ }
+
+ //============================================================
+ // Event Handling/Dispatching (out of chart's scope)
+ //------------------------------------------------------------
+
+ boxplot.dispatch.on('elementMouseover.tooltip', function(evt) {
+ tooltip.data(evt).hidden(false);
+ });
+
+ boxplot.dispatch.on('elementMouseout.tooltip', function(evt) {
+ tooltip.data(evt).hidden(true);
+ });
+
+ boxplot.dispatch.on('elementMousemove.tooltip', function(evt) {
+ tooltip.position({top: d3.event.pageY, left: d3.event.pageX})();
+ });
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ chart.dispatch = dispatch;
+ chart.boxplot = boxplot;
+ chart.xAxis = xAxis;
+ chart.yAxis = yAxis;
+ chart.tooltip = tooltip;
+
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ staggerLabels: {get: function(){return staggerLabels;}, set: function(_){staggerLabels=_;}},
+ showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}},
+ showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}},
+ tooltips: {get: function(){return tooltips;}, set: function(_){tooltips=_;}},
+ tooltipContent: {get: function(){return tooltip;}, set: function(_){tooltip=_;}},
+ noData: {get: function(){return noData;}, set: function(_){noData=_;}},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ duration: {get: function(){return duration;}, set: function(_){
+ duration = _;
+ renderWatch.reset(duration);
+ boxplot.duration(duration);
+ xAxis.duration(duration);
+ yAxis.duration(duration);
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ boxplot.color(color);
+ }},
+ rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){
+ rightAlignYAxis = _;
+ yAxis.orient( (_) ? 'right' : 'left');
+ }}
+ });
+
+ nv.utils.inheritOptions(chart, boxplot);
+ nv.utils.initOptions(chart);
+
+ return chart;
+ }
+// Chart design based on the recommendations of Stephen Few. Implementation
+// based on the work of Clint Ivy, Jamie Love, and Jason Davies.
+// http://projects.instantcognition.com/protovis/bulletchart/
+
+ nv.models.bullet = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var margin = {top: 0, right: 0, bottom: 0, left: 0}
+ , orient = 'left' // TODO top & bottom
+ , reverse = false
+ , ranges = function(d) { return d.ranges }
+ , markers = function(d) { return d.markers ? d.markers : [0] }
+ , measures = function(d) { return d.measures }
+ , rangeLabels = function(d) { return d.rangeLabels ? d.rangeLabels : [] }
+ , markerLabels = function(d) { return d.markerLabels ? d.markerLabels : [] }
+ , measureLabels = function(d) { return d.measureLabels ? d.measureLabels : [] }
+ , forceX = [0] // List of numbers to Force into the X scale (ie. 0, or a max / min, etc.)
+ , width = 380
+ , height = 30
+ , container = null
+ , tickFormat = null
+ , color = nv.utils.getColor(['#1f77b4'])
+ , dispatch = d3.dispatch('elementMouseover', 'elementMouseout', 'elementMousemove')
+ ;
+
+ function chart(selection) {
+ selection.each(function(d, i) {
+ var availableWidth = width - margin.left - margin.right,
+ availableHeight = height - margin.top - margin.bottom;
+
+ container = d3.select(this);
+ nv.utils.initSVG(container);
+
+ var rangez = ranges.call(this, d, i).slice().sort(d3.descending),
+ markerz = markers.call(this, d, i).slice().sort(d3.descending),
+ measurez = measures.call(this, d, i).slice().sort(d3.descending),
+ rangeLabelz = rangeLabels.call(this, d, i).slice(),
+ markerLabelz = markerLabels.call(this, d, i).slice(),
+ measureLabelz = measureLabels.call(this, d, i).slice();
+
+ // Setup Scales
+ // Compute the new x-scale.
+ var x1 = d3.scale.linear()
+ .domain( d3.extent(d3.merge([forceX, rangez])) )
+ .range(reverse ? [availableWidth, 0] : [0, availableWidth]);
+
+ // Retrieve the old x-scale, if this is an update.
+ var x0 = this.__chart__ || d3.scale.linear()
+ .domain([0, Infinity])
+ .range(x1.range());
+
+ // Stash the new scale.
+ this.__chart__ = x1;
+
+ var rangeMin = d3.min(rangez), //rangez[2]
+ rangeMax = d3.max(rangez), //rangez[0]
+ rangeAvg = rangez[1];
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-bullet').data([d]);
+ var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-bullet');
+ var gEnter = wrapEnter.append('g');
+ var g = wrap.select('g');
+
+ gEnter.append('rect').attr('class', 'nv-range nv-rangeMax');
+ gEnter.append('rect').attr('class', 'nv-range nv-rangeAvg');
+ gEnter.append('rect').attr('class', 'nv-range nv-rangeMin');
+ gEnter.append('rect').attr('class', 'nv-measure');
+
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ var w0 = function(d) { return Math.abs(x0(d) - x0(0)) }, // TODO: could optimize by precalculating x0(0) and x1(0)
+ w1 = function(d) { return Math.abs(x1(d) - x1(0)) };
+ var xp0 = function(d) { return d < 0 ? x0(d) : x0(0) },
+ xp1 = function(d) { return d < 0 ? x1(d) : x1(0) };
+
+ g.select('rect.nv-rangeMax')
+ .attr('height', availableHeight)
+ .attr('width', w1(rangeMax > 0 ? rangeMax : rangeMin))
+ .attr('x', xp1(rangeMax > 0 ? rangeMax : rangeMin))
+ .datum(rangeMax > 0 ? rangeMax : rangeMin)
+
+ g.select('rect.nv-rangeAvg')
+ .attr('height', availableHeight)
+ .attr('width', w1(rangeAvg))
+ .attr('x', xp1(rangeAvg))
+ .datum(rangeAvg)
+
+ g.select('rect.nv-rangeMin')
+ .attr('height', availableHeight)
+ .attr('width', w1(rangeMax))
+ .attr('x', xp1(rangeMax))
+ .attr('width', w1(rangeMax > 0 ? rangeMin : rangeMax))
+ .attr('x', xp1(rangeMax > 0 ? rangeMin : rangeMax))
+ .datum(rangeMax > 0 ? rangeMin : rangeMax)
+
+ g.select('rect.nv-measure')
+ .style('fill', color)
+ .attr('height', availableHeight / 3)
+ .attr('y', availableHeight / 3)
+ .attr('width', measurez < 0 ?
+ x1(0) - x1(measurez[0])
+ : x1(measurez[0]) - x1(0))
+ .attr('x', xp1(measurez))
+ .on('mouseover', function() {
+ dispatch.elementMouseover({
+ value: measurez[0],
+ label: measureLabelz[0] || 'Current',
+ color: d3.select(this).style("fill")
+ })
+ })
+ .on('mousemove', function() {
+ dispatch.elementMousemove({
+ value: measurez[0],
+ label: measureLabelz[0] || 'Current',
+ color: d3.select(this).style("fill")
+ })
+ })
+ .on('mouseout', function() {
+ dispatch.elementMouseout({
+ value: measurez[0],
+ label: measureLabelz[0] || 'Current',
+ color: d3.select(this).style("fill")
+ })
+ });
+
+ var h3 = availableHeight / 6;
+
+ var markerData = markerz.map( function(marker, index) {
+ return {value: marker, label: markerLabelz[index]}
+ });
+ gEnter
+ .selectAll("path.nv-markerTriangle")
+ .data(markerData)
+ .enter()
+ .append('path')
+ .attr('class', 'nv-markerTriangle')
+ .attr('transform', function(d) { return 'translate(' + x1(d.value) + ',' + (availableHeight / 2) + ')' })
+ .attr('d', 'M0,' + h3 + 'L' + h3 + ',' + (-h3) + ' ' + (-h3) + ',' + (-h3) + 'Z')
+ .on('mouseover', function(d) {
+ dispatch.elementMouseover({
+ value: d.value,
+ label: d.label || 'Previous',
+ color: d3.select(this).style("fill"),
+ pos: [x1(d.value), availableHeight/2]
+ })
+
+ })
+ .on('mousemove', function(d) {
+ dispatch.elementMousemove({
+ value: d.value,
+ label: d.label || 'Previous',
+ color: d3.select(this).style("fill")
+ })
+ })
+ .on('mouseout', function(d, i) {
+ dispatch.elementMouseout({
+ value: d.value,
+ label: d.label || 'Previous',
+ color: d3.select(this).style("fill")
+ })
+ });
+
+ wrap.selectAll('.nv-range')
+ .on('mouseover', function(d,i) {
+ var label = rangeLabelz[i] || (!i ? "Maximum" : i == 1 ? "Mean" : "Minimum");
+ dispatch.elementMouseover({
+ value: d,
+ label: label,
+ color: d3.select(this).style("fill")
+ })
+ })
+ .on('mousemove', function() {
+ dispatch.elementMousemove({
+ value: measurez[0],
+ label: measureLabelz[0] || 'Previous',
+ color: d3.select(this).style("fill")
+ })
+ })
+ .on('mouseout', function(d,i) {
+ var label = rangeLabelz[i] || (!i ? "Maximum" : i == 1 ? "Mean" : "Minimum");
+ dispatch.elementMouseout({
+ value: d,
+ label: label,
+ color: d3.select(this).style("fill")
+ })
+ });
+ });
+
+ return chart;
+ }
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ chart.dispatch = dispatch;
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ ranges: {get: function(){return ranges;}, set: function(_){ranges=_;}}, // ranges (bad, satisfactory, good)
+ markers: {get: function(){return markers;}, set: function(_){markers=_;}}, // markers (previous, goal)
+ measures: {get: function(){return measures;}, set: function(_){measures=_;}}, // measures (actual, forecast)
+ forceX: {get: function(){return forceX;}, set: function(_){forceX=_;}},
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ tickFormat: {get: function(){return tickFormat;}, set: function(_){tickFormat=_;}},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ orient: {get: function(){return orient;}, set: function(_){ // left, right, top, bottom
+ orient = _;
+ reverse = orient == 'right' || orient == 'bottom';
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ }}
+ });
+
+ nv.utils.initOptions(chart);
+ return chart;
+ };
+
+
+
+// Chart design based on the recommendations of Stephen Few. Implementation
+// based on the work of Clint Ivy, Jamie Love, and Jason Davies.
+// http://projects.instantcognition.com/protovis/bulletchart/
+ nv.models.bulletChart = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var bullet = nv.models.bullet();
+ var tooltip = nv.models.tooltip();
+
+ var orient = 'left' // TODO top & bottom
+ , reverse = false
+ , margin = {top: 5, right: 40, bottom: 20, left: 120}
+ , ranges = function(d) { return d.ranges }
+ , markers = function(d) { return d.markers ? d.markers : [0] }
+ , measures = function(d) { return d.measures }
+ , width = null
+ , height = 55
+ , tickFormat = null
+ , ticks = null
+ , noData = null
+ , dispatch = d3.dispatch('tooltipShow', 'tooltipHide')
+ ;
+
+ tooltip.duration(0).headerEnabled(false);
+
+ function chart(selection) {
+ selection.each(function(d, i) {
+ var container = d3.select(this);
+ nv.utils.initSVG(container);
+
+ var availableWidth = nv.utils.availableWidth(width, container, margin),
+ availableHeight = height - margin.top - margin.bottom,
+ that = this;
+
+ chart.update = function() { chart(selection) };
+ chart.container = this;
+
+ // Display No Data message if there's nothing to show.
+ if (!d || !ranges.call(this, d, i)) {
+ nv.utils.noData(chart, container)
+ return chart;
+ } else {
+ container.selectAll('.nv-noData').remove();
+ }
+
+ var rangez = ranges.call(this, d, i).slice().sort(d3.descending),
+ markerz = markers.call(this, d, i).slice().sort(d3.descending),
+ measurez = measures.call(this, d, i).slice().sort(d3.descending);
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-bulletChart').data([d]);
+ var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-bulletChart');
+ var gEnter = wrapEnter.append('g');
+ var g = wrap.select('g');
+
+ gEnter.append('g').attr('class', 'nv-bulletWrap');
+ gEnter.append('g').attr('class', 'nv-titles');
+
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ // Compute the new x-scale.
+ var x1 = d3.scale.linear()
+ .domain([0, Math.max(rangez[0], markerz[0], measurez[0])]) // TODO: need to allow forceX and forceY, and xDomain, yDomain
+ .range(reverse ? [availableWidth, 0] : [0, availableWidth]);
+
+ // Retrieve the old x-scale, if this is an update.
+ var x0 = this.__chart__ || d3.scale.linear()
+ .domain([0, Infinity])
+ .range(x1.range());
+
+ // Stash the new scale.
+ this.__chart__ = x1;
+
+ var w0 = function(d) { return Math.abs(x0(d) - x0(0)) }, // TODO: could optimize by precalculating x0(0) and x1(0)
+ w1 = function(d) { return Math.abs(x1(d) - x1(0)) };
+
+ var title = gEnter.select('.nv-titles').append('g')
+ .attr('text-anchor', 'end')
+ .attr('transform', 'translate(-6,' + (height - margin.top - margin.bottom) / 2 + ')');
+ title.append('text')
+ .attr('class', 'nv-title')
+ .text(function(d) { return d.title; });
+
+ title.append('text')
+ .attr('class', 'nv-subtitle')
+ .attr('dy', '1em')
+ .text(function(d) { return d.subtitle; });
+
+ bullet
+ .width(availableWidth)
+ .height(availableHeight)
+
+ var bulletWrap = g.select('.nv-bulletWrap');
+ d3.transition(bulletWrap).call(bullet);
+
+ // Compute the tick format.
+ var format = tickFormat || x1.tickFormat( availableWidth / 100 );
+
+ // Update the tick groups.
+ var tick = g.selectAll('g.nv-tick')
+ .data(x1.ticks( ticks ? ticks : (availableWidth / 50) ), function(d) {
+ return this.textContent || format(d);
+ });
+
+ // Initialize the ticks with the old scale, x0.
+ var tickEnter = tick.enter().append('g')
+ .attr('class', 'nv-tick')
+ .attr('transform', function(d) { return 'translate(' + x0(d) + ',0)' })
+ .style('opacity', 1e-6);
+
+ tickEnter.append('line')
+ .attr('y1', availableHeight)
+ .attr('y2', availableHeight * 7 / 6);
+
+ tickEnter.append('text')
+ .attr('text-anchor', 'middle')
+ .attr('dy', '1em')
+ .attr('y', availableHeight * 7 / 6)
+ .text(format);
+
+ // Transition the updating ticks to the new scale, x1.
+ var tickUpdate = d3.transition(tick)
+ .attr('transform', function(d) { return 'translate(' + x1(d) + ',0)' })
+ .style('opacity', 1);
+
+ tickUpdate.select('line')
+ .attr('y1', availableHeight)
+ .attr('y2', availableHeight * 7 / 6);
+
+ tickUpdate.select('text')
+ .attr('y', availableHeight * 7 / 6);
+
+ // Transition the exiting ticks to the new scale, x1.
+ d3.transition(tick.exit())
+ .attr('transform', function(d) { return 'translate(' + x1(d) + ',0)' })
+ .style('opacity', 1e-6)
+ .remove();
+ });
+
+ d3.timer.flush();
+ return chart;
+ }
+
+ //============================================================
+ // Event Handling/Dispatching (out of chart's scope)
+ //------------------------------------------------------------
+
+ bullet.dispatch.on('elementMouseover.tooltip', function(evt) {
+ evt['series'] = {
+ key: evt.label,
+ value: evt.value,
+ color: evt.color
+ };
+ tooltip.data(evt).hidden(false);
+ });
+
+ bullet.dispatch.on('elementMouseout.tooltip', function(evt) {
+ tooltip.hidden(true);
+ });
+
+ bullet.dispatch.on('elementMousemove.tooltip', function(evt) {
+ tooltip.position({top: d3.event.pageY, left: d3.event.pageX})();
+ });
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ chart.bullet = bullet;
+ chart.dispatch = dispatch;
+ chart.tooltip = tooltip;
+
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ ranges: {get: function(){return ranges;}, set: function(_){ranges=_;}}, // ranges (bad, satisfactory, good)
+ markers: {get: function(){return markers;}, set: function(_){markers=_;}}, // markers (previous, goal)
+ measures: {get: function(){return measures;}, set: function(_){measures=_;}}, // measures (actual, forecast)
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ tickFormat: {get: function(){return tickFormat;}, set: function(_){tickFormat=_;}},
+ ticks: {get: function(){return ticks;}, set: function(_){ticks=_;}},
+ noData: {get: function(){return noData;}, set: function(_){noData=_;}},
+
+ // deprecated options
+ tooltips: {get: function(){return tooltip.enabled();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead');
+ tooltip.enabled(!!_);
+ }},
+ tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead');
+ tooltip.contentGenerator(_);
+ }},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ orient: {get: function(){return orient;}, set: function(_){ // left, right, top, bottom
+ orient = _;
+ reverse = orient == 'right' || orient == 'bottom';
+ }}
+ });
+
+ nv.utils.inheritOptions(chart, bullet);
+ nv.utils.initOptions(chart);
+
+ return chart;
+ };
+
+
+
+ nv.models.candlestickBar = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var margin = {top: 0, right: 0, bottom: 0, left: 0}
+ , width = null
+ , height = null
+ , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one
+ , container
+ , x = d3.scale.linear()
+ , y = d3.scale.linear()
+ , getX = function(d) { return d.x }
+ , getY = function(d) { return d.y }
+ , getOpen = function(d) { return d.open }
+ , getClose = function(d) { return d.close }
+ , getHigh = function(d) { return d.high }
+ , getLow = function(d) { return d.low }
+ , forceX = []
+ , forceY = []
+ , padData = false // If true, adds half a data points width to front and back, for lining up a line chart with a bar chart
+ , clipEdge = true
+ , color = nv.utils.defaultColor()
+ , interactive = false
+ , xDomain
+ , yDomain
+ , xRange
+ , yRange
+ , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'stateChange', 'changeState', 'renderEnd', 'chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'elementMousemove')
+ ;
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ function chart(selection) {
+ selection.each(function(data) {
+ container = d3.select(this);
+ var availableWidth = nv.utils.availableWidth(width, container, margin),
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+
+ nv.utils.initSVG(container);
+
+ // Width of the candlestick bars.
+ var barWidth = (availableWidth / data[0].values.length) * .45;
+
+ // Setup Scales
+ x.domain(xDomain || d3.extent(data[0].values.map(getX).concat(forceX) ));
+
+ if (padData)
+ x.range(xRange || [availableWidth * .5 / data[0].values.length, availableWidth * (data[0].values.length - .5) / data[0].values.length ]);
+ else
+ x.range(xRange || [5 + barWidth / 2, availableWidth - barWidth / 2 - 5]);
+
+ y.domain(yDomain || [
+ d3.min(data[0].values.map(getLow).concat(forceY)),
+ d3.max(data[0].values.map(getHigh).concat(forceY))
+ ]
+ ).range(yRange || [availableHeight, 0]);
+
+ // If scale's domain don't have a range, slightly adjust to make one... so a chart can show a single data point
+ if (x.domain()[0] === x.domain()[1])
+ x.domain()[0] ?
+ x.domain([x.domain()[0] - x.domain()[0] * 0.01, x.domain()[1] + x.domain()[1] * 0.01])
+ : x.domain([-1,1]);
+
+ if (y.domain()[0] === y.domain()[1])
+ y.domain()[0] ?
+ y.domain([y.domain()[0] + y.domain()[0] * 0.01, y.domain()[1] - y.domain()[1] * 0.01])
+ : y.domain([-1,1]);
+
+ // Setup containers and skeleton of chart
+ var wrap = d3.select(this).selectAll('g.nv-wrap.nv-candlestickBar').data([data[0].values]);
+ var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-candlestickBar');
+ var defsEnter = wrapEnter.append('defs');
+ var gEnter = wrapEnter.append('g');
+ var g = wrap.select('g');
+
+ gEnter.append('g').attr('class', 'nv-ticks');
+
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ container
+ .on('click', function(d,i) {
+ dispatch.chartClick({
+ data: d,
+ index: i,
+ pos: d3.event,
+ id: id
+ });
+ });
+
+ defsEnter.append('clipPath')
+ .attr('id', 'nv-chart-clip-path-' + id)
+ .append('rect');
+
+ wrap.select('#nv-chart-clip-path-' + id + ' rect')
+ .attr('width', availableWidth)
+ .attr('height', availableHeight);
+
+ g .attr('clip-path', clipEdge ? 'url(#nv-chart-clip-path-' + id + ')' : '');
+
+ var ticks = wrap.select('.nv-ticks').selectAll('.nv-tick')
+ .data(function(d) { return d });
+ ticks.exit().remove();
+
+ // The colors are currently controlled by CSS.
+ var tickGroups = ticks.enter().append('g')
+ .attr('class', function(d, i, j) { return (getOpen(d, i) > getClose(d, i) ? 'nv-tick negative' : 'nv-tick positive') + ' nv-tick-' + j + '-' + i});
+
+ var lines = tickGroups.append('line')
+ .attr('class', 'nv-candlestick-lines')
+ .attr('transform', function(d, i) { return 'translate(' + x(getX(d, i)) + ',0)'; })
+ .attr('x1', 0)
+ .attr('y1', function(d, i) { return y(getHigh(d, i)); })
+ .attr('x2', 0)
+ .attr('y2', function(d, i) { return y(getLow(d, i)); });
+
+ var rects = tickGroups.append('rect')
+ .attr('class', 'nv-candlestick-rects nv-bars')
+ .attr('transform', function(d, i) {
+ return 'translate(' + (x(getX(d, i)) - barWidth/2) + ','
+ + (y(getY(d, i)) - (getOpen(d, i) > getClose(d, i) ? (y(getClose(d, i)) - y(getOpen(d, i))) : 0))
+ + ')';
+ })
+ .attr('x', 0)
+ .attr('y', 0)
+ .attr('width', barWidth)
+ .attr('height', function(d, i) {
+ var open = getOpen(d, i);
+ var close = getClose(d, i);
+ return open > close ? y(close) - y(open) : y(open) - y(close);
+ });
+
+ container.selectAll('.nv-candlestick-lines').transition()
+ .attr('transform', function(d, i) { return 'translate(' + x(getX(d, i)) + ',0)'; })
+ .attr('x1', 0)
+ .attr('y1', function(d, i) { return y(getHigh(d, i)); })
+ .attr('x2', 0)
+ .attr('y2', function(d, i) { return y(getLow(d, i)); });
+
+ container.selectAll('.nv-candlestick-rects').transition()
+ .attr('transform', function(d, i) {
+ return 'translate(' + (x(getX(d, i)) - barWidth/2) + ','
+ + (y(getY(d, i)) - (getOpen(d, i) > getClose(d, i) ? (y(getClose(d, i)) - y(getOpen(d, i))) : 0))
+ + ')';
+ })
+ .attr('x', 0)
+ .attr('y', 0)
+ .attr('width', barWidth)
+ .attr('height', function(d, i) {
+ var open = getOpen(d, i);
+ var close = getClose(d, i);
+ return open > close ? y(close) - y(open) : y(open) - y(close);
+ });
+ });
+
+ return chart;
+ }
+
+
+ //Create methods to allow outside functions to highlight a specific bar.
+ chart.highlightPoint = function(pointIndex, isHoverOver) {
+ chart.clearHighlights();
+ container.select(".nv-candlestickBar .nv-tick-0-" + pointIndex)
+ .classed("hover", isHoverOver)
+ ;
+ };
+
+ chart.clearHighlights = function() {
+ container.select(".nv-candlestickBar .nv-tick.hover")
+ .classed("hover", false)
+ ;
+ };
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ chart.dispatch = dispatch;
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ xScale: {get: function(){return x;}, set: function(_){x=_;}},
+ yScale: {get: function(){return y;}, set: function(_){y=_;}},
+ xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}},
+ yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}},
+ xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}},
+ yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}},
+ forceX: {get: function(){return forceX;}, set: function(_){forceX=_;}},
+ forceY: {get: function(){return forceY;}, set: function(_){forceY=_;}},
+ padData: {get: function(){return padData;}, set: function(_){padData=_;}},
+ clipEdge: {get: function(){return clipEdge;}, set: function(_){clipEdge=_;}},
+ id: {get: function(){return id;}, set: function(_){id=_;}},
+ interactive: {get: function(){return interactive;}, set: function(_){interactive=_;}},
+
+ x: {get: function(){return getX;}, set: function(_){getX=_;}},
+ y: {get: function(){return getY;}, set: function(_){getY=_;}},
+ open: {get: function(){return getOpen();}, set: function(_){getOpen=_;}},
+ close: {get: function(){return getClose();}, set: function(_){getClose=_;}},
+ high: {get: function(){return getHigh;}, set: function(_){getHigh=_;}},
+ low: {get: function(){return getLow;}, set: function(_){getLow=_;}},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top != undefined ? _.top : margin.top;
+ margin.right = _.right != undefined ? _.right : margin.right;
+ margin.bottom = _.bottom != undefined ? _.bottom : margin.bottom;
+ margin.left = _.left != undefined ? _.left : margin.left;
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ }}
+ });
+
+ nv.utils.initOptions(chart);
+ return chart;
+ };
+
+ nv.models.cumulativeLineChart = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var lines = nv.models.line()
+ , xAxis = nv.models.axis()
+ , yAxis = nv.models.axis()
+ , legend = nv.models.legend()
+ , controls = nv.models.legend()
+ , interactiveLayer = nv.interactiveGuideline()
+ , tooltip = nv.models.tooltip()
+ ;
+
+ var margin = {top: 30, right: 30, bottom: 50, left: 60}
+ , color = nv.utils.defaultColor()
+ , width = null
+ , height = null
+ , showLegend = true
+ , showXAxis = true
+ , showYAxis = true
+ , rightAlignYAxis = false
+ , showControls = true
+ , useInteractiveGuideline = false
+ , rescaleY = true
+ , x //can be accessed via chart.xScale()
+ , y //can be accessed via chart.yScale()
+ , id = lines.id()
+ , state = nv.utils.state()
+ , defaultState = null
+ , noData = null
+ , average = function(d) { return d.average }
+ , dispatch = d3.dispatch('stateChange', 'changeState', 'renderEnd')
+ , transitionDuration = 250
+ , duration = 250
+ , noErrorCheck = false //if set to TRUE, will bypass an error check in the indexify function.
+ ;
+
+ state.index = 0;
+ state.rescaleY = rescaleY;
+
+ xAxis.orient('bottom').tickPadding(7);
+ yAxis.orient((rightAlignYAxis) ? 'right' : 'left');
+
+ tooltip.valueFormatter(function(d, i) {
+ return yAxis.tickFormat()(d, i);
+ }).headerFormatter(function(d, i) {
+ return xAxis.tickFormat()(d, i);
+ });
+
+ controls.updateState(false);
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ var dx = d3.scale.linear()
+ , index = {i: 0, x: 0}
+ , renderWatch = nv.utils.renderWatch(dispatch, duration)
+ ;
+
+ var stateGetter = function(data) {
+ return function(){
+ return {
+ active: data.map(function(d) { return !d.disabled }),
+ index: index.i,
+ rescaleY: rescaleY
+ };
+ }
+ };
+
+ var stateSetter = function(data) {
+ return function(state) {
+ if (state.index !== undefined)
+ index.i = state.index;
+ if (state.rescaleY !== undefined)
+ rescaleY = state.rescaleY;
+ if (state.active !== undefined)
+ data.forEach(function(series,i) {
+ series.disabled = !state.active[i];
+ });
+ }
+ };
+
+ function chart(selection) {
+ renderWatch.reset();
+ renderWatch.models(lines);
+ if (showXAxis) renderWatch.models(xAxis);
+ if (showYAxis) renderWatch.models(yAxis);
+ selection.each(function(data) {
+ var container = d3.select(this);
+ nv.utils.initSVG(container);
+ container.classed('nv-chart-' + id, true);
+ var that = this;
+
+ var availableWidth = nv.utils.availableWidth(width, container, margin),
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+
+ chart.update = function() {
+ if (duration === 0)
+ container.call(chart);
+ else
+ container.transition().duration(duration).call(chart)
+ };
+ chart.container = this;
+
+ state
+ .setter(stateSetter(data), chart.update)
+ .getter(stateGetter(data))
+ .update();
+
+ // DEPRECATED set state.disableddisabled
+ state.disabled = data.map(function(d) { return !!d.disabled });
+
+ if (!defaultState) {
+ var key;
+ defaultState = {};
+ for (key in state) {
+ if (state[key] instanceof Array)
+ defaultState[key] = state[key].slice(0);
+ else
+ defaultState[key] = state[key];
+ }
+ }
+
+ var indexDrag = d3.behavior.drag()
+ .on('dragstart', dragStart)
+ .on('drag', dragMove)
+ .on('dragend', dragEnd);
+
+
+ function dragStart(d,i) {
+ d3.select(chart.container)
+ .style('cursor', 'ew-resize');
+ }
+
+ function dragMove(d,i) {
+ index.x = d3.event.x;
+ index.i = Math.round(dx.invert(index.x));
+ updateZero();
+ }
+
+ function dragEnd(d,i) {
+ d3.select(chart.container)
+ .style('cursor', 'auto');
+
+ // update state and send stateChange with new index
+ state.index = index.i;
+ dispatch.stateChange(state);
+ }
+
+ // Display No Data message if there's nothing to show.
+ if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) {
+ nv.utils.noData(chart, container)
+ return chart;
+ } else {
+ container.selectAll('.nv-noData').remove();
+ }
+
+ // Setup Scales
+ x = lines.xScale();
+ y = lines.yScale();
+
+ if (!rescaleY) {
+ var seriesDomains = data
+ .filter(function(series) { return !series.disabled })
+ .map(function(series,i) {
+ var initialDomain = d3.extent(series.values, lines.y());
+
+ //account for series being disabled when losing 95% or more
+ if (initialDomain[0] < -.95) initialDomain[0] = -.95;
+
+ return [
+ (initialDomain[0] - initialDomain[1]) / (1 + initialDomain[1]),
+ (initialDomain[1] - initialDomain[0]) / (1 + initialDomain[0])
+ ];
+ });
+
+ var completeDomain = [
+ d3.min(seriesDomains, function(d) { return d[0] }),
+ d3.max(seriesDomains, function(d) { return d[1] })
+ ];
+
+ lines.yDomain(completeDomain);
+ } else {
+ lines.yDomain(null);
+ }
+
+ dx.domain([0, data[0].values.length - 1]) //Assumes all series have same length
+ .range([0, availableWidth])
+ .clamp(true);
+
+ var data = indexify(index.i, data);
+
+ // Setup containers and skeleton of chart
+ var interactivePointerEvents = (useInteractiveGuideline) ? "none" : "all";
+ var wrap = container.selectAll('g.nv-wrap.nv-cumulativeLine').data([data]);
+ var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-cumulativeLine').append('g');
+ var g = wrap.select('g');
+
+ gEnter.append('g').attr('class', 'nv-interactive');
+ gEnter.append('g').attr('class', 'nv-x nv-axis').style("pointer-events","none");
+ gEnter.append('g').attr('class', 'nv-y nv-axis');
+ gEnter.append('g').attr('class', 'nv-background');
+ gEnter.append('g').attr('class', 'nv-linesWrap').style("pointer-events",interactivePointerEvents);
+ gEnter.append('g').attr('class', 'nv-avgLinesWrap').style("pointer-events","none");
+ gEnter.append('g').attr('class', 'nv-legendWrap');
+ gEnter.append('g').attr('class', 'nv-controlsWrap');
+
+ // Legend
+ if (showLegend) {
+ legend.width(availableWidth);
+
+ g.select('.nv-legendWrap')
+ .datum(data)
+ .call(legend);
+
+ if ( margin.top != legend.height()) {
+ margin.top = legend.height();
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+ }
+
+ g.select('.nv-legendWrap')
+ .attr('transform', 'translate(0,' + (-margin.top) +')')
+ }
+
+ // Controls
+ if (showControls) {
+ var controlsData = [
+ { key: 'Re-scale y-axis', disabled: !rescaleY }
+ ];
+
+ controls
+ .width(140)
+ .color(['#444', '#444', '#444'])
+ .rightAlign(false)
+ .margin({top: 5, right: 0, bottom: 5, left: 20})
+ ;
+
+ g.select('.nv-controlsWrap')
+ .datum(controlsData)
+ .attr('transform', 'translate(0,' + (-margin.top) +')')
+ .call(controls);
+ }
+
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ if (rightAlignYAxis) {
+ g.select(".nv-y.nv-axis")
+ .attr("transform", "translate(" + availableWidth + ",0)");
+ }
+
+ // Show error if series goes below 100%
+ var tempDisabled = data.filter(function(d) { return d.tempDisabled });
+
+ wrap.select('.tempDisabled').remove(); //clean-up and prevent duplicates
+ if (tempDisabled.length) {
+ wrap.append('text').attr('class', 'tempDisabled')
+ .attr('x', availableWidth / 2)
+ .attr('y', '-.71em')
+ .style('text-anchor', 'end')
+ .text(tempDisabled.map(function(d) { return d.key }).join(', ') + ' values cannot be calculated for this time period.');
+ }
+
+ //Set up interactive layer
+ if (useInteractiveGuideline) {
+ interactiveLayer
+ .width(availableWidth)
+ .height(availableHeight)
+ .margin({left:margin.left,top:margin.top})
+ .svgContainer(container)
+ .xScale(x);
+ wrap.select(".nv-interactive").call(interactiveLayer);
+ }
+
+ gEnter.select('.nv-background')
+ .append('rect');
+
+ g.select('.nv-background rect')
+ .attr('width', availableWidth)
+ .attr('height', availableHeight);
+
+ lines
+ //.x(function(d) { return d.x })
+ .y(function(d) { return d.display.y })
+ .width(availableWidth)
+ .height(availableHeight)
+ .color(data.map(function(d,i) {
+ return d.color || color(d, i);
+ }).filter(function(d,i) { return !data[i].disabled && !data[i].tempDisabled; }));
+
+ var linesWrap = g.select('.nv-linesWrap')
+ .datum(data.filter(function(d) { return !d.disabled && !d.tempDisabled }));
+
+ linesWrap.call(lines);
+
+ //Store a series index number in the data array.
+ data.forEach(function(d,i) {
+ d.seriesIndex = i;
+ });
+
+ var avgLineData = data.filter(function(d) {
+ return !d.disabled && !!average(d);
+ });
+
+ var avgLines = g.select(".nv-avgLinesWrap").selectAll("line")
+ .data(avgLineData, function(d) { return d.key; });
+
+ var getAvgLineY = function(d) {
+ //If average lines go off the svg element, clamp them to the svg bounds.
+ var yVal = y(average(d));
+ if (yVal < 0) return 0;
+ if (yVal > availableHeight) return availableHeight;
+ return yVal;
+ };
+
+ avgLines.enter()
+ .append('line')
+ .style('stroke-width',2)
+ .style('stroke-dasharray','10,10')
+ .style('stroke',function (d,i) {
+ return lines.color()(d,d.seriesIndex);
+ })
+ .attr('x1',0)
+ .attr('x2',availableWidth)
+ .attr('y1', getAvgLineY)
+ .attr('y2', getAvgLineY);
+
+ avgLines
+ .style('stroke-opacity',function(d){
+ //If average lines go offscreen, make them transparent
+ var yVal = y(average(d));
+ if (yVal < 0 || yVal > availableHeight) return 0;
+ return 1;
+ })
+ .attr('x1',0)
+ .attr('x2',availableWidth)
+ .attr('y1', getAvgLineY)
+ .attr('y2', getAvgLineY);
+
+ avgLines.exit().remove();
+
+ //Create index line
+ var indexLine = linesWrap.selectAll('.nv-indexLine')
+ .data([index]);
+ indexLine.enter().append('rect').attr('class', 'nv-indexLine')
+ .attr('width', 3)
+ .attr('x', -2)
+ .attr('fill', 'red')
+ .attr('fill-opacity', .5)
+ .style("pointer-events","all")
+ .call(indexDrag);
+
+ indexLine
+ .attr('transform', function(d) { return 'translate(' + dx(d.i) + ',0)' })
+ .attr('height', availableHeight);
+
+ // Setup Axes
+ if (showXAxis) {
+ xAxis
+ .scale(x)
+ ._ticks( nv.utils.calcTicksX(availableWidth/70, data) )
+ .tickSize(-availableHeight, 0);
+
+ g.select('.nv-x.nv-axis')
+ .attr('transform', 'translate(0,' + y.range()[0] + ')');
+ g.select('.nv-x.nv-axis')
+ .call(xAxis);
+ }
+
+ if (showYAxis) {
+ yAxis
+ .scale(y)
+ ._ticks( nv.utils.calcTicksY(availableHeight/36, data) )
+ .tickSize( -availableWidth, 0);
+
+ g.select('.nv-y.nv-axis')
+ .call(yAxis);
+ }
+
+ //============================================================
+ // Event Handling/Dispatching (in chart's scope)
+ //------------------------------------------------------------
+
+ function updateZero() {
+ indexLine
+ .data([index]);
+
+ //When dragging the index line, turn off line transitions.
+ // Then turn them back on when done dragging.
+ var oldDuration = chart.duration();
+ chart.duration(0);
+ chart.update();
+ chart.duration(oldDuration);
+ }
+
+ g.select('.nv-background rect')
+ .on('click', function() {
+ index.x = d3.mouse(this)[0];
+ index.i = Math.round(dx.invert(index.x));
+
+ // update state and send stateChange with new index
+ state.index = index.i;
+ dispatch.stateChange(state);
+
+ updateZero();
+ });
+
+ lines.dispatch.on('elementClick', function(e) {
+ index.i = e.pointIndex;
+ index.x = dx(index.i);
+
+ // update state and send stateChange with new index
+ state.index = index.i;
+ dispatch.stateChange(state);
+
+ updateZero();
+ });
+
+ controls.dispatch.on('legendClick', function(d,i) {
+ d.disabled = !d.disabled;
+ rescaleY = !d.disabled;
+
+ state.rescaleY = rescaleY;
+ dispatch.stateChange(state);
+ chart.update();
+ });
+
+ legend.dispatch.on('stateChange', function(newState) {
+ for (var key in newState)
+ state[key] = newState[key];
+ dispatch.stateChange(state);
+ chart.update();
+ });
+
+ interactiveLayer.dispatch.on('elementMousemove', function(e) {
+ lines.clearHighlights();
+ var singlePoint, pointIndex, pointXLocation, allData = [];
+
+ data
+ .filter(function(series, i) {
+ series.seriesIndex = i;
+ return !series.disabled;
+ })
+ .forEach(function(series,i) {
+ pointIndex = nv.interactiveBisect(series.values, e.pointXValue, chart.x());
+ lines.highlightPoint(i, pointIndex, true);
+ var point = series.values[pointIndex];
+ if (typeof point === 'undefined') return;
+ if (typeof singlePoint === 'undefined') singlePoint = point;
+ if (typeof pointXLocation === 'undefined') pointXLocation = chart.xScale()(chart.x()(point,pointIndex));
+ allData.push({
+ key: series.key,
+ value: chart.y()(point, pointIndex),
+ color: color(series,series.seriesIndex)
+ });
+ });
+
+ //Highlight the tooltip entry based on which point the mouse is closest to.
+ if (allData.length > 2) {
+ var yValue = chart.yScale().invert(e.mouseY);
+ var domainExtent = Math.abs(chart.yScale().domain()[0] - chart.yScale().domain()[1]);
+ var threshold = 0.03 * domainExtent;
+ var indexToHighlight = nv.nearestValueIndex(allData.map(function(d){return d.value}),yValue,threshold);
+ if (indexToHighlight !== null)
+ allData[indexToHighlight].highlight = true;
+ }
+
+ var xValue = xAxis.tickFormat()(chart.x()(singlePoint,pointIndex), pointIndex);
+ interactiveLayer.tooltip
+ .position({left: pointXLocation + margin.left, top: e.mouseY + margin.top})
+ .chartContainer(that.parentNode)
+ .valueFormatter(function(d,i) {
+ return yAxis.tickFormat()(d);
+ })
+ .data(
+ {
+ value: xValue,
+ series: allData
+ }
+ )();
+
+ interactiveLayer.renderGuideLine(pointXLocation);
+ });
+
+ interactiveLayer.dispatch.on("elementMouseout",function(e) {
+ lines.clearHighlights();
+ });
+
+ // Update chart from a state object passed to event handler
+ dispatch.on('changeState', function(e) {
+ if (typeof e.disabled !== 'undefined') {
+ data.forEach(function(series,i) {
+ series.disabled = e.disabled[i];
+ });
+
+ state.disabled = e.disabled;
+ }
+
+ if (typeof e.index !== 'undefined') {
+ index.i = e.index;
+ index.x = dx(index.i);
+
+ state.index = e.index;
+
+ indexLine
+ .data([index]);
+ }
+
+ if (typeof e.rescaleY !== 'undefined') {
+ rescaleY = e.rescaleY;
+ }
+
+ chart.update();
+ });
+
+ });
+
+ renderWatch.renderEnd('cumulativeLineChart immediate');
+
+ return chart;
+ }
+
+ //============================================================
+ // Event Handling/Dispatching (out of chart's scope)
+ //------------------------------------------------------------
+
+ lines.dispatch.on('elementMouseover.tooltip', function(evt) {
+ var point = {
+ x: chart.x()(evt.point),
+ y: chart.y()(evt.point),
+ color: evt.point.color
+ };
+ evt.point = point;
+ tooltip.data(evt).position(evt.pos).hidden(false);
+ });
+
+ lines.dispatch.on('elementMouseout.tooltip', function(evt) {
+ tooltip.hidden(true)
+ });
+
+ //============================================================
+ // Functions
+ //------------------------------------------------------------
+
+ var indexifyYGetter = null;
+ /* Normalize the data according to an index point. */
+ function indexify(idx, data) {
+ if (!indexifyYGetter) indexifyYGetter = lines.y();
+ return data.map(function(line, i) {
+ if (!line.values) {
+ return line;
+ }
+ var indexValue = line.values[idx];
+ if (indexValue == null) {
+ return line;
+ }
+ var v = indexifyYGetter(indexValue, idx);
+
+ //TODO: implement check below, and disable series if series loses 100% or more cause divide by 0 issue
+ if (v < -.95 && !noErrorCheck) {
+ //if a series loses more than 100%, calculations fail.. anything close can cause major distortion (but is mathematically correct till it hits 100)
+
+ line.tempDisabled = true;
+ return line;
+ }
+
+ line.tempDisabled = false;
+
+ line.values = line.values.map(function(point, pointIndex) {
+ point.display = {'y': (indexifyYGetter(point, pointIndex) - v) / (1 + v) };
+ return point;
+ });
+
+ return line;
+ })
+ }
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ // expose chart's sub-components
+ chart.dispatch = dispatch;
+ chart.lines = lines;
+ chart.legend = legend;
+ chart.controls = controls;
+ chart.xAxis = xAxis;
+ chart.yAxis = yAxis;
+ chart.interactiveLayer = interactiveLayer;
+ chart.state = state;
+ chart.tooltip = tooltip;
+
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ rescaleY: {get: function(){return rescaleY;}, set: function(_){rescaleY=_;}},
+ showControls: {get: function(){return showControls;}, set: function(_){showControls=_;}},
+ showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}},
+ average: {get: function(){return average;}, set: function(_){average=_;}},
+ defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}},
+ noData: {get: function(){return noData;}, set: function(_){noData=_;}},
+ showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}},
+ showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}},
+ noErrorCheck: {get: function(){return noErrorCheck;}, set: function(_){noErrorCheck=_;}},
+
+ // deprecated options
+ tooltips: {get: function(){return tooltip.enabled();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead');
+ tooltip.enabled(!!_);
+ }},
+ tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead');
+ tooltip.contentGenerator(_);
+ }},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ legend.color(color);
+ }},
+ useInteractiveGuideline: {get: function(){return useInteractiveGuideline;}, set: function(_){
+ useInteractiveGuideline = _;
+ if (_ === true) {
+ chart.interactive(false);
+ chart.useVoronoi(false);
+ }
+ }},
+ rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){
+ rightAlignYAxis = _;
+ yAxis.orient( (_) ? 'right' : 'left');
+ }},
+ duration: {get: function(){return duration;}, set: function(_){
+ duration = _;
+ lines.duration(duration);
+ xAxis.duration(duration);
+ yAxis.duration(duration);
+ renderWatch.reset(duration);
+ }}
+ });
+
+ nv.utils.inheritOptions(chart, lines);
+ nv.utils.initOptions(chart);
+
+ return chart;
+ };
+//TODO: consider deprecating by adding necessary features to multiBar model
+ nv.models.discreteBar = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var margin = {top: 0, right: 0, bottom: 0, left: 0}
+ , width = 960
+ , height = 500
+ , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one
+ , container
+ , x = d3.scale.ordinal()
+ , y = d3.scale.linear()
+ , getX = function(d) { return d.x }
+ , getY = function(d) { return d.y }
+ , forceY = [0] // 0 is forced by default.. this makes sense for the majority of bar graphs... user can always do chart.forceY([]) to remove
+ , color = nv.utils.defaultColor()
+ , showValues = false
+ , valueFormat = d3.format(',.2f')
+ , xDomain
+ , yDomain
+ , xRange
+ , yRange
+ , dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd')
+ , rectClass = 'discreteBar'
+ , duration = 250
+ ;
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ var x0, y0;
+ var renderWatch = nv.utils.renderWatch(dispatch, duration);
+
+ function chart(selection) {
+ renderWatch.reset();
+ selection.each(function(data) {
+ var availableWidth = width - margin.left - margin.right,
+ availableHeight = height - margin.top - margin.bottom;
+
+ container = d3.select(this);
+ nv.utils.initSVG(container);
+
+ //add series index to each data point for reference
+ data.forEach(function(series, i) {
+ series.values.forEach(function(point) {
+ point.series = i;
+ });
+ });
+
+ // Setup Scales
+ // remap and flatten the data for use in calculating the scales' domains
+ var seriesData = (xDomain && yDomain) ? [] : // if we know xDomain and yDomain, no need to calculate
+ data.map(function(d) {
+ return d.values.map(function(d,i) {
+ return { x: getX(d,i), y: getY(d,i), y0: d.y0 }
+ })
+ });
+
+ x .domain(xDomain || d3.merge(seriesData).map(function(d) { return d.x }))
+ .rangeBands(xRange || [0, availableWidth], .1);
+ y .domain(yDomain || d3.extent(d3.merge(seriesData).map(function(d) { return d.y }).concat(forceY)));
+
+ // If showValues, pad the Y axis range to account for label height
+ if (showValues) y.range(yRange || [availableHeight - (y.domain()[0] < 0 ? 12 : 0), y.domain()[1] > 0 ? 12 : 0]);
+ else y.range(yRange || [availableHeight, 0]);
+
+ //store old scales if they exist
+ x0 = x0 || x;
+ y0 = y0 || y.copy().range([y(0),y(0)]);
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-discretebar').data([data]);
+ var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-discretebar');
+ var gEnter = wrapEnter.append('g');
+ var g = wrap.select('g');
+
+ gEnter.append('g').attr('class', 'nv-groups');
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ //TODO: by definition, the discrete bar should not have multiple groups, will modify/remove later
+ var groups = wrap.select('.nv-groups').selectAll('.nv-group')
+ .data(function(d) { return d }, function(d) { return d.key });
+ groups.enter().append('g')
+ .style('stroke-opacity', 1e-6)
+ .style('fill-opacity', 1e-6);
+ groups.exit()
+ .watchTransition(renderWatch, 'discreteBar: exit groups')
+ .style('stroke-opacity', 1e-6)
+ .style('fill-opacity', 1e-6)
+ .remove();
+ groups
+ .attr('class', function(d,i) { return 'nv-group nv-series-' + i })
+ .classed('hover', function(d) { return d.hover });
+ groups
+ .watchTransition(renderWatch, 'discreteBar: groups')
+ .style('stroke-opacity', 1)
+ .style('fill-opacity', .75);
+
+ var bars = groups.selectAll('g.nv-bar')
+ .data(function(d) { return d.values });
+ bars.exit().remove();
+
+ var barsEnter = bars.enter().append('g')
+ .attr('transform', function(d,i,j) {
+ return 'translate(' + (x(getX(d,i)) + x.rangeBand() * .05 ) + ', ' + y(0) + ')'
+ })
+ .on('mouseover', function(d,i) { //TODO: figure out why j works above, but not here
+ d3.select(this).classed('hover', true);
+ dispatch.elementMouseover({
+ data: d,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+ })
+ .on('mouseout', function(d,i) {
+ d3.select(this).classed('hover', false);
+ dispatch.elementMouseout({
+ data: d,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+ })
+ .on('mousemove', function(d,i) {
+ dispatch.elementMousemove({
+ data: d,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+ })
+ .on('click', function(d,i) {
+ dispatch.elementClick({
+ data: d,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+ d3.event.stopPropagation();
+ })
+ .on('dblclick', function(d,i) {
+ dispatch.elementDblClick({
+ data: d,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+ d3.event.stopPropagation();
+ });
+
+ barsEnter.append('rect')
+ .attr('height', 0)
+ .attr('width', x.rangeBand() * .9 / data.length )
+
+ if (showValues) {
+ barsEnter.append('text')
+ .attr('text-anchor', 'middle')
+ ;
+
+ bars.select('text')
+ .text(function(d,i) { return valueFormat(getY(d,i)) })
+ .watchTransition(renderWatch, 'discreteBar: bars text')
+ .attr('x', x.rangeBand() * .9 / 2)
+ .attr('y', function(d,i) { return getY(d,i) < 0 ? y(getY(d,i)) - y(0) + 12 : -4 })
+
+ ;
+ } else {
+ bars.selectAll('text').remove();
+ }
+
+ bars
+ .attr('class', function(d,i) { return getY(d,i) < 0 ? 'nv-bar negative' : 'nv-bar positive' })
+ .style('fill', function(d,i) { return d.color || color(d,i) })
+ .style('stroke', function(d,i) { return d.color || color(d,i) })
+ .select('rect')
+ .attr('class', rectClass)
+ .watchTransition(renderWatch, 'discreteBar: bars rect')
+ .attr('width', x.rangeBand() * .9 / data.length);
+ bars.watchTransition(renderWatch, 'discreteBar: bars')
+ //.delay(function(d,i) { return i * 1200 / data[0].values.length })
+ .attr('transform', function(d,i) {
+ var left = x(getX(d,i)) + x.rangeBand() * .05,
+ top = getY(d,i) < 0 ?
+ y(0) :
+ y(0) - y(getY(d,i)) < 1 ?
+ y(0) - 1 : //make 1 px positive bars show up above y=0
+ y(getY(d,i));
+
+ return 'translate(' + left + ', ' + top + ')'
+ })
+ .select('rect')
+ .attr('height', function(d,i) {
+ return Math.max(Math.abs(y(getY(d,i)) - y((yDomain && yDomain[0]) || 0)) || 1)
+ });
+
+
+ //store old scales for use in transitions on update
+ x0 = x.copy();
+ y0 = y.copy();
+
+ });
+
+ renderWatch.renderEnd('discreteBar immediate');
+ return chart;
+ }
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ chart.dispatch = dispatch;
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ forceY: {get: function(){return forceY;}, set: function(_){forceY=_;}},
+ showValues: {get: function(){return showValues;}, set: function(_){showValues=_;}},
+ x: {get: function(){return getX;}, set: function(_){getX=_;}},
+ y: {get: function(){return getY;}, set: function(_){getY=_;}},
+ xScale: {get: function(){return x;}, set: function(_){x=_;}},
+ yScale: {get: function(){return y;}, set: function(_){y=_;}},
+ xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}},
+ yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}},
+ xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}},
+ yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}},
+ valueFormat: {get: function(){return valueFormat;}, set: function(_){valueFormat=_;}},
+ id: {get: function(){return id;}, set: function(_){id=_;}},
+ rectClass: {get: function(){return rectClass;}, set: function(_){rectClass=_;}},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ }},
+ duration: {get: function(){return duration;}, set: function(_){
+ duration = _;
+ renderWatch.reset(duration);
+ }}
+ });
+
+ nv.utils.initOptions(chart);
+
+ return chart;
+ };
+
+ nv.models.discreteBarChart = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var discretebar = nv.models.discreteBar()
+ , xAxis = nv.models.axis()
+ , yAxis = nv.models.axis()
+ , tooltip = nv.models.tooltip()
+ ;
+
+ var margin = {top: 15, right: 10, bottom: 50, left: 60}
+ , width = null
+ , height = null
+ , color = nv.utils.getColor()
+ , showXAxis = true
+ , showYAxis = true
+ , rightAlignYAxis = false
+ , staggerLabels = false
+ , x
+ , y
+ , noData = null
+ , dispatch = d3.dispatch('beforeUpdate','renderEnd')
+ , duration = 250
+ ;
+
+ xAxis
+ .orient('bottom')
+ .showMaxMin(false)
+ .tickFormat(function(d) { return d })
+ ;
+ yAxis
+ .orient((rightAlignYAxis) ? 'right' : 'left')
+ .tickFormat(d3.format(',.1f'))
+ ;
+
+ tooltip
+ .duration(0)
+ .headerEnabled(false)
+ .valueFormatter(function(d, i) {
+ return yAxis.tickFormat()(d, i);
+ })
+ .keyFormatter(function(d, i) {
+ return xAxis.tickFormat()(d, i);
+ });
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ var renderWatch = nv.utils.renderWatch(dispatch, duration);
+
+ function chart(selection) {
+ renderWatch.reset();
+ renderWatch.models(discretebar);
+ if (showXAxis) renderWatch.models(xAxis);
+ if (showYAxis) renderWatch.models(yAxis);
+
+ selection.each(function(data) {
+ var container = d3.select(this),
+ that = this;
+ nv.utils.initSVG(container);
+ var availableWidth = nv.utils.availableWidth(width, container, margin),
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+
+ chart.update = function() {
+ dispatch.beforeUpdate();
+ container.transition().duration(duration).call(chart);
+ };
+ chart.container = this;
+
+ // Display No Data message if there's nothing to show.
+ if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) {
+ nv.utils.noData(chart, container);
+ return chart;
+ } else {
+ container.selectAll('.nv-noData').remove();
+ }
+
+ // Setup Scales
+ x = discretebar.xScale();
+ y = discretebar.yScale().clamp(true);
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-discreteBarWithAxes').data([data]);
+ var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-discreteBarWithAxes').append('g');
+ var defsEnter = gEnter.append('defs');
+ var g = wrap.select('g');
+
+ gEnter.append('g').attr('class', 'nv-x nv-axis');
+ gEnter.append('g').attr('class', 'nv-y nv-axis')
+ .append('g').attr('class', 'nv-zeroLine')
+ .append('line');
+
+ gEnter.append('g').attr('class', 'nv-barsWrap');
+
+ g.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ if (rightAlignYAxis) {
+ g.select(".nv-y.nv-axis")
+ .attr("transform", "translate(" + availableWidth + ",0)");
+ }
+
+ // Main Chart Component(s)
+ discretebar
+ .width(availableWidth)
+ .height(availableHeight);
+
+ var barsWrap = g.select('.nv-barsWrap')
+ .datum(data.filter(function(d) { return !d.disabled }));
+
+ barsWrap.transition().call(discretebar);
+
+
+ defsEnter.append('clipPath')
+ .attr('id', 'nv-x-label-clip-' + discretebar.id())
+ .append('rect');
+
+ g.select('#nv-x-label-clip-' + discretebar.id() + ' rect')
+ .attr('width', x.rangeBand() * (staggerLabels ? 2 : 1))
+ .attr('height', 16)
+ .attr('x', -x.rangeBand() / (staggerLabels ? 1 : 2 ));
+
+ // Setup Axes
+ if (showXAxis) {
+ xAxis
+ .scale(x)
+ ._ticks( nv.utils.calcTicksX(availableWidth/100, data) )
+ .tickSize(-availableHeight, 0);
+
+ g.select('.nv-x.nv-axis')
+ .attr('transform', 'translate(0,' + (y.range()[0] + ((discretebar.showValues() && y.domain()[0] < 0) ? 16 : 0)) + ')');
+ g.select('.nv-x.nv-axis').call(xAxis);
+
+ var xTicks = g.select('.nv-x.nv-axis').selectAll('g');
+ if (staggerLabels) {
+ xTicks
+ .selectAll('text')
+ .attr('transform', function(d,i,j) { return 'translate(0,' + (j % 2 == 0 ? '5' : '17') + ')' })
+ }
+ }
+
+ if (showYAxis) {
+ yAxis
+ .scale(y)
+ ._ticks( nv.utils.calcTicksY(availableHeight/36, data) )
+ .tickSize( -availableWidth, 0);
+
+ g.select('.nv-y.nv-axis').call(yAxis);
+ }
+
+ // Zero line
+ g.select(".nv-zeroLine line")
+ .attr("x1",0)
+ .attr("x2",availableWidth)
+ .attr("y1", y(0))
+ .attr("y2", y(0))
+ ;
+ });
+
+ renderWatch.renderEnd('discreteBar chart immediate');
+ return chart;
+ }
+
+ //============================================================
+ // Event Handling/Dispatching (out of chart's scope)
+ //------------------------------------------------------------
+
+ discretebar.dispatch.on('elementMouseover.tooltip', function(evt) {
+ evt['series'] = {
+ key: chart.x()(evt.data),
+ value: chart.y()(evt.data),
+ color: evt.color
+ };
+ tooltip.data(evt).hidden(false);
+ });
+
+ discretebar.dispatch.on('elementMouseout.tooltip', function(evt) {
+ tooltip.hidden(true);
+ });
+
+ discretebar.dispatch.on('elementMousemove.tooltip', function(evt) {
+ tooltip.position({top: d3.event.pageY, left: d3.event.pageX})();
+ });
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ chart.dispatch = dispatch;
+ chart.discretebar = discretebar;
+ chart.xAxis = xAxis;
+ chart.yAxis = yAxis;
+ chart.tooltip = tooltip;
+
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ staggerLabels: {get: function(){return staggerLabels;}, set: function(_){staggerLabels=_;}},
+ showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}},
+ showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}},
+ noData: {get: function(){return noData;}, set: function(_){noData=_;}},
+
+ // deprecated options
+ tooltips: {get: function(){return tooltip.enabled();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead');
+ tooltip.enabled(!!_);
+ }},
+ tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead');
+ tooltip.contentGenerator(_);
+ }},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ duration: {get: function(){return duration;}, set: function(_){
+ duration = _;
+ renderWatch.reset(duration);
+ discretebar.duration(duration);
+ xAxis.duration(duration);
+ yAxis.duration(duration);
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ discretebar.color(color);
+ }},
+ rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){
+ rightAlignYAxis = _;
+ yAxis.orient( (_) ? 'right' : 'left');
+ }}
+ });
+
+ nv.utils.inheritOptions(chart, discretebar);
+ nv.utils.initOptions(chart);
+
+ return chart;
+ }
+
+ nv.models.distribution = function() {
+ "use strict";
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var margin = {top: 0, right: 0, bottom: 0, left: 0}
+ , width = 400 //technically width or height depending on x or y....
+ , size = 8
+ , axis = 'x' // 'x' or 'y'... horizontal or vertical
+ , getData = function(d) { return d[axis] } // defaults d.x or d.y
+ , color = nv.utils.defaultColor()
+ , scale = d3.scale.linear()
+ , domain
+ , duration = 250
+ , dispatch = d3.dispatch('renderEnd')
+ ;
+
+ //============================================================
+
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ var scale0;
+ var renderWatch = nv.utils.renderWatch(dispatch, duration);
+
+ //============================================================
+
+
+ function chart(selection) {
+ renderWatch.reset();
+ selection.each(function(data) {
+ var availableLength = width - (axis === 'x' ? margin.left + margin.right : margin.top + margin.bottom),
+ naxis = axis == 'x' ? 'y' : 'x',
+ container = d3.select(this);
+ nv.utils.initSVG(container);
+
+ //------------------------------------------------------------
+ // Setup Scales
+
+ scale0 = scale0 || scale;
+
+ //------------------------------------------------------------
+
+
+ //------------------------------------------------------------
+ // Setup containers and skeleton of chart
+
+ var wrap = container.selectAll('g.nv-distribution').data([data]);
+ var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-distribution');
+ var gEnter = wrapEnter.append('g');
+ var g = wrap.select('g');
+
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
+
+ //------------------------------------------------------------
+
+
+ var distWrap = g.selectAll('g.nv-dist')
+ .data(function(d) { return d }, function(d) { return d.key });
+
+ distWrap.enter().append('g');
+ distWrap
+ .attr('class', function(d,i) { return 'nv-dist nv-series-' + i })
+ .style('stroke', function(d,i) { return color(d, i) });
+
+ var dist = distWrap.selectAll('line.nv-dist' + axis)
+ .data(function(d) { return d.values })
+ dist.enter().append('line')
+ .attr(axis + '1', function(d,i) { return scale0(getData(d,i)) })
+ .attr(axis + '2', function(d,i) { return scale0(getData(d,i)) })
+ renderWatch.transition(distWrap.exit().selectAll('line.nv-dist' + axis), 'dist exit')
+ // .transition()
+ .attr(axis + '1', function(d,i) { return scale(getData(d,i)) })
+ .attr(axis + '2', function(d,i) { return scale(getData(d,i)) })
+ .style('stroke-opacity', 0)
+ .remove();
+ dist
+ .attr('class', function(d,i) { return 'nv-dist' + axis + ' nv-dist' + axis + '-' + i })
+ .attr(naxis + '1', 0)
+ .attr(naxis + '2', size);
+ renderWatch.transition(dist, 'dist')
+ // .transition()
+ .attr(axis + '1', function(d,i) { return scale(getData(d,i)) })
+ .attr(axis + '2', function(d,i) { return scale(getData(d,i)) })
+
+
+ scale0 = scale.copy();
+
+ });
+ renderWatch.renderEnd('distribution immediate');
+ return chart;
+ }
+
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+ chart.options = nv.utils.optionsFunc.bind(chart);
+ chart.dispatch = dispatch;
+
+ chart.margin = function(_) {
+ if (!arguments.length) return margin;
+ margin.top = typeof _.top != 'undefined' ? _.top : margin.top;
+ margin.right = typeof _.right != 'undefined' ? _.right : margin.right;
+ margin.bottom = typeof _.bottom != 'undefined' ? _.bottom : margin.bottom;
+ margin.left = typeof _.left != 'undefined' ? _.left : margin.left;
+ return chart;
+ };
+
+ chart.width = function(_) {
+ if (!arguments.length) return width;
+ width = _;
+ return chart;
+ };
+
+ chart.axis = function(_) {
+ if (!arguments.length) return axis;
+ axis = _;
+ return chart;
+ };
+
+ chart.size = function(_) {
+ if (!arguments.length) return size;
+ size = _;
+ return chart;
+ };
+
+ chart.getData = function(_) {
+ if (!arguments.length) return getData;
+ getData = d3.functor(_);
+ return chart;
+ };
+
+ chart.scale = function(_) {
+ if (!arguments.length) return scale;
+ scale = _;
+ return chart;
+ };
+
+ chart.color = function(_) {
+ if (!arguments.length) return color;
+ color = nv.utils.getColor(_);
+ return chart;
+ };
+
+ chart.duration = function(_) {
+ if (!arguments.length) return duration;
+ duration = _;
+ renderWatch.reset(duration);
+ return chart;
+ };
+ //============================================================
+
+
+ return chart;
+ }
+ nv.models.furiousLegend = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var margin = {top: 5, right: 0, bottom: 5, left: 0}
+ , width = 400
+ , height = 20
+ , getKey = function(d) { return d.key }
+ , color = nv.utils.getColor()
+ , align = true
+ , padding = 28 //define how much space between legend items. - recommend 32 for furious version
+ , rightAlign = true
+ , updateState = true //If true, legend will update data.disabled and trigger a 'stateChange' dispatch.
+ , radioButtonMode = false //If true, clicking legend items will cause it to behave like a radio button. (only one can be selected at a time)
+ , expanded = false
+ , dispatch = d3.dispatch('legendClick', 'legendDblclick', 'legendMouseover', 'legendMouseout', 'stateChange')
+ , vers = 'classic' //Options are "classic" and "furious"
+ ;
+
+ function chart(selection) {
+ selection.each(function(data) {
+ var availableWidth = width - margin.left - margin.right,
+ container = d3.select(this);
+ nv.utils.initSVG(container);
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-legend').data([data]);
+ var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-legend').append('g');
+ var g = wrap.select('g');
+
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ var series = g.selectAll('.nv-series')
+ .data(function(d) {
+ if(vers != 'furious') return d;
+
+ return d.filter(function(n) {
+ return expanded ? true : !n.disengaged;
+ });
+ });
+ var seriesEnter = series.enter().append('g').attr('class', 'nv-series')
+
+ var seriesShape;
+
+ if(vers == 'classic') {
+ seriesEnter.append('circle')
+ .style('stroke-width', 2)
+ .attr('class','nv-legend-symbol')
+ .attr('r', 5);
+
+ seriesShape = series.select('circle');
+ } else if (vers == 'furious') {
+ seriesEnter.append('rect')
+ .style('stroke-width', 2)
+ .attr('class','nv-legend-symbol')
+ .attr('rx', 3)
+ .attr('ry', 3);
+
+ seriesShape = series.select('rect');
+
+ seriesEnter.append('g')
+ .attr('class', 'nv-check-box')
+ .property('innerHTML','
')
+ .attr('transform', 'translate(-10,-8)scale(0.5)');
+
+ var seriesCheckbox = series.select('.nv-check-box');
+
+ seriesCheckbox.each(function(d,i) {
+ d3.select(this).selectAll('path')
+ .attr('stroke', setTextColor(d,i));
+ });
+ }
+
+ seriesEnter.append('text')
+ .attr('text-anchor', 'start')
+ .attr('class','nv-legend-text')
+ .attr('dy', '.32em')
+ .attr('dx', '8');
+
+ var seriesText = series.select('text.nv-legend-text');
+
+ series
+ .on('mouseover', function(d,i) {
+ dispatch.legendMouseover(d,i); //TODO: Make consistent with other event objects
+ })
+ .on('mouseout', function(d,i) {
+ dispatch.legendMouseout(d,i);
+ })
+ .on('click', function(d,i) {
+ dispatch.legendClick(d,i);
+ // make sure we re-get data in case it was modified
+ var data = series.data();
+ if (updateState) {
+ if(vers =='classic') {
+ if (radioButtonMode) {
+ //Radio button mode: set every series to disabled,
+ // and enable the clicked series.
+ data.forEach(function(series) { series.disabled = true});
+ d.disabled = false;
+ }
+ else {
+ d.disabled = !d.disabled;
+ if (data.every(function(series) { return series.disabled})) {
+ //the default behavior of NVD3 legends is, if every single series
+ // is disabled, turn all series' back on.
+ data.forEach(function(series) { series.disabled = false});
+ }
+ }
+ } else if(vers == 'furious') {
+ if(expanded) {
+ d.disengaged = !d.disengaged;
+ d.userDisabled = d.userDisabled == undefined ? !!d.disabled : d.userDisabled;
+ d.disabled = d.disengaged || d.userDisabled;
+ } else if (!expanded) {
+ d.disabled = !d.disabled;
+ d.userDisabled = d.disabled;
+ var engaged = data.filter(function(d) { return !d.disengaged; });
+ if (engaged.every(function(series) { return series.userDisabled })) {
+ //the default behavior of NVD3 legends is, if every single series
+ // is disabled, turn all series' back on.
+ data.forEach(function(series) {
+ series.disabled = series.userDisabled = false;
+ });
+ }
+ }
+ }
+ dispatch.stateChange({
+ disabled: data.map(function(d) { return !!d.disabled }),
+ disengaged: data.map(function(d) { return !!d.disengaged })
+ });
+
+ }
+ })
+ .on('dblclick', function(d,i) {
+ if(vers == 'furious' && expanded) return;
+ dispatch.legendDblclick(d,i);
+ if (updateState) {
+ // make sure we re-get data in case it was modified
+ var data = series.data();
+ //the default behavior of NVD3 legends, when double clicking one,
+ // is to set all other series' to false, and make the double clicked series enabled.
+ data.forEach(function(series) {
+ series.disabled = true;
+ if(vers == 'furious') series.userDisabled = series.disabled;
+ });
+ d.disabled = false;
+ if(vers == 'furious') d.userDisabled = d.disabled;
+ dispatch.stateChange({
+ disabled: data.map(function(d) { return !!d.disabled })
+ });
+ }
+ });
+
+ series.classed('nv-disabled', function(d) { return d.userDisabled });
+ series.exit().remove();
+
+ seriesText
+ .attr('fill', setTextColor)
+ .text(getKey);
+
+ //TODO: implement fixed-width and max-width options (max-width is especially useful with the align option)
+ // NEW ALIGNING CODE, TODO: clean up
+
+ var versPadding;
+ switch(vers) {
+ case 'furious' :
+ versPadding = 23;
+ break;
+ case 'classic' :
+ versPadding = 20;
+ }
+
+ if (align) {
+
+ var seriesWidths = [];
+ series.each(function(d,i) {
+ var legendText = d3.select(this).select('text');
+ var nodeTextLength;
+ try {
+ nodeTextLength = legendText.node().getComputedTextLength();
+ // If the legendText is display:none'd (nodeTextLength == 0), simulate an error so we approximate, instead
+ if(nodeTextLength <= 0) throw Error();
+ }
+ catch(e) {
+ nodeTextLength = nv.utils.calcApproxTextWidth(legendText);
+ }
+
+ seriesWidths.push(nodeTextLength + padding);
+ });
+
+ var seriesPerRow = 0;
+ var legendWidth = 0;
+ var columnWidths = [];
+
+ while ( legendWidth < availableWidth && seriesPerRow < seriesWidths.length) {
+ columnWidths[seriesPerRow] = seriesWidths[seriesPerRow];
+ legendWidth += seriesWidths[seriesPerRow++];
+ }
+ if (seriesPerRow === 0) seriesPerRow = 1; //minimum of one series per row
+
+ while ( legendWidth > availableWidth && seriesPerRow > 1 ) {
+ columnWidths = [];
+ seriesPerRow--;
+
+ for (var k = 0; k < seriesWidths.length; k++) {
+ if (seriesWidths[k] > (columnWidths[k % seriesPerRow] || 0) )
+ columnWidths[k % seriesPerRow] = seriesWidths[k];
+ }
+
+ legendWidth = columnWidths.reduce(function(prev, cur, index, array) {
+ return prev + cur;
+ });
+ }
+
+ var xPositions = [];
+ for (var i = 0, curX = 0; i < seriesPerRow; i++) {
+ xPositions[i] = curX;
+ curX += columnWidths[i];
+ }
+
+ series
+ .attr('transform', function(d, i) {
+ return 'translate(' + xPositions[i % seriesPerRow] + ',' + (5 + Math.floor(i / seriesPerRow) * versPadding) + ')';
+ });
+
+ //position legend as far right as possible within the total width
+ if (rightAlign) {
+ g.attr('transform', 'translate(' + (width - margin.right - legendWidth) + ',' + margin.top + ')');
+ }
+ else {
+ g.attr('transform', 'translate(0' + ',' + margin.top + ')');
+ }
+
+ height = margin.top + margin.bottom + (Math.ceil(seriesWidths.length / seriesPerRow) * versPadding);
+
+ } else {
+
+ var ypos = 5,
+ newxpos = 5,
+ maxwidth = 0,
+ xpos;
+ series
+ .attr('transform', function(d, i) {
+ var length = d3.select(this).select('text').node().getComputedTextLength() + padding;
+ xpos = newxpos;
+
+ if (width < margin.left + margin.right + xpos + length) {
+ newxpos = xpos = 5;
+ ypos += versPadding;
+ }
+
+ newxpos += length;
+ if (newxpos > maxwidth) maxwidth = newxpos;
+
+ return 'translate(' + xpos + ',' + ypos + ')';
+ });
+
+ //position legend as far right as possible within the total width
+ g.attr('transform', 'translate(' + (width - margin.right - maxwidth) + ',' + margin.top + ')');
+
+ height = margin.top + margin.bottom + ypos + 15;
+ }
+
+ if(vers == 'furious') {
+ // Size rectangles after text is placed
+ seriesShape
+ .attr('width', function(d,i) {
+ return seriesText[0][i].getComputedTextLength() + 27;
+ })
+ .attr('height', 18)
+ .attr('y', -9)
+ .attr('x', -15)
+ }
+
+ seriesShape
+ .style('fill', setBGColor)
+ .style('stroke', function(d,i) { return d.color || color(d, i) });
+ });
+
+ function setTextColor(d,i) {
+ if(vers != 'furious') return '#000';
+ if(expanded) {
+ return d.disengaged ? color(d,i) : '#fff';
+ } else if (!expanded) {
+ return !!d.disabled ? color(d,i) : '#fff';
+ }
+ }
+
+ function setBGColor(d,i) {
+ if(expanded && vers == 'furious') {
+ return d.disengaged ? '#fff' : color(d,i);
+ } else {
+ return !!d.disabled ? '#fff' : color(d,i);
+ }
+ }
+
+ return chart;
+ }
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ chart.dispatch = dispatch;
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ key: {get: function(){return getKey;}, set: function(_){getKey=_;}},
+ align: {get: function(){return align;}, set: function(_){align=_;}},
+ rightAlign: {get: function(){return rightAlign;}, set: function(_){rightAlign=_;}},
+ padding: {get: function(){return padding;}, set: function(_){padding=_;}},
+ updateState: {get: function(){return updateState;}, set: function(_){updateState=_;}},
+ radioButtonMode: {get: function(){return radioButtonMode;}, set: function(_){radioButtonMode=_;}},
+ expanded: {get: function(){return expanded;}, set: function(_){expanded=_;}},
+ vers: {get: function(){return vers;}, set: function(_){vers=_;}},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ }}
+ });
+
+ nv.utils.initOptions(chart);
+
+ return chart;
+ };
+//TODO: consider deprecating and using multibar with single series for this
+ nv.models.historicalBar = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var margin = {top: 0, right: 0, bottom: 0, left: 0}
+ , width = null
+ , height = null
+ , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one
+ , container = null
+ , x = d3.scale.linear()
+ , y = d3.scale.linear()
+ , getX = function(d) { return d.x }
+ , getY = function(d) { return d.y }
+ , forceX = []
+ , forceY = [0]
+ , padData = false
+ , clipEdge = true
+ , color = nv.utils.defaultColor()
+ , xDomain
+ , yDomain
+ , xRange
+ , yRange
+ , dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd')
+ , interactive = true
+ ;
+
+ var renderWatch = nv.utils.renderWatch(dispatch, 0);
+
+ function chart(selection) {
+ selection.each(function(data) {
+ renderWatch.reset();
+
+ container = d3.select(this);
+ var availableWidth = nv.utils.availableWidth(width, container, margin),
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+
+ nv.utils.initSVG(container);
+
+ // Setup Scales
+ x.domain(xDomain || d3.extent(data[0].values.map(getX).concat(forceX) ));
+
+ if (padData)
+ x.range(xRange || [availableWidth * .5 / data[0].values.length, availableWidth * (data[0].values.length - .5) / data[0].values.length ]);
+ else
+ x.range(xRange || [0, availableWidth]);
+
+ y.domain(yDomain || d3.extent(data[0].values.map(getY).concat(forceY) ))
+ .range(yRange || [availableHeight, 0]);
+
+ // If scale's domain don't have a range, slightly adjust to make one... so a chart can show a single data point
+ if (x.domain()[0] === x.domain()[1])
+ x.domain()[0] ?
+ x.domain([x.domain()[0] - x.domain()[0] * 0.01, x.domain()[1] + x.domain()[1] * 0.01])
+ : x.domain([-1,1]);
+
+ if (y.domain()[0] === y.domain()[1])
+ y.domain()[0] ?
+ y.domain([y.domain()[0] + y.domain()[0] * 0.01, y.domain()[1] - y.domain()[1] * 0.01])
+ : y.domain([-1,1]);
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-historicalBar-' + id).data([data[0].values]);
+ var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-historicalBar-' + id);
+ var defsEnter = wrapEnter.append('defs');
+ var gEnter = wrapEnter.append('g');
+ var g = wrap.select('g');
+
+ gEnter.append('g').attr('class', 'nv-bars');
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ container
+ .on('click', function(d,i) {
+ dispatch.chartClick({
+ data: d,
+ index: i,
+ pos: d3.event,
+ id: id
+ });
+ });
+
+ defsEnter.append('clipPath')
+ .attr('id', 'nv-chart-clip-path-' + id)
+ .append('rect');
+
+ wrap.select('#nv-chart-clip-path-' + id + ' rect')
+ .attr('width', availableWidth)
+ .attr('height', availableHeight);
+
+ g.attr('clip-path', clipEdge ? 'url(#nv-chart-clip-path-' + id + ')' : '');
+
+ var bars = wrap.select('.nv-bars').selectAll('.nv-bar')
+ .data(function(d) { return d }, function(d,i) {return getX(d,i)});
+ bars.exit().remove();
+
+ bars.enter().append('rect')
+ .attr('x', 0 )
+ .attr('y', function(d,i) { return nv.utils.NaNtoZero(y(Math.max(0, getY(d,i)))) })
+ .attr('height', function(d,i) { return nv.utils.NaNtoZero(Math.abs(y(getY(d,i)) - y(0))) })
+ .attr('transform', function(d,i) { return 'translate(' + (x(getX(d,i)) - availableWidth / data[0].values.length * .45) + ',0)'; })
+ .on('mouseover', function(d,i) {
+ if (!interactive) return;
+ d3.select(this).classed('hover', true);
+ dispatch.elementMouseover({
+ data: d,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+
+ })
+ .on('mouseout', function(d,i) {
+ if (!interactive) return;
+ d3.select(this).classed('hover', false);
+ dispatch.elementMouseout({
+ data: d,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+ })
+ .on('mousemove', function(d,i) {
+ if (!interactive) return;
+ dispatch.elementMousemove({
+ data: d,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+ })
+ .on('click', function(d,i) {
+ if (!interactive) return;
+ dispatch.elementClick({
+ data: d,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+ d3.event.stopPropagation();
+ })
+ .on('dblclick', function(d,i) {
+ if (!interactive) return;
+ dispatch.elementDblClick({
+ data: d,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+ d3.event.stopPropagation();
+ });
+
+ bars
+ .attr('fill', function(d,i) { return color(d, i); })
+ .attr('class', function(d,i,j) { return (getY(d,i) < 0 ? 'nv-bar negative' : 'nv-bar positive') + ' nv-bar-' + j + '-' + i })
+ .watchTransition(renderWatch, 'bars')
+ .attr('transform', function(d,i) { return 'translate(' + (x(getX(d,i)) - availableWidth / data[0].values.length * .45) + ',0)'; })
+ //TODO: better width calculations that don't assume always uniform data spacing;w
+ .attr('width', (availableWidth / data[0].values.length) * .9 );
+
+ bars.watchTransition(renderWatch, 'bars')
+ .attr('y', function(d,i) {
+ var rval = getY(d,i) < 0 ?
+ y(0) :
+ y(0) - y(getY(d,i)) < 1 ?
+ y(0) - 1 :
+ y(getY(d,i));
+ return nv.utils.NaNtoZero(rval);
+ })
+ .attr('height', function(d,i) { return nv.utils.NaNtoZero(Math.max(Math.abs(y(getY(d,i)) - y(0)),1)) });
+
+ });
+
+ renderWatch.renderEnd('historicalBar immediate');
+ return chart;
+ }
+
+ //Create methods to allow outside functions to highlight a specific bar.
+ chart.highlightPoint = function(pointIndex, isHoverOver) {
+ container
+ .select(".nv-bars .nv-bar-0-" + pointIndex)
+ .classed("hover", isHoverOver)
+ ;
+ };
+
+ chart.clearHighlights = function() {
+ container
+ .select(".nv-bars .nv-bar.hover")
+ .classed("hover", false)
+ ;
+ };
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ chart.dispatch = dispatch;
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ forceX: {get: function(){return forceX;}, set: function(_){forceX=_;}},
+ forceY: {get: function(){return forceY;}, set: function(_){forceY=_;}},
+ padData: {get: function(){return padData;}, set: function(_){padData=_;}},
+ x: {get: function(){return getX;}, set: function(_){getX=_;}},
+ y: {get: function(){return getY;}, set: function(_){getY=_;}},
+ xScale: {get: function(){return x;}, set: function(_){x=_;}},
+ yScale: {get: function(){return y;}, set: function(_){y=_;}},
+ xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}},
+ yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}},
+ xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}},
+ yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}},
+ clipEdge: {get: function(){return clipEdge;}, set: function(_){clipEdge=_;}},
+ id: {get: function(){return id;}, set: function(_){id=_;}},
+ interactive: {get: function(){return interactive;}, set: function(_){interactive=_;}},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ }}
+ });
+
+ nv.utils.initOptions(chart);
+
+ return chart;
+ };
+
+ nv.models.historicalBarChart = function(bar_model) {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var bars = bar_model || nv.models.historicalBar()
+ , xAxis = nv.models.axis()
+ , yAxis = nv.models.axis()
+ , legend = nv.models.legend()
+ , interactiveLayer = nv.interactiveGuideline()
+ , tooltip = nv.models.tooltip()
+ ;
+
+
+ var margin = {top: 30, right: 90, bottom: 50, left: 90}
+ , color = nv.utils.defaultColor()
+ , width = null
+ , height = null
+ , showLegend = false
+ , showXAxis = true
+ , showYAxis = true
+ , rightAlignYAxis = false
+ , useInteractiveGuideline = false
+ , x
+ , y
+ , state = {}
+ , defaultState = null
+ , noData = null
+ , dispatch = d3.dispatch('tooltipHide', 'stateChange', 'changeState', 'renderEnd')
+ , transitionDuration = 250
+ ;
+
+ xAxis.orient('bottom').tickPadding(7);
+ yAxis.orient( (rightAlignYAxis) ? 'right' : 'left');
+ tooltip
+ .duration(0)
+ .headerEnabled(false)
+ .valueFormatter(function(d, i) {
+ return yAxis.tickFormat()(d, i);
+ })
+ .headerFormatter(function(d, i) {
+ return xAxis.tickFormat()(d, i);
+ });
+
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ var renderWatch = nv.utils.renderWatch(dispatch, 0);
+
+ function chart(selection) {
+ selection.each(function(data) {
+ renderWatch.reset();
+ renderWatch.models(bars);
+ if (showXAxis) renderWatch.models(xAxis);
+ if (showYAxis) renderWatch.models(yAxis);
+
+ var container = d3.select(this),
+ that = this;
+ nv.utils.initSVG(container);
+ var availableWidth = nv.utils.availableWidth(width, container, margin),
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+
+ chart.update = function() { container.transition().duration(transitionDuration).call(chart) };
+ chart.container = this;
+
+ //set state.disabled
+ state.disabled = data.map(function(d) { return !!d.disabled });
+
+ if (!defaultState) {
+ var key;
+ defaultState = {};
+ for (key in state) {
+ if (state[key] instanceof Array)
+ defaultState[key] = state[key].slice(0);
+ else
+ defaultState[key] = state[key];
+ }
+ }
+
+ // Display noData message if there's nothing to show.
+ if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) {
+ nv.utils.noData(chart, container)
+ return chart;
+ } else {
+ container.selectAll('.nv-noData').remove();
+ }
+
+ // Setup Scales
+ x = bars.xScale();
+ y = bars.yScale();
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-historicalBarChart').data([data]);
+ var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-historicalBarChart').append('g');
+ var g = wrap.select('g');
+
+ gEnter.append('g').attr('class', 'nv-x nv-axis');
+ gEnter.append('g').attr('class', 'nv-y nv-axis');
+ gEnter.append('g').attr('class', 'nv-barsWrap');
+ gEnter.append('g').attr('class', 'nv-legendWrap');
+ gEnter.append('g').attr('class', 'nv-interactive');
+
+ // Legend
+ if (showLegend) {
+ legend.width(availableWidth);
+
+ g.select('.nv-legendWrap')
+ .datum(data)
+ .call(legend);
+
+ if ( margin.top != legend.height()) {
+ margin.top = legend.height();
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+ }
+
+ wrap.select('.nv-legendWrap')
+ .attr('transform', 'translate(0,' + (-margin.top) +')')
+ }
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ if (rightAlignYAxis) {
+ g.select(".nv-y.nv-axis")
+ .attr("transform", "translate(" + availableWidth + ",0)");
+ }
+
+ //Set up interactive layer
+ if (useInteractiveGuideline) {
+ interactiveLayer
+ .width(availableWidth)
+ .height(availableHeight)
+ .margin({left:margin.left, top:margin.top})
+ .svgContainer(container)
+ .xScale(x);
+ wrap.select(".nv-interactive").call(interactiveLayer);
+ }
+ bars
+ .width(availableWidth)
+ .height(availableHeight)
+ .color(data.map(function(d,i) {
+ return d.color || color(d, i);
+ }).filter(function(d,i) { return !data[i].disabled }));
+
+ var barsWrap = g.select('.nv-barsWrap')
+ .datum(data.filter(function(d) { return !d.disabled }));
+ barsWrap.transition().call(bars);
+
+ // Setup Axes
+ if (showXAxis) {
+ xAxis
+ .scale(x)
+ ._ticks( nv.utils.calcTicksX(availableWidth/100, data) )
+ .tickSize(-availableHeight, 0);
+
+ g.select('.nv-x.nv-axis')
+ .attr('transform', 'translate(0,' + y.range()[0] + ')');
+ g.select('.nv-x.nv-axis')
+ .transition()
+ .call(xAxis);
+ }
+
+ if (showYAxis) {
+ yAxis
+ .scale(y)
+ ._ticks( nv.utils.calcTicksY(availableHeight/36, data) )
+ .tickSize( -availableWidth, 0);
+
+ g.select('.nv-y.nv-axis')
+ .transition()
+ .call(yAxis);
+ }
+
+ //============================================================
+ // Event Handling/Dispatching (in chart's scope)
+ //------------------------------------------------------------
+
+ interactiveLayer.dispatch.on('elementMousemove', function(e) {
+ bars.clearHighlights();
+
+ var singlePoint, pointIndex, pointXLocation, allData = [];
+ data
+ .filter(function(series, i) {
+ series.seriesIndex = i;
+ return !series.disabled;
+ })
+ .forEach(function(series,i) {
+ pointIndex = nv.interactiveBisect(series.values, e.pointXValue, chart.x());
+ bars.highlightPoint(pointIndex,true);
+ var point = series.values[pointIndex];
+ if (point === undefined) return;
+ if (singlePoint === undefined) singlePoint = point;
+ if (pointXLocation === undefined) pointXLocation = chart.xScale()(chart.x()(point,pointIndex));
+ allData.push({
+ key: series.key,
+ value: chart.y()(point, pointIndex),
+ color: color(series,series.seriesIndex),
+ data: series.values[pointIndex]
+ });
+ });
+
+ var xValue = xAxis.tickFormat()(chart.x()(singlePoint,pointIndex));
+ interactiveLayer.tooltip
+ .position({left: pointXLocation + margin.left, top: e.mouseY + margin.top})
+ .chartContainer(that.parentNode)
+ .valueFormatter(function(d,i) {
+ return yAxis.tickFormat()(d);
+ })
+ .data({
+ value: xValue,
+ index: pointIndex,
+ series: allData
+ })();
+
+ interactiveLayer.renderGuideLine(pointXLocation);
+
+ });
+
+ interactiveLayer.dispatch.on("elementMouseout",function(e) {
+ dispatch.tooltipHide();
+ bars.clearHighlights();
+ });
+
+ legend.dispatch.on('legendClick', function(d,i) {
+ d.disabled = !d.disabled;
+
+ if (!data.filter(function(d) { return !d.disabled }).length) {
+ data.map(function(d) {
+ d.disabled = false;
+ wrap.selectAll('.nv-series').classed('disabled', false);
+ return d;
+ });
+ }
+
+ state.disabled = data.map(function(d) { return !!d.disabled });
+ dispatch.stateChange(state);
+
+ selection.transition().call(chart);
+ });
+
+ legend.dispatch.on('legendDblclick', function(d) {
+ //Double clicking should always enable current series, and disabled all others.
+ data.forEach(function(d) {
+ d.disabled = true;
+ });
+ d.disabled = false;
+
+ state.disabled = data.map(function(d) { return !!d.disabled });
+ dispatch.stateChange(state);
+ chart.update();
+ });
+
+ dispatch.on('changeState', function(e) {
+ if (typeof e.disabled !== 'undefined') {
+ data.forEach(function(series,i) {
+ series.disabled = e.disabled[i];
+ });
+
+ state.disabled = e.disabled;
+ }
+
+ chart.update();
+ });
+ });
+
+ renderWatch.renderEnd('historicalBarChart immediate');
+ return chart;
+ }
+
+ //============================================================
+ // Event Handling/Dispatching (out of chart's scope)
+ //------------------------------------------------------------
+
+ bars.dispatch.on('elementMouseover.tooltip', function(evt) {
+ evt['series'] = {
+ key: chart.x()(evt.data),
+ value: chart.y()(evt.data),
+ color: evt.color
+ };
+ tooltip.data(evt).hidden(false);
+ });
+
+ bars.dispatch.on('elementMouseout.tooltip', function(evt) {
+ tooltip.hidden(true);
+ });
+
+ bars.dispatch.on('elementMousemove.tooltip', function(evt) {
+ tooltip.position({top: d3.event.pageY, left: d3.event.pageX})();
+ });
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ // expose chart's sub-components
+ chart.dispatch = dispatch;
+ chart.bars = bars;
+ chart.legend = legend;
+ chart.xAxis = xAxis;
+ chart.yAxis = yAxis;
+ chart.interactiveLayer = interactiveLayer;
+ chart.tooltip = tooltip;
+
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}},
+ showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}},
+ showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}},
+ defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}},
+ noData: {get: function(){return noData;}, set: function(_){noData=_;}},
+
+ // deprecated options
+ tooltips: {get: function(){return tooltip.enabled();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead');
+ tooltip.enabled(!!_);
+ }},
+ tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead');
+ tooltip.contentGenerator(_);
+ }},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ legend.color(color);
+ bars.color(color);
+ }},
+ duration: {get: function(){return transitionDuration;}, set: function(_){
+ transitionDuration=_;
+ renderWatch.reset(transitionDuration);
+ yAxis.duration(transitionDuration);
+ xAxis.duration(transitionDuration);
+ }},
+ rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){
+ rightAlignYAxis = _;
+ yAxis.orient( (_) ? 'right' : 'left');
+ }},
+ useInteractiveGuideline: {get: function(){return useInteractiveGuideline;}, set: function(_){
+ useInteractiveGuideline = _;
+ if (_ === true) {
+ chart.interactive(false);
+ }
+ }}
+ });
+
+ nv.utils.inheritOptions(chart, bars);
+ nv.utils.initOptions(chart);
+
+ return chart;
+ };
+
+
+// ohlcChart is just a historical chart with ohlc bars and some tweaks
+ nv.models.ohlcBarChart = function() {
+ var chart = nv.models.historicalBarChart(nv.models.ohlcBar());
+
+ // special default tooltip since we show multiple values per x
+ chart.useInteractiveGuideline(true);
+ chart.interactiveLayer.tooltip.contentGenerator(function(data) {
+ // we assume only one series exists for this chart
+ var d = data.series[0].data;
+ // match line colors as defined in nv.d3.css
+ var color = d.open < d.close ? "2ca02c" : "d62728";
+ return '' +
+ '
' + data.value + ' ' +
+ '
' +
+ 'open: ' + chart.yAxis.tickFormat()(d.open) + ' ' +
+ 'close: ' + chart.yAxis.tickFormat()(d.close) + ' ' +
+ 'high ' + chart.yAxis.tickFormat()(d.high) + ' ' +
+ 'low: ' + chart.yAxis.tickFormat()(d.low) + ' ' +
+ '
';
+ });
+ return chart;
+ };
+
+// candlestickChart is just a historical chart with candlestick bars and some tweaks
+ nv.models.candlestickBarChart = function() {
+ var chart = nv.models.historicalBarChart(nv.models.candlestickBar());
+
+ // special default tooltip since we show multiple values per x
+ chart.useInteractiveGuideline(true);
+ chart.interactiveLayer.tooltip.contentGenerator(function(data) {
+ // we assume only one series exists for this chart
+ var d = data.series[0].data;
+ // match line colors as defined in nv.d3.css
+ var color = d.open < d.close ? "2ca02c" : "d62728";
+ return '' +
+ '
' + data.value + ' ' +
+ '
' +
+ 'open: ' + chart.yAxis.tickFormat()(d.open) + ' ' +
+ 'close: ' + chart.yAxis.tickFormat()(d.close) + ' ' +
+ 'high ' + chart.yAxis.tickFormat()(d.high) + ' ' +
+ 'low: ' + chart.yAxis.tickFormat()(d.low) + ' ' +
+ '
';
+ });
+ return chart;
+ };
+ nv.models.legend = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var margin = {top: 5, right: 0, bottom: 5, left: 0}
+ , width = 400
+ , height = 20
+ , getKey = function(d) { return d.key }
+ , color = nv.utils.getColor()
+ , align = true
+ , padding = 32 //define how much space between legend items. - recommend 32 for furious version
+ , rightAlign = true
+ , updateState = true //If true, legend will update data.disabled and trigger a 'stateChange' dispatch.
+ , radioButtonMode = false //If true, clicking legend items will cause it to behave like a radio button. (only one can be selected at a time)
+ , expanded = false
+ , dispatch = d3.dispatch('legendClick', 'legendDblclick', 'legendMouseover', 'legendMouseout', 'stateChange')
+ , vers = 'classic' //Options are "classic" and "furious"
+ ;
+
+ function chart(selection) {
+ selection.each(function(data) {
+ var availableWidth = width - margin.left - margin.right,
+ container = d3.select(this);
+ nv.utils.initSVG(container);
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-legend').data([data]);
+ var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-legend').append('g');
+ var g = wrap.select('g');
+
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ var series = g.selectAll('.nv-series')
+ .data(function(d) {
+ if(vers != 'furious') return d;
+
+ return d.filter(function(n) {
+ return expanded ? true : !n.disengaged;
+ });
+ });
+
+ var seriesEnter = series.enter().append('g').attr('class', 'nv-series');
+ var seriesShape;
+
+ var versPadding;
+ switch(vers) {
+ case 'furious' :
+ versPadding = 23;
+ break;
+ case 'classic' :
+ versPadding = 20;
+ }
+
+ if(vers == 'classic') {
+ seriesEnter.append('circle')
+ .style('stroke-width', 2)
+ .attr('class','nv-legend-symbol')
+ .attr('r', 5);
+
+ seriesShape = series.select('circle');
+ } else if (vers == 'furious') {
+ seriesEnter.append('rect')
+ .style('stroke-width', 2)
+ .attr('class','nv-legend-symbol')
+ .attr('rx', 3)
+ .attr('ry', 3);
+
+ seriesShape = series.select('.nv-legend-symbol');
+
+ seriesEnter.append('g')
+ .attr('class', 'nv-check-box')
+ .property('innerHTML','
')
+ .attr('transform', 'translate(-10,-8)scale(0.5)');
+
+ var seriesCheckbox = series.select('.nv-check-box');
+
+ seriesCheckbox.each(function(d,i) {
+ d3.select(this).selectAll('path')
+ .attr('stroke', setTextColor(d,i));
+ });
+ }
+
+ seriesEnter.append('text')
+ .attr('text-anchor', 'start')
+ .attr('class','nv-legend-text')
+ .attr('dy', '.32em')
+ .attr('dx', '8');
+
+ var seriesText = series.select('text.nv-legend-text');
+
+ series
+ .on('mouseover', function(d,i) {
+ dispatch.legendMouseover(d,i); //TODO: Make consistent with other event objects
+ })
+ .on('mouseout', function(d,i) {
+ dispatch.legendMouseout(d,i);
+ })
+ .on('click', function(d,i) {
+ dispatch.legendClick(d,i);
+ // make sure we re-get data in case it was modified
+ var data = series.data();
+ if (updateState) {
+ if(vers =='classic') {
+ if (radioButtonMode) {
+ //Radio button mode: set every series to disabled,
+ // and enable the clicked series.
+ data.forEach(function(series) { series.disabled = true});
+ d.disabled = false;
+ }
+ else {
+ d.disabled = !d.disabled;
+ if (data.every(function(series) { return series.disabled})) {
+ //the default behavior of NVD3 legends is, if every single series
+ // is disabled, turn all series' back on.
+ data.forEach(function(series) { series.disabled = false});
+ }
+ }
+ } else if(vers == 'furious') {
+ if(expanded) {
+ d.disengaged = !d.disengaged;
+ d.userDisabled = d.userDisabled == undefined ? !!d.disabled : d.userDisabled;
+ d.disabled = d.disengaged || d.userDisabled;
+ } else if (!expanded) {
+ d.disabled = !d.disabled;
+ d.userDisabled = d.disabled;
+ var engaged = data.filter(function(d) { return !d.disengaged; });
+ if (engaged.every(function(series) { return series.userDisabled })) {
+ //the default behavior of NVD3 legends is, if every single series
+ // is disabled, turn all series' back on.
+ data.forEach(function(series) {
+ series.disabled = series.userDisabled = false;
+ });
+ }
+ }
+ }
+ dispatch.stateChange({
+ disabled: data.map(function(d) { return !!d.disabled }),
+ disengaged: data.map(function(d) { return !!d.disengaged })
+ });
+
+ }
+ })
+ .on('dblclick', function(d,i) {
+ if(vers == 'furious' && expanded) return;
+ dispatch.legendDblclick(d,i);
+ if (updateState) {
+ // make sure we re-get data in case it was modified
+ var data = series.data();
+ //the default behavior of NVD3 legends, when double clicking one,
+ // is to set all other series' to false, and make the double clicked series enabled.
+ data.forEach(function(series) {
+ series.disabled = true;
+ if(vers == 'furious') series.userDisabled = series.disabled;
+ });
+ d.disabled = false;
+ if(vers == 'furious') d.userDisabled = d.disabled;
+ dispatch.stateChange({
+ disabled: data.map(function(d) { return !!d.disabled })
+ });
+ }
+ });
+
+ series.classed('nv-disabled', function(d) { return d.userDisabled });
+ series.exit().remove();
+
+ seriesText
+ .attr('fill', setTextColor)
+ .text(getKey);
+
+ //TODO: implement fixed-width and max-width options (max-width is especially useful with the align option)
+ // NEW ALIGNING CODE, TODO: clean up
+ var legendWidth = 0;
+ if (align) {
+
+ var seriesWidths = [];
+ series.each(function(d,i) {
+ var legendText = d3.select(this).select('text');
+ var nodeTextLength;
+ try {
+ nodeTextLength = legendText.node().getComputedTextLength();
+ // If the legendText is display:none'd (nodeTextLength == 0), simulate an error so we approximate, instead
+ if(nodeTextLength <= 0) throw Error();
+ }
+ catch(e) {
+ nodeTextLength = nv.utils.calcApproxTextWidth(legendText);
+ }
+
+ seriesWidths.push(nodeTextLength + padding);
+ });
+
+ var seriesPerRow = 0;
+ var columnWidths = [];
+ legendWidth = 0;
+
+ while ( legendWidth < availableWidth && seriesPerRow < seriesWidths.length) {
+ columnWidths[seriesPerRow] = seriesWidths[seriesPerRow];
+ legendWidth += seriesWidths[seriesPerRow++];
+ }
+ if (seriesPerRow === 0) seriesPerRow = 1; //minimum of one series per row
+
+ while ( legendWidth > availableWidth && seriesPerRow > 1 ) {
+ columnWidths = [];
+ seriesPerRow--;
+
+ for (var k = 0; k < seriesWidths.length; k++) {
+ if (seriesWidths[k] > (columnWidths[k % seriesPerRow] || 0) )
+ columnWidths[k % seriesPerRow] = seriesWidths[k];
+ }
+
+ legendWidth = columnWidths.reduce(function(prev, cur, index, array) {
+ return prev + cur;
+ });
+ }
+
+ var xPositions = [];
+ for (var i = 0, curX = 0; i < seriesPerRow; i++) {
+ xPositions[i] = curX;
+ curX += columnWidths[i];
+ }
+
+ series
+ .attr('transform', function(d, i) {
+ return 'translate(' + xPositions[i % seriesPerRow] + ',' + (5 + Math.floor(i / seriesPerRow) * versPadding) + ')';
+ });
+
+ //position legend as far right as possible within the total width
+ if (rightAlign) {
+ g.attr('transform', 'translate(' + (width - margin.right - legendWidth) + ',' + margin.top + ')');
+ }
+ else {
+ g.attr('transform', 'translate(0' + ',' + margin.top + ')');
+ }
+
+ height = margin.top + margin.bottom + (Math.ceil(seriesWidths.length / seriesPerRow) * versPadding);
+
+ } else {
+
+ var ypos = 5,
+ newxpos = 5,
+ maxwidth = 0,
+ xpos;
+ series
+ .attr('transform', function(d, i) {
+ var length = d3.select(this).select('text').node().getComputedTextLength() + padding;
+ xpos = newxpos;
+
+ if (width < margin.left + margin.right + xpos + length) {
+ newxpos = xpos = 5;
+ ypos += versPadding;
+ }
+
+ newxpos += length;
+ if (newxpos > maxwidth) maxwidth = newxpos;
+
+ if(legendWidth < xpos + maxwidth) {
+ legendWidth = xpos + maxwidth;
+ }
+ return 'translate(' + xpos + ',' + ypos + ')';
+ });
+
+ //position legend as far right as possible within the total width
+ g.attr('transform', 'translate(' + (width - margin.right - maxwidth) + ',' + margin.top + ')');
+
+ height = margin.top + margin.bottom + ypos + 15;
+ }
+
+ if(vers == 'furious') {
+ // Size rectangles after text is placed
+ seriesShape
+ .attr('width', function(d,i) {
+ return seriesText[0][i].getComputedTextLength() + 27;
+ })
+ .attr('height', 18)
+ .attr('y', -9)
+ .attr('x', -15);
+
+ // The background for the expanded legend (UI)
+ gEnter.insert('rect',':first-child')
+ .attr('class', 'nv-legend-bg')
+ .attr('fill', '#eee')
+ // .attr('stroke', '#444')
+ .attr('opacity',0);
+
+ var seriesBG = g.select('.nv-legend-bg');
+
+ seriesBG
+ .transition().duration(300)
+ .attr('x', -versPadding )
+ .attr('width', legendWidth + versPadding - 12)
+ .attr('height', height + 10)
+ .attr('y', -margin.top - 10)
+ .attr('opacity', expanded ? 1 : 0);
+
+
+ }
+
+ seriesShape
+ .style('fill', setBGColor)
+ .style('fill-opacity', setBGOpacity)
+ .style('stroke', setBGColor);
+ });
+
+ function setTextColor(d,i) {
+ if(vers != 'furious') return '#000';
+ if(expanded) {
+ return d.disengaged ? '#000' : '#fff';
+ } else if (!expanded) {
+ if(!d.color) d.color = color(d,i);
+ return !!d.disabled ? d.color : '#fff';
+ }
+ }
+
+ function setBGColor(d,i) {
+ if(expanded && vers == 'furious') {
+ return d.disengaged ? '#eee' : d.color || color(d,i);
+ } else {
+ return d.color || color(d,i);
+ }
+ }
+
+
+ function setBGOpacity(d,i) {
+ if(expanded && vers == 'furious') {
+ return 1;
+ } else {
+ return !!d.disabled ? 0 : 1;
+ }
+ }
+
+ return chart;
+ }
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ chart.dispatch = dispatch;
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ key: {get: function(){return getKey;}, set: function(_){getKey=_;}},
+ align: {get: function(){return align;}, set: function(_){align=_;}},
+ rightAlign: {get: function(){return rightAlign;}, set: function(_){rightAlign=_;}},
+ padding: {get: function(){return padding;}, set: function(_){padding=_;}},
+ updateState: {get: function(){return updateState;}, set: function(_){updateState=_;}},
+ radioButtonMode: {get: function(){return radioButtonMode;}, set: function(_){radioButtonMode=_;}},
+ expanded: {get: function(){return expanded;}, set: function(_){expanded=_;}},
+ vers: {get: function(){return vers;}, set: function(_){vers=_;}},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ }}
+ });
+
+ nv.utils.initOptions(chart);
+
+ return chart;
+ };
+
+ nv.models.line = function() {
+ "use strict";
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var scatter = nv.models.scatter()
+ ;
+
+ var margin = {top: 0, right: 0, bottom: 0, left: 0}
+ , width = 960
+ , height = 500
+ , container = null
+ , strokeWidth = 1.5
+ , color = nv.utils.defaultColor() // a function that returns a color
+ , getX = function(d) { return d.x } // accessor to get the x value from a data point
+ , getY = function(d) { return d.y } // accessor to get the y value from a data point
+ , defined = function(d,i) { return !isNaN(getY(d,i)) && getY(d,i) !== null } // allows a line to be not continuous when it is not defined
+ , isArea = function(d) { return d.area } // decides if a line is an area or just a line
+ , clipEdge = false // if true, masks lines within x and y scale
+ , x //can be accessed via chart.xScale()
+ , y //can be accessed via chart.yScale()
+ , interpolate = "linear" // controls the line interpolation
+ , duration = 250
+ , dispatch = d3.dispatch('elementClick', 'elementMouseover', 'elementMouseout', 'renderEnd')
+ ;
+
+ scatter
+ .pointSize(16) // default size
+ .pointDomain([16,256]) //set to speed up calculation, needs to be unset if there is a custom size accessor
+ ;
+
+ //============================================================
+
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ var x0, y0 //used to store previous scales
+ , renderWatch = nv.utils.renderWatch(dispatch, duration)
+ ;
+
+ //============================================================
+
+
+ function chart(selection) {
+ renderWatch.reset();
+ renderWatch.models(scatter);
+ selection.each(function(data) {
+ container = d3.select(this);
+ var availableWidth = nv.utils.availableWidth(width, container, margin),
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+ nv.utils.initSVG(container);
+
+ // Setup Scales
+ x = scatter.xScale();
+ y = scatter.yScale();
+
+ x0 = x0 || x;
+ y0 = y0 || y;
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-line').data([data]);
+ var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-line');
+ var defsEnter = wrapEnter.append('defs');
+ var gEnter = wrapEnter.append('g');
+ var g = wrap.select('g');
+
+ gEnter.append('g').attr('class', 'nv-groups');
+ gEnter.append('g').attr('class', 'nv-scatterWrap');
+
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ scatter
+ .width(availableWidth)
+ .height(availableHeight);
+
+ var scatterWrap = wrap.select('.nv-scatterWrap');
+ scatterWrap.call(scatter);
+
+ defsEnter.append('clipPath')
+ .attr('id', 'nv-edge-clip-' + scatter.id())
+ .append('rect');
+
+ wrap.select('#nv-edge-clip-' + scatter.id() + ' rect')
+ .attr('width', availableWidth)
+ .attr('height', (availableHeight > 0) ? availableHeight : 0);
+
+ g .attr('clip-path', clipEdge ? 'url(#nv-edge-clip-' + scatter.id() + ')' : '');
+ scatterWrap
+ .attr('clip-path', clipEdge ? 'url(#nv-edge-clip-' + scatter.id() + ')' : '');
+
+ var groups = wrap.select('.nv-groups').selectAll('.nv-group')
+ .data(function(d) { return d }, function(d) { return d.key });
+ groups.enter().append('g')
+ .style('stroke-opacity', 1e-6)
+ .style('stroke-width', function(d) { return d.strokeWidth || strokeWidth })
+ .style('fill-opacity', 1e-6);
+
+ groups.exit().remove();
+
+ groups
+ .attr('class', function(d,i) {
+ return (d.classed || '') + ' nv-group nv-series-' + i;
+ })
+ .classed('hover', function(d) { return d.hover })
+ .style('fill', function(d,i){ return color(d, i) })
+ .style('stroke', function(d,i){ return color(d, i)});
+ groups.watchTransition(renderWatch, 'line: groups')
+ .style('stroke-opacity', 1)
+ .style('fill-opacity', function(d) { return d.fillOpacity || .5});
+
+ var areaPaths = groups.selectAll('path.nv-area')
+ .data(function(d) { return isArea(d) ? [d] : [] }); // this is done differently than lines because I need to check if series is an area
+ areaPaths.enter().append('path')
+ .attr('class', 'nv-area')
+ .attr('d', function(d) {
+ return d3.svg.area()
+ .interpolate(interpolate)
+ .defined(defined)
+ .x(function(d,i) { return nv.utils.NaNtoZero(x0(getX(d,i))) })
+ .y0(function(d,i) { return nv.utils.NaNtoZero(y0(getY(d,i))) })
+ .y1(function(d,i) { return y0( y.domain()[0] <= 0 ? y.domain()[1] >= 0 ? 0 : y.domain()[1] : y.domain()[0] ) })
+ //.y1(function(d,i) { return y0(0) }) //assuming 0 is within y domain.. may need to tweak this
+ .apply(this, [d.values])
+ });
+ groups.exit().selectAll('path.nv-area')
+ .remove();
+
+ areaPaths.watchTransition(renderWatch, 'line: areaPaths')
+ .attr('d', function(d) {
+ return d3.svg.area()
+ .interpolate(interpolate)
+ .defined(defined)
+ .x(function(d,i) { return nv.utils.NaNtoZero(x(getX(d,i))) })
+ .y0(function(d,i) { return nv.utils.NaNtoZero(y(getY(d,i))) })
+ .y1(function(d,i) { return y( y.domain()[0] <= 0 ? y.domain()[1] >= 0 ? 0 : y.domain()[1] : y.domain()[0] ) })
+ //.y1(function(d,i) { return y0(0) }) //assuming 0 is within y domain.. may need to tweak this
+ .apply(this, [d.values])
+ });
+
+ var linePaths = groups.selectAll('path.nv-line')
+ .data(function(d) { return [d.values] });
+
+ linePaths.enter().append('path')
+ .attr('class', 'nv-line')
+ .attr('d',
+ d3.svg.line()
+ .interpolate(interpolate)
+ .defined(defined)
+ .x(function(d,i) { return nv.utils.NaNtoZero(x0(getX(d,i))) })
+ .y(function(d,i) { return nv.utils.NaNtoZero(y0(getY(d,i))) })
+ );
+
+ linePaths.watchTransition(renderWatch, 'line: linePaths')
+ .attr('d',
+ d3.svg.line()
+ .interpolate(interpolate)
+ .defined(defined)
+ .x(function(d,i) { return nv.utils.NaNtoZero(x(getX(d,i))) })
+ .y(function(d,i) { return nv.utils.NaNtoZero(y(getY(d,i))) })
+ );
+
+ //store old scales for use in transitions on update
+ x0 = x.copy();
+ y0 = y.copy();
+ });
+ renderWatch.renderEnd('line immediate');
+ return chart;
+ }
+
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ chart.dispatch = dispatch;
+ chart.scatter = scatter;
+ // Pass through events
+ scatter.dispatch.on('elementClick', function(){ dispatch.elementClick.apply(this, arguments); });
+ scatter.dispatch.on('elementMouseover', function(){ dispatch.elementMouseover.apply(this, arguments); });
+ scatter.dispatch.on('elementMouseout', function(){ dispatch.elementMouseout.apply(this, arguments); });
+
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ defined: {get: function(){return defined;}, set: function(_){defined=_;}},
+ interpolate: {get: function(){return interpolate;}, set: function(_){interpolate=_;}},
+ clipEdge: {get: function(){return clipEdge;}, set: function(_){clipEdge=_;}},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ duration: {get: function(){return duration;}, set: function(_){
+ duration = _;
+ renderWatch.reset(duration);
+ scatter.duration(duration);
+ }},
+ isArea: {get: function(){return isArea;}, set: function(_){
+ isArea = d3.functor(_);
+ }},
+ x: {get: function(){return getX;}, set: function(_){
+ getX = _;
+ scatter.x(_);
+ }},
+ y: {get: function(){return getY;}, set: function(_){
+ getY = _;
+ scatter.y(_);
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ scatter.color(color);
+ }}
+ });
+
+ nv.utils.inheritOptions(chart, scatter);
+ nv.utils.initOptions(chart);
+
+ return chart;
+ };
+ nv.models.lineChart = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var lines = nv.models.line()
+ , xAxis = nv.models.axis()
+ , yAxis = nv.models.axis()
+ , legend = nv.models.legend()
+ , interactiveLayer = nv.interactiveGuideline()
+ , tooltip = nv.models.tooltip()
+ ;
+
+ var margin = {top: 30, right: 20, bottom: 50, left: 60}
+ , color = nv.utils.defaultColor()
+ , width = null
+ , height = null
+ , showLegend = true
+ , showXAxis = true
+ , showYAxis = true
+ , rightAlignYAxis = false
+ , useInteractiveGuideline = false
+ , x
+ , y
+ , state = nv.utils.state()
+ , defaultState = null
+ , noData = null
+ , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'stateChange', 'changeState', 'renderEnd')
+ , duration = 250
+ ;
+
+ // set options on sub-objects for this chart
+ xAxis.orient('bottom').tickPadding(7);
+ yAxis.orient(rightAlignYAxis ? 'right' : 'left');
+ tooltip.valueFormatter(function(d, i) {
+ return yAxis.tickFormat()(d, i);
+ }).headerFormatter(function(d, i) {
+ return xAxis.tickFormat()(d, i);
+ });
+
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ var renderWatch = nv.utils.renderWatch(dispatch, duration);
+
+ var stateGetter = function(data) {
+ return function(){
+ return {
+ active: data.map(function(d) { return !d.disabled })
+ };
+ }
+ };
+
+ var stateSetter = function(data) {
+ return function(state) {
+ if (state.active !== undefined)
+ data.forEach(function(series,i) {
+ series.disabled = !state.active[i];
+ });
+ }
+ };
+
+ function chart(selection) {
+ renderWatch.reset();
+ renderWatch.models(lines);
+ if (showXAxis) renderWatch.models(xAxis);
+ if (showYAxis) renderWatch.models(yAxis);
+
+ selection.each(function(data) {
+ var container = d3.select(this),
+ that = this;
+ nv.utils.initSVG(container);
+ var availableWidth = nv.utils.availableWidth(width, container, margin),
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+
+ chart.update = function() {
+ if (duration === 0)
+ container.call(chart);
+ else
+ container.transition().duration(duration).call(chart)
+ };
+ chart.container = this;
+
+ state
+ .setter(stateSetter(data), chart.update)
+ .getter(stateGetter(data))
+ .update();
+
+ // DEPRECATED set state.disableddisabled
+ state.disabled = data.map(function(d) { return !!d.disabled });
+
+ if (!defaultState) {
+ var key;
+ defaultState = {};
+ for (key in state) {
+ if (state[key] instanceof Array)
+ defaultState[key] = state[key].slice(0);
+ else
+ defaultState[key] = state[key];
+ }
+ }
+
+ // Display noData message if there's nothing to show.
+ if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) {
+ nv.utils.noData(chart, container)
+ return chart;
+ } else {
+ container.selectAll('.nv-noData').remove();
+ }
+
+
+ // Setup Scales
+ x = lines.xScale();
+ y = lines.yScale();
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-lineChart').data([data]);
+ var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-lineChart').append('g');
+ var g = wrap.select('g');
+
+ gEnter.append("rect").style("opacity",0);
+ gEnter.append('g').attr('class', 'nv-x nv-axis');
+ gEnter.append('g').attr('class', 'nv-y nv-axis');
+ gEnter.append('g').attr('class', 'nv-linesWrap');
+ gEnter.append('g').attr('class', 'nv-legendWrap');
+ gEnter.append('g').attr('class', 'nv-interactive');
+
+ g.select("rect")
+ .attr("width",availableWidth)
+ .attr("height",(availableHeight > 0) ? availableHeight : 0);
+
+ // Legend
+ if (showLegend) {
+ legend.width(availableWidth);
+
+ g.select('.nv-legendWrap')
+ .datum(data)
+ .call(legend);
+
+ if ( margin.top != legend.height()) {
+ margin.top = legend.height();
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+ }
+
+ wrap.select('.nv-legendWrap')
+ .attr('transform', 'translate(0,' + (-margin.top) +')')
+ }
+
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ if (rightAlignYAxis) {
+ g.select(".nv-y.nv-axis")
+ .attr("transform", "translate(" + availableWidth + ",0)");
+ }
+
+ //Set up interactive layer
+ if (useInteractiveGuideline) {
+ interactiveLayer
+ .width(availableWidth)
+ .height(availableHeight)
+ .margin({left:margin.left, top:margin.top})
+ .svgContainer(container)
+ .xScale(x);
+ wrap.select(".nv-interactive").call(interactiveLayer);
+ }
+
+ lines
+ .width(availableWidth)
+ .height(availableHeight)
+ .color(data.map(function(d,i) {
+ return d.color || color(d, i);
+ }).filter(function(d,i) { return !data[i].disabled }));
+
+
+ var linesWrap = g.select('.nv-linesWrap')
+ .datum(data.filter(function(d) { return !d.disabled }));
+
+ linesWrap.call(lines);
+
+ // Setup Axes
+ if (showXAxis) {
+ xAxis
+ .scale(x)
+ ._ticks(nv.utils.calcTicksX(availableWidth/100, data) )
+ .tickSize(-availableHeight, 0);
+
+ g.select('.nv-x.nv-axis')
+ .attr('transform', 'translate(0,' + y.range()[0] + ')');
+ g.select('.nv-x.nv-axis')
+ .call(xAxis);
+ }
+
+ if (showYAxis) {
+ yAxis
+ .scale(y)
+ ._ticks(nv.utils.calcTicksY(availableHeight/36, data) )
+ .tickSize( -availableWidth, 0);
+
+ g.select('.nv-y.nv-axis')
+ .call(yAxis);
+ }
+
+ //============================================================
+ // Event Handling/Dispatching (in chart's scope)
+ //------------------------------------------------------------
+
+ legend.dispatch.on('stateChange', function(newState) {
+ for (var key in newState)
+ state[key] = newState[key];
+ dispatch.stateChange(state);
+ chart.update();
+ });
+
+ interactiveLayer.dispatch.on('elementMousemove', function(e) {
+ lines.clearHighlights();
+ var singlePoint, pointIndex, pointXLocation, allData = [];
+ data
+ .filter(function(series, i) {
+ series.seriesIndex = i;
+ return !series.disabled;
+ })
+ .forEach(function(series,i) {
+ pointIndex = nv.interactiveBisect(series.values, e.pointXValue, chart.x());
+ var point = series.values[pointIndex];
+ var pointYValue = chart.y()(point, pointIndex);
+ if (pointYValue != null) {
+ lines.highlightPoint(i, pointIndex, true);
+ }
+ if (point === undefined) return;
+ if (singlePoint === undefined) singlePoint = point;
+ if (pointXLocation === undefined) pointXLocation = chart.xScale()(chart.x()(point,pointIndex));
+ allData.push({
+ key: series.key,
+ value: pointYValue,
+ color: color(series,series.seriesIndex)
+ });
+ });
+ //Highlight the tooltip entry based on which point the mouse is closest to.
+ if (allData.length > 2) {
+ var yValue = chart.yScale().invert(e.mouseY);
+ var domainExtent = Math.abs(chart.yScale().domain()[0] - chart.yScale().domain()[1]);
+ var threshold = 0.03 * domainExtent;
+ var indexToHighlight = nv.nearestValueIndex(allData.map(function(d){return d.value}),yValue,threshold);
+ if (indexToHighlight !== null)
+ allData[indexToHighlight].highlight = true;
+ }
+
+ var xValue = xAxis.tickFormat()(chart.x()(singlePoint,pointIndex));
+ interactiveLayer.tooltip
+ .position({left: e.mouseX + margin.left, top: e.mouseY + margin.top})
+ .chartContainer(that.parentNode)
+ .valueFormatter(function(d,i) {
+ return d == null ? "N/A" : yAxis.tickFormat()(d);
+ })
+ .data({
+ value: xValue,
+ index: pointIndex,
+ series: allData
+ })();
+
+ interactiveLayer.renderGuideLine(pointXLocation);
+
+ });
+
+ interactiveLayer.dispatch.on('elementClick', function(e) {
+ var pointXLocation, allData = [];
+
+ data.filter(function(series, i) {
+ series.seriesIndex = i;
+ return !series.disabled;
+ }).forEach(function(series) {
+ var pointIndex = nv.interactiveBisect(series.values, e.pointXValue, chart.x());
+ var point = series.values[pointIndex];
+ if (typeof point === 'undefined') return;
+ if (typeof pointXLocation === 'undefined') pointXLocation = chart.xScale()(chart.x()(point,pointIndex));
+ var yPos = chart.yScale()(chart.y()(point,pointIndex));
+ allData.push({
+ point: point,
+ pointIndex: pointIndex,
+ pos: [pointXLocation, yPos],
+ seriesIndex: series.seriesIndex,
+ series: series
+ });
+ });
+
+ lines.dispatch.elementClick(allData);
+ });
+
+ interactiveLayer.dispatch.on("elementMouseout",function(e) {
+ lines.clearHighlights();
+ });
+
+ dispatch.on('changeState', function(e) {
+ if (typeof e.disabled !== 'undefined' && data.length === e.disabled.length) {
+ data.forEach(function(series,i) {
+ series.disabled = e.disabled[i];
+ });
+
+ state.disabled = e.disabled;
+ }
+
+ chart.update();
+ });
+
+ });
+
+ renderWatch.renderEnd('lineChart immediate');
+ return chart;
+ }
+
+ //============================================================
+ // Event Handling/Dispatching (out of chart's scope)
+ //------------------------------------------------------------
+
+ lines.dispatch.on('elementMouseover.tooltip', function(evt) {
+ tooltip.data(evt).position(evt.pos).hidden(false);
+ });
+
+ lines.dispatch.on('elementMouseout.tooltip', function(evt) {
+ tooltip.hidden(true)
+ });
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ // expose chart's sub-components
+ chart.dispatch = dispatch;
+ chart.lines = lines;
+ chart.legend = legend;
+ chart.xAxis = xAxis;
+ chart.yAxis = yAxis;
+ chart.interactiveLayer = interactiveLayer;
+ chart.tooltip = tooltip;
+
+ chart.dispatch = dispatch;
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}},
+ showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}},
+ showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}},
+ defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}},
+ noData: {get: function(){return noData;}, set: function(_){noData=_;}},
+
+ // deprecated options
+ tooltips: {get: function(){return tooltip.enabled();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead');
+ tooltip.enabled(!!_);
+ }},
+ tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead');
+ tooltip.contentGenerator(_);
+ }},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ duration: {get: function(){return duration;}, set: function(_){
+ duration = _;
+ renderWatch.reset(duration);
+ lines.duration(duration);
+ xAxis.duration(duration);
+ yAxis.duration(duration);
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ legend.color(color);
+ lines.color(color);
+ }},
+ rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){
+ rightAlignYAxis = _;
+ yAxis.orient( rightAlignYAxis ? 'right' : 'left');
+ }},
+ useInteractiveGuideline: {get: function(){return useInteractiveGuideline;}, set: function(_){
+ useInteractiveGuideline = _;
+ if (useInteractiveGuideline) {
+ lines.interactive(false);
+ lines.useVoronoi(false);
+ }
+ }}
+ });
+
+ nv.utils.inheritOptions(chart, lines);
+ nv.utils.initOptions(chart);
+
+ return chart;
+ };
+ nv.models.linePlusBarChart = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var lines = nv.models.line()
+ , lines2 = nv.models.line()
+ , bars = nv.models.historicalBar()
+ , bars2 = nv.models.historicalBar()
+ , xAxis = nv.models.axis()
+ , x2Axis = nv.models.axis()
+ , y1Axis = nv.models.axis()
+ , y2Axis = nv.models.axis()
+ , y3Axis = nv.models.axis()
+ , y4Axis = nv.models.axis()
+ , legend = nv.models.legend()
+ , brush = d3.svg.brush()
+ , tooltip = nv.models.tooltip()
+ ;
+
+ var margin = {top: 30, right: 30, bottom: 30, left: 60}
+ , margin2 = {top: 0, right: 30, bottom: 20, left: 60}
+ , width = null
+ , height = null
+ , getX = function(d) { return d.x }
+ , getY = function(d) { return d.y }
+ , color = nv.utils.defaultColor()
+ , showLegend = true
+ , focusEnable = true
+ , focusShowAxisY = false
+ , focusShowAxisX = true
+ , focusHeight = 50
+ , extent
+ , brushExtent = null
+ , x
+ , x2
+ , y1
+ , y2
+ , y3
+ , y4
+ , noData = null
+ , dispatch = d3.dispatch('brush', 'stateChange', 'changeState')
+ , transitionDuration = 0
+ , state = nv.utils.state()
+ , defaultState = null
+ , legendLeftAxisHint = ' (left axis)'
+ , legendRightAxisHint = ' (right axis)'
+ ;
+
+ lines.clipEdge(true);
+ lines2.interactive(false);
+ // We don't want any points emitted for the focus chart's scatter graph.
+ lines2.pointActive(function(d) { return false });
+ xAxis.orient('bottom').tickPadding(5);
+ y1Axis.orient('left');
+ y2Axis.orient('right');
+ x2Axis.orient('bottom').tickPadding(5);
+ y3Axis.orient('left');
+ y4Axis.orient('right');
+
+ tooltip.headerEnabled(true).headerFormatter(function(d, i) {
+ return xAxis.tickFormat()(d, i);
+ });
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ var stateGetter = function(data) {
+ return function(){
+ return {
+ active: data.map(function(d) { return !d.disabled })
+ };
+ }
+ };
+
+ var stateSetter = function(data) {
+ return function(state) {
+ if (state.active !== undefined)
+ data.forEach(function(series,i) {
+ series.disabled = !state.active[i];
+ });
+ }
+ };
+
+ function chart(selection) {
+ selection.each(function(data) {
+ var container = d3.select(this),
+ that = this;
+ nv.utils.initSVG(container);
+ var availableWidth = nv.utils.availableWidth(width, container, margin),
+ availableHeight1 = nv.utils.availableHeight(height, container, margin)
+ - (focusEnable ? focusHeight : 0),
+ availableHeight2 = focusHeight - margin2.top - margin2.bottom;
+
+ chart.update = function() { container.transition().duration(transitionDuration).call(chart); };
+ chart.container = this;
+
+ state
+ .setter(stateSetter(data), chart.update)
+ .getter(stateGetter(data))
+ .update();
+
+ // DEPRECATED set state.disableddisabled
+ state.disabled = data.map(function(d) { return !!d.disabled });
+
+ if (!defaultState) {
+ var key;
+ defaultState = {};
+ for (key in state) {
+ if (state[key] instanceof Array)
+ defaultState[key] = state[key].slice(0);
+ else
+ defaultState[key] = state[key];
+ }
+ }
+
+ // Display No Data message if there's nothing to show.
+ if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) {
+ nv.utils.noData(chart, container)
+ return chart;
+ } else {
+ container.selectAll('.nv-noData').remove();
+ }
+
+ // Setup Scales
+ var dataBars = data.filter(function(d) { return !d.disabled && d.bar });
+ var dataLines = data.filter(function(d) { return !d.bar }); // removed the !d.disabled clause here to fix Issue #240
+
+ x = bars.xScale();
+ x2 = x2Axis.scale();
+ y1 = bars.yScale();
+ y2 = lines.yScale();
+ y3 = bars2.yScale();
+ y4 = lines2.yScale();
+
+ var series1 = data
+ .filter(function(d) { return !d.disabled && d.bar })
+ .map(function(d) {
+ return d.values.map(function(d,i) {
+ return { x: getX(d,i), y: getY(d,i) }
+ })
+ });
+
+ var series2 = data
+ .filter(function(d) { return !d.disabled && !d.bar })
+ .map(function(d) {
+ return d.values.map(function(d,i) {
+ return { x: getX(d,i), y: getY(d,i) }
+ })
+ });
+
+ x.range([0, availableWidth]);
+
+ x2 .domain(d3.extent(d3.merge(series1.concat(series2)), function(d) { return d.x } ))
+ .range([0, availableWidth]);
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-linePlusBar').data([data]);
+ var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-linePlusBar').append('g');
+ var g = wrap.select('g');
+
+ gEnter.append('g').attr('class', 'nv-legendWrap');
+
+ // this is the main chart
+ var focusEnter = gEnter.append('g').attr('class', 'nv-focus');
+ focusEnter.append('g').attr('class', 'nv-x nv-axis');
+ focusEnter.append('g').attr('class', 'nv-y1 nv-axis');
+ focusEnter.append('g').attr('class', 'nv-y2 nv-axis');
+ focusEnter.append('g').attr('class', 'nv-barsWrap');
+ focusEnter.append('g').attr('class', 'nv-linesWrap');
+
+ // context chart is where you can focus in
+ var contextEnter = gEnter.append('g').attr('class', 'nv-context');
+ contextEnter.append('g').attr('class', 'nv-x nv-axis');
+ contextEnter.append('g').attr('class', 'nv-y1 nv-axis');
+ contextEnter.append('g').attr('class', 'nv-y2 nv-axis');
+ contextEnter.append('g').attr('class', 'nv-barsWrap');
+ contextEnter.append('g').attr('class', 'nv-linesWrap');
+ contextEnter.append('g').attr('class', 'nv-brushBackground');
+ contextEnter.append('g').attr('class', 'nv-x nv-brush');
+
+ //============================================================
+ // Legend
+ //------------------------------------------------------------
+
+ if (showLegend) {
+ var legendWidth = legend.align() ? availableWidth / 2 : availableWidth;
+ var legendXPosition = legend.align() ? legendWidth : 0;
+
+ legend.width(legendWidth);
+
+ g.select('.nv-legendWrap')
+ .datum(data.map(function(series) {
+ series.originalKey = series.originalKey === undefined ? series.key : series.originalKey;
+ series.key = series.originalKey + (series.bar ? legendLeftAxisHint : legendRightAxisHint);
+ return series;
+ }))
+ .call(legend);
+
+ if ( margin.top != legend.height()) {
+ margin.top = legend.height();
+ // FIXME: shouldn't this be "- (focusEnabled ? focusHeight : 0)"?
+ availableHeight1 = nv.utils.availableHeight(height, container, margin) - focusHeight;
+ }
+
+ g.select('.nv-legendWrap')
+ .attr('transform', 'translate(' + legendXPosition + ',' + (-margin.top) +')');
+ }
+
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ //============================================================
+ // Context chart (focus chart) components
+ //------------------------------------------------------------
+
+ // hide or show the focus context chart
+ g.select('.nv-context').style('display', focusEnable ? 'initial' : 'none');
+
+ bars2
+ .width(availableWidth)
+ .height(availableHeight2)
+ .color(data.map(function (d, i) {
+ return d.color || color(d, i);
+ }).filter(function (d, i) {
+ return !data[i].disabled && data[i].bar
+ }));
+ lines2
+ .width(availableWidth)
+ .height(availableHeight2)
+ .color(data.map(function (d, i) {
+ return d.color || color(d, i);
+ }).filter(function (d, i) {
+ return !data[i].disabled && !data[i].bar
+ }));
+
+ var bars2Wrap = g.select('.nv-context .nv-barsWrap')
+ .datum(dataBars.length ? dataBars : [
+ {values: []}
+ ]);
+ var lines2Wrap = g.select('.nv-context .nv-linesWrap')
+ .datum(!dataLines[0].disabled ? dataLines : [
+ {values: []}
+ ]);
+
+ g.select('.nv-context')
+ .attr('transform', 'translate(0,' + ( availableHeight1 + margin.bottom + margin2.top) + ')');
+
+ bars2Wrap.transition().call(bars2);
+ lines2Wrap.transition().call(lines2);
+
+ // context (focus chart) axis controls
+ if (focusShowAxisX) {
+ x2Axis
+ ._ticks( nv.utils.calcTicksX(availableWidth / 100, data))
+ .tickSize(-availableHeight2, 0);
+ g.select('.nv-context .nv-x.nv-axis')
+ .attr('transform', 'translate(0,' + y3.range()[0] + ')');
+ g.select('.nv-context .nv-x.nv-axis').transition()
+ .call(x2Axis);
+ }
+
+ if (focusShowAxisY) {
+ y3Axis
+ .scale(y3)
+ ._ticks( availableHeight2 / 36 )
+ .tickSize( -availableWidth, 0);
+ y4Axis
+ .scale(y4)
+ ._ticks( availableHeight2 / 36 )
+ .tickSize(dataBars.length ? 0 : -availableWidth, 0); // Show the y2 rules only if y1 has none
+
+ g.select('.nv-context .nv-y3.nv-axis')
+ .style('opacity', dataBars.length ? 1 : 0)
+ .attr('transform', 'translate(0,' + x2.range()[0] + ')');
+ g.select('.nv-context .nv-y2.nv-axis')
+ .style('opacity', dataLines.length ? 1 : 0)
+ .attr('transform', 'translate(' + x2.range()[1] + ',0)');
+
+ g.select('.nv-context .nv-y1.nv-axis').transition()
+ .call(y3Axis);
+ g.select('.nv-context .nv-y2.nv-axis').transition()
+ .call(y4Axis);
+ }
+
+ // Setup Brush
+ brush.x(x2).on('brush', onBrush);
+
+ if (brushExtent) brush.extent(brushExtent);
+
+ var brushBG = g.select('.nv-brushBackground').selectAll('g')
+ .data([brushExtent || brush.extent()]);
+
+ var brushBGenter = brushBG.enter()
+ .append('g');
+
+ brushBGenter.append('rect')
+ .attr('class', 'left')
+ .attr('x', 0)
+ .attr('y', 0)
+ .attr('height', availableHeight2);
+
+ brushBGenter.append('rect')
+ .attr('class', 'right')
+ .attr('x', 0)
+ .attr('y', 0)
+ .attr('height', availableHeight2);
+
+ var gBrush = g.select('.nv-x.nv-brush')
+ .call(brush);
+ gBrush.selectAll('rect')
+ //.attr('y', -5)
+ .attr('height', availableHeight2);
+ gBrush.selectAll('.resize').append('path').attr('d', resizePath);
+
+ //============================================================
+ // Event Handling/Dispatching (in chart's scope)
+ //------------------------------------------------------------
+
+ legend.dispatch.on('stateChange', function(newState) {
+ for (var key in newState)
+ state[key] = newState[key];
+ dispatch.stateChange(state);
+ chart.update();
+ });
+
+ // Update chart from a state object passed to event handler
+ dispatch.on('changeState', function(e) {
+ if (typeof e.disabled !== 'undefined') {
+ data.forEach(function(series,i) {
+ series.disabled = e.disabled[i];
+ });
+ state.disabled = e.disabled;
+ }
+ chart.update();
+ });
+
+ //============================================================
+ // Functions
+ //------------------------------------------------------------
+
+ // Taken from crossfilter (http://square.github.com/crossfilter/)
+ function resizePath(d) {
+ var e = +(d == 'e'),
+ x = e ? 1 : -1,
+ y = availableHeight2 / 3;
+ return 'M' + (.5 * x) + ',' + y
+ + 'A6,6 0 0 ' + e + ' ' + (6.5 * x) + ',' + (y + 6)
+ + 'V' + (2 * y - 6)
+ + 'A6,6 0 0 ' + e + ' ' + (.5 * x) + ',' + (2 * y)
+ + 'Z'
+ + 'M' + (2.5 * x) + ',' + (y + 8)
+ + 'V' + (2 * y - 8)
+ + 'M' + (4.5 * x) + ',' + (y + 8)
+ + 'V' + (2 * y - 8);
+ }
+
+
+ function updateBrushBG() {
+ if (!brush.empty()) brush.extent(brushExtent);
+ brushBG
+ .data([brush.empty() ? x2.domain() : brushExtent])
+ .each(function(d,i) {
+ var leftWidth = x2(d[0]) - x2.range()[0],
+ rightWidth = x2.range()[1] - x2(d[1]);
+ d3.select(this).select('.left')
+ .attr('width', leftWidth < 0 ? 0 : leftWidth);
+
+ d3.select(this).select('.right')
+ .attr('x', x2(d[1]))
+ .attr('width', rightWidth < 0 ? 0 : rightWidth);
+ });
+ }
+
+ function onBrush() {
+ brushExtent = brush.empty() ? null : brush.extent();
+ extent = brush.empty() ? x2.domain() : brush.extent();
+ dispatch.brush({extent: extent, brush: brush});
+ updateBrushBG();
+
+ // Prepare Main (Focus) Bars and Lines
+ bars
+ .width(availableWidth)
+ .height(availableHeight1)
+ .color(data.map(function(d,i) {
+ return d.color || color(d, i);
+ }).filter(function(d,i) { return !data[i].disabled && data[i].bar }));
+
+ lines
+ .width(availableWidth)
+ .height(availableHeight1)
+ .color(data.map(function(d,i) {
+ return d.color || color(d, i);
+ }).filter(function(d,i) { return !data[i].disabled && !data[i].bar }));
+
+ var focusBarsWrap = g.select('.nv-focus .nv-barsWrap')
+ .datum(!dataBars.length ? [{values:[]}] :
+ dataBars
+ .map(function(d,i) {
+ return {
+ key: d.key,
+ values: d.values.filter(function(d,i) {
+ return bars.x()(d,i) >= extent[0] && bars.x()(d,i) <= extent[1];
+ })
+ }
+ })
+ );
+
+ var focusLinesWrap = g.select('.nv-focus .nv-linesWrap')
+ .datum(dataLines[0].disabled ? [{values:[]}] :
+ dataLines
+ .map(function(d,i) {
+ return {
+ area: d.area,
+ fillOpacity: d.fillOpacity,
+ key: d.key,
+ values: d.values.filter(function(d,i) {
+ return lines.x()(d,i) >= extent[0] && lines.x()(d,i) <= extent[1];
+ })
+ }
+ })
+ );
+
+ // Update Main (Focus) X Axis
+ if (dataBars.length) {
+ x = bars.xScale();
+ } else {
+ x = lines.xScale();
+ }
+
+ xAxis
+ .scale(x)
+ ._ticks( nv.utils.calcTicksX(availableWidth/100, data) )
+ .tickSize(-availableHeight1, 0);
+
+ xAxis.domain([Math.ceil(extent[0]), Math.floor(extent[1])]);
+
+ g.select('.nv-x.nv-axis').transition().duration(transitionDuration)
+ .call(xAxis);
+
+ // Update Main (Focus) Bars and Lines
+ focusBarsWrap.transition().duration(transitionDuration).call(bars);
+ focusLinesWrap.transition().duration(transitionDuration).call(lines);
+
+ // Setup and Update Main (Focus) Y Axes
+ g.select('.nv-focus .nv-x.nv-axis')
+ .attr('transform', 'translate(0,' + y1.range()[0] + ')');
+
+ y1Axis
+ .scale(y1)
+ ._ticks( nv.utils.calcTicksY(availableHeight1/36, data) )
+ .tickSize(-availableWidth, 0);
+ y2Axis
+ .scale(y2)
+ ._ticks( nv.utils.calcTicksY(availableHeight1/36, data) )
+ .tickSize(dataBars.length ? 0 : -availableWidth, 0); // Show the y2 rules only if y1 has none
+
+ g.select('.nv-focus .nv-y1.nv-axis')
+ .style('opacity', dataBars.length ? 1 : 0);
+ g.select('.nv-focus .nv-y2.nv-axis')
+ .style('opacity', dataLines.length && !dataLines[0].disabled ? 1 : 0)
+ .attr('transform', 'translate(' + x.range()[1] + ',0)');
+
+ g.select('.nv-focus .nv-y1.nv-axis').transition().duration(transitionDuration)
+ .call(y1Axis);
+ g.select('.nv-focus .nv-y2.nv-axis').transition().duration(transitionDuration)
+ .call(y2Axis);
+ }
+
+ onBrush();
+
+ });
+
+ return chart;
+ }
+
+ //============================================================
+ // Event Handling/Dispatching (out of chart's scope)
+ //------------------------------------------------------------
+
+ lines.dispatch.on('elementMouseover.tooltip', function(evt) {
+ tooltip
+ .duration(100)
+ .valueFormatter(function(d, i) {
+ return y2Axis.tickFormat()(d, i);
+ })
+ .data(evt)
+ .position(evt.pos)
+ .hidden(false);
+ });
+
+ lines.dispatch.on('elementMouseout.tooltip', function(evt) {
+ tooltip.hidden(true)
+ });
+
+ bars.dispatch.on('elementMouseover.tooltip', function(evt) {
+ evt.value = chart.x()(evt.data);
+ evt['series'] = {
+ value: chart.y()(evt.data),
+ color: evt.color
+ };
+ tooltip
+ .duration(0)
+ .valueFormatter(function(d, i) {
+ return y1Axis.tickFormat()(d, i);
+ })
+ .data(evt)
+ .hidden(false);
+ });
+
+ bars.dispatch.on('elementMouseout.tooltip', function(evt) {
+ tooltip.hidden(true);
+ });
+
+ bars.dispatch.on('elementMousemove.tooltip', function(evt) {
+ tooltip.position({top: d3.event.pageY, left: d3.event.pageX})();
+ });
+
+ //============================================================
+
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ // expose chart's sub-components
+ chart.dispatch = dispatch;
+ chart.legend = legend;
+ chart.lines = lines;
+ chart.lines2 = lines2;
+ chart.bars = bars;
+ chart.bars2 = bars2;
+ chart.xAxis = xAxis;
+ chart.x2Axis = x2Axis;
+ chart.y1Axis = y1Axis;
+ chart.y2Axis = y2Axis;
+ chart.y3Axis = y3Axis;
+ chart.y4Axis = y4Axis;
+ chart.tooltip = tooltip;
+
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}},
+ brushExtent: {get: function(){return brushExtent;}, set: function(_){brushExtent=_;}},
+ noData: {get: function(){return noData;}, set: function(_){noData=_;}},
+ focusEnable: {get: function(){return focusEnable;}, set: function(_){focusEnable=_;}},
+ focusHeight: {get: function(){return focusHeight;}, set: function(_){focusHeight=_;}},
+ focusShowAxisX: {get: function(){return focusShowAxisX;}, set: function(_){focusShowAxisX=_;}},
+ focusShowAxisY: {get: function(){return focusShowAxisY;}, set: function(_){focusShowAxisY=_;}},
+ legendLeftAxisHint: {get: function(){return legendLeftAxisHint;}, set: function(_){legendLeftAxisHint=_;}},
+ legendRightAxisHint: {get: function(){return legendRightAxisHint;}, set: function(_){legendRightAxisHint=_;}},
+
+ // deprecated options
+ tooltips: {get: function(){return tooltip.enabled();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead');
+ tooltip.enabled(!!_);
+ }},
+ tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead');
+ tooltip.contentGenerator(_);
+ }},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ focusMargin: {get: function(){return margin2;}, set: function(_){
+ margin2.top = _.top !== undefined ? _.top : margin2.top;
+ margin2.right = _.right !== undefined ? _.right : margin2.right;
+ margin2.bottom = _.bottom !== undefined ? _.bottom : margin2.bottom;
+ margin2.left = _.left !== undefined ? _.left : margin2.left;
+ }},
+ duration: {get: function(){return transitionDuration;}, set: function(_){
+ transitionDuration = _;
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ legend.color(color);
+ }},
+ x: {get: function(){return getX;}, set: function(_){
+ getX = _;
+ lines.x(_);
+ lines2.x(_);
+ bars.x(_);
+ bars2.x(_);
+ }},
+ y: {get: function(){return getY;}, set: function(_){
+ getY = _;
+ lines.y(_);
+ lines2.y(_);
+ bars.y(_);
+ bars2.y(_);
+ }}
+ });
+
+ nv.utils.inheritOptions(chart, lines);
+ nv.utils.initOptions(chart);
+
+ return chart;
+ };
+ nv.models.lineWithFocusChart = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var lines = nv.models.line()
+ , lines2 = nv.models.line()
+ , xAxis = nv.models.axis()
+ , yAxis = nv.models.axis()
+ , x2Axis = nv.models.axis()
+ , y2Axis = nv.models.axis()
+ , legend = nv.models.legend()
+ , brush = d3.svg.brush()
+ , tooltip = nv.models.tooltip()
+ , interactiveLayer = nv.interactiveGuideline()
+ ;
+
+ var margin = {top: 30, right: 30, bottom: 30, left: 60}
+ , margin2 = {top: 0, right: 30, bottom: 20, left: 60}
+ , color = nv.utils.defaultColor()
+ , width = null
+ , height = null
+ , height2 = 50
+ , useInteractiveGuideline = false
+ , x
+ , y
+ , x2
+ , y2
+ , showLegend = true
+ , brushExtent = null
+ , noData = null
+ , dispatch = d3.dispatch('brush', 'stateChange', 'changeState')
+ , transitionDuration = 250
+ , state = nv.utils.state()
+ , defaultState = null
+ ;
+
+ lines.clipEdge(true).duration(0);
+ lines2.interactive(false);
+ // We don't want any points emitted for the focus chart's scatter graph.
+ lines2.pointActive(function(d) { return false });
+ xAxis.orient('bottom').tickPadding(5);
+ yAxis.orient('left');
+ x2Axis.orient('bottom').tickPadding(5);
+ y2Axis.orient('left');
+
+ tooltip.valueFormatter(function(d, i) {
+ return yAxis.tickFormat()(d, i);
+ }).headerFormatter(function(d, i) {
+ return xAxis.tickFormat()(d, i);
+ });
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ var stateGetter = function(data) {
+ return function(){
+ return {
+ active: data.map(function(d) { return !d.disabled })
+ };
+ }
+ };
+
+ var stateSetter = function(data) {
+ return function(state) {
+ if (state.active !== undefined)
+ data.forEach(function(series,i) {
+ series.disabled = !state.active[i];
+ });
+ }
+ };
+
+ function chart(selection) {
+ selection.each(function(data) {
+ var container = d3.select(this),
+ that = this;
+ nv.utils.initSVG(container);
+ var availableWidth = nv.utils.availableWidth(width, container, margin),
+ availableHeight1 = nv.utils.availableHeight(height, container, margin) - height2,
+ availableHeight2 = height2 - margin2.top - margin2.bottom;
+
+ chart.update = function() { container.transition().duration(transitionDuration).call(chart) };
+ chart.container = this;
+
+ state
+ .setter(stateSetter(data), chart.update)
+ .getter(stateGetter(data))
+ .update();
+
+ // DEPRECATED set state.disableddisabled
+ state.disabled = data.map(function(d) { return !!d.disabled });
+
+ if (!defaultState) {
+ var key;
+ defaultState = {};
+ for (key in state) {
+ if (state[key] instanceof Array)
+ defaultState[key] = state[key].slice(0);
+ else
+ defaultState[key] = state[key];
+ }
+ }
+
+ // Display No Data message if there's nothing to show.
+ if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) {
+ nv.utils.noData(chart, container)
+ return chart;
+ } else {
+ container.selectAll('.nv-noData').remove();
+ }
+
+ // Setup Scales
+ x = lines.xScale();
+ y = lines.yScale();
+ x2 = lines2.xScale();
+ y2 = lines2.yScale();
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-lineWithFocusChart').data([data]);
+ var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-lineWithFocusChart').append('g');
+ var g = wrap.select('g');
+
+ gEnter.append('g').attr('class', 'nv-legendWrap');
+
+ var focusEnter = gEnter.append('g').attr('class', 'nv-focus');
+ focusEnter.append('g').attr('class', 'nv-x nv-axis');
+ focusEnter.append('g').attr('class', 'nv-y nv-axis');
+ focusEnter.append('g').attr('class', 'nv-linesWrap');
+ focusEnter.append('g').attr('class', 'nv-interactive');
+
+ var contextEnter = gEnter.append('g').attr('class', 'nv-context');
+ contextEnter.append('g').attr('class', 'nv-x nv-axis');
+ contextEnter.append('g').attr('class', 'nv-y nv-axis');
+ contextEnter.append('g').attr('class', 'nv-linesWrap');
+ contextEnter.append('g').attr('class', 'nv-brushBackground');
+ contextEnter.append('g').attr('class', 'nv-x nv-brush');
+
+ // Legend
+ if (showLegend) {
+ legend.width(availableWidth);
+
+ g.select('.nv-legendWrap')
+ .datum(data)
+ .call(legend);
+
+ if ( margin.top != legend.height()) {
+ margin.top = legend.height();
+ availableHeight1 = nv.utils.availableHeight(height, container, margin) - height2;
+ }
+
+ g.select('.nv-legendWrap')
+ .attr('transform', 'translate(0,' + (-margin.top) +')')
+ }
+
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+
+ //Set up interactive layer
+ if (useInteractiveGuideline) {
+ interactiveLayer
+ .width(availableWidth)
+ .height(availableHeight1)
+ .margin({left:margin.left, top:margin.top})
+ .svgContainer(container)
+ .xScale(x);
+ wrap.select(".nv-interactive").call(interactiveLayer);
+ }
+
+ // Main Chart Component(s)
+ lines
+ .width(availableWidth)
+ .height(availableHeight1)
+ .color(
+ data
+ .map(function(d,i) {
+ return d.color || color(d, i);
+ })
+ .filter(function(d,i) {
+ return !data[i].disabled;
+ })
+ );
+
+ lines2
+ .defined(lines.defined())
+ .width(availableWidth)
+ .height(availableHeight2)
+ .color(
+ data
+ .map(function(d,i) {
+ return d.color || color(d, i);
+ })
+ .filter(function(d,i) {
+ return !data[i].disabled;
+ })
+ );
+
+ g.select('.nv-context')
+ .attr('transform', 'translate(0,' + ( availableHeight1 + margin.bottom + margin2.top) + ')')
+
+ var contextLinesWrap = g.select('.nv-context .nv-linesWrap')
+ .datum(data.filter(function(d) { return !d.disabled }))
+
+ d3.transition(contextLinesWrap).call(lines2);
+
+ // Setup Main (Focus) Axes
+ xAxis
+ .scale(x)
+ ._ticks( nv.utils.calcTicksX(availableWidth/100, data) )
+ .tickSize(-availableHeight1, 0);
+
+ yAxis
+ .scale(y)
+ ._ticks( nv.utils.calcTicksY(availableHeight1/36, data) )
+ .tickSize( -availableWidth, 0);
+
+ g.select('.nv-focus .nv-x.nv-axis')
+ .attr('transform', 'translate(0,' + availableHeight1 + ')');
+
+ // Setup Brush
+ brush
+ .x(x2)
+ .on('brush', function() {
+ onBrush();
+ });
+
+ if (brushExtent) brush.extent(brushExtent);
+
+ var brushBG = g.select('.nv-brushBackground').selectAll('g')
+ .data([brushExtent || brush.extent()])
+
+ var brushBGenter = brushBG.enter()
+ .append('g');
+
+ brushBGenter.append('rect')
+ .attr('class', 'left')
+ .attr('x', 0)
+ .attr('y', 0)
+ .attr('height', availableHeight2);
+
+ brushBGenter.append('rect')
+ .attr('class', 'right')
+ .attr('x', 0)
+ .attr('y', 0)
+ .attr('height', availableHeight2);
+
+ var gBrush = g.select('.nv-x.nv-brush')
+ .call(brush);
+ gBrush.selectAll('rect')
+ .attr('height', availableHeight2);
+ gBrush.selectAll('.resize').append('path').attr('d', resizePath);
+
+ onBrush();
+
+ // Setup Secondary (Context) Axes
+ x2Axis
+ .scale(x2)
+ ._ticks( nv.utils.calcTicksX(availableWidth/100, data) )
+ .tickSize(-availableHeight2, 0);
+
+ g.select('.nv-context .nv-x.nv-axis')
+ .attr('transform', 'translate(0,' + y2.range()[0] + ')');
+ d3.transition(g.select('.nv-context .nv-x.nv-axis'))
+ .call(x2Axis);
+
+ y2Axis
+ .scale(y2)
+ ._ticks( nv.utils.calcTicksY(availableHeight2/36, data) )
+ .tickSize( -availableWidth, 0);
+
+ d3.transition(g.select('.nv-context .nv-y.nv-axis'))
+ .call(y2Axis);
+
+ g.select('.nv-context .nv-x.nv-axis')
+ .attr('transform', 'translate(0,' + y2.range()[0] + ')');
+
+ //============================================================
+ // Event Handling/Dispatching (in chart's scope)
+ //------------------------------------------------------------
+
+ legend.dispatch.on('stateChange', function(newState) {
+ for (var key in newState)
+ state[key] = newState[key];
+ dispatch.stateChange(state);
+ chart.update();
+ });
+
+ interactiveLayer.dispatch.on('elementMousemove', function(e) {
+ lines.clearHighlights();
+ var singlePoint, pointIndex, pointXLocation, allData = [];
+ data
+ .filter(function(series, i) {
+ series.seriesIndex = i;
+ return !series.disabled;
+ })
+ .forEach(function(series,i) {
+ var extent = brush.empty() ? x2.domain() : brush.extent();
+ var currentValues = series.values.filter(function(d,i) {
+ return lines.x()(d,i) >= extent[0] && lines.x()(d,i) <= extent[1];
+ });
+
+ pointIndex = nv.interactiveBisect(currentValues, e.pointXValue, lines.x());
+ var point = currentValues[pointIndex];
+ var pointYValue = chart.y()(point, pointIndex);
+ if (pointYValue != null) {
+ lines.highlightPoint(i, pointIndex, true);
+ }
+ if (point === undefined) return;
+ if (singlePoint === undefined) singlePoint = point;
+ if (pointXLocation === undefined) pointXLocation = chart.xScale()(chart.x()(point,pointIndex));
+ allData.push({
+ key: series.key,
+ value: chart.y()(point, pointIndex),
+ color: color(series,series.seriesIndex)
+ });
+ });
+ //Highlight the tooltip entry based on which point the mouse is closest to.
+ if (allData.length > 2) {
+ var yValue = chart.yScale().invert(e.mouseY);
+ var domainExtent = Math.abs(chart.yScale().domain()[0] - chart.yScale().domain()[1]);
+ var threshold = 0.03 * domainExtent;
+ var indexToHighlight = nv.nearestValueIndex(allData.map(function(d){return d.value}),yValue,threshold);
+ if (indexToHighlight !== null)
+ allData[indexToHighlight].highlight = true;
+ }
+
+ var xValue = xAxis.tickFormat()(chart.x()(singlePoint,pointIndex));
+ interactiveLayer.tooltip
+ .position({left: e.mouseX + margin.left, top: e.mouseY + margin.top})
+ .chartContainer(that.parentNode)
+ .valueFormatter(function(d,i) {
+ return d == null ? "N/A" : yAxis.tickFormat()(d);
+ })
+ .data({
+ value: xValue,
+ index: pointIndex,
+ series: allData
+ })();
+
+ interactiveLayer.renderGuideLine(pointXLocation);
+
+ });
+
+ interactiveLayer.dispatch.on("elementMouseout",function(e) {
+ lines.clearHighlights();
+ });
+
+ dispatch.on('changeState', function(e) {
+ if (typeof e.disabled !== 'undefined') {
+ data.forEach(function(series,i) {
+ series.disabled = e.disabled[i];
+ });
+ }
+ chart.update();
+ });
+
+ //============================================================
+ // Functions
+ //------------------------------------------------------------
+
+ // Taken from crossfilter (http://square.github.com/crossfilter/)
+ function resizePath(d) {
+ var e = +(d == 'e'),
+ x = e ? 1 : -1,
+ y = availableHeight2 / 3;
+ return 'M' + (.5 * x) + ',' + y
+ + 'A6,6 0 0 ' + e + ' ' + (6.5 * x) + ',' + (y + 6)
+ + 'V' + (2 * y - 6)
+ + 'A6,6 0 0 ' + e + ' ' + (.5 * x) + ',' + (2 * y)
+ + 'Z'
+ + 'M' + (2.5 * x) + ',' + (y + 8)
+ + 'V' + (2 * y - 8)
+ + 'M' + (4.5 * x) + ',' + (y + 8)
+ + 'V' + (2 * y - 8);
+ }
+
+
+ function updateBrushBG() {
+ if (!brush.empty()) brush.extent(brushExtent);
+ brushBG
+ .data([brush.empty() ? x2.domain() : brushExtent])
+ .each(function(d,i) {
+ var leftWidth = x2(d[0]) - x.range()[0],
+ rightWidth = availableWidth - x2(d[1]);
+ d3.select(this).select('.left')
+ .attr('width', leftWidth < 0 ? 0 : leftWidth);
+
+ d3.select(this).select('.right')
+ .attr('x', x2(d[1]))
+ .attr('width', rightWidth < 0 ? 0 : rightWidth);
+ });
+ }
+
+
+ function onBrush() {
+ brushExtent = brush.empty() ? null : brush.extent();
+ var extent = brush.empty() ? x2.domain() : brush.extent();
+
+ //The brush extent cannot be less than one. If it is, don't update the line chart.
+ if (Math.abs(extent[0] - extent[1]) <= 1) {
+ return;
+ }
+
+ dispatch.brush({extent: extent, brush: brush});
+
+
+ updateBrushBG();
+
+ // Update Main (Focus)
+ var focusLinesWrap = g.select('.nv-focus .nv-linesWrap')
+ .datum(
+ data
+ .filter(function(d) { return !d.disabled })
+ .map(function(d,i) {
+ return {
+ key: d.key,
+ area: d.area,
+ values: d.values.filter(function(d,i) {
+ return lines.x()(d,i) >= extent[0] && lines.x()(d,i) <= extent[1];
+ })
+ }
+ })
+ );
+ focusLinesWrap.transition().duration(transitionDuration).call(lines);
+
+
+ // Update Main (Focus) Axes
+ g.select('.nv-focus .nv-x.nv-axis').transition().duration(transitionDuration)
+ .call(xAxis);
+ g.select('.nv-focus .nv-y.nv-axis').transition().duration(transitionDuration)
+ .call(yAxis);
+ }
+ });
+
+ return chart;
+ }
+
+ //============================================================
+ // Event Handling/Dispatching (out of chart's scope)
+ //------------------------------------------------------------
+
+ lines.dispatch.on('elementMouseover.tooltip', function(evt) {
+ tooltip.data(evt).position(evt.pos).hidden(false);
+ });
+
+ lines.dispatch.on('elementMouseout.tooltip', function(evt) {
+ tooltip.hidden(true)
+ });
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ // expose chart's sub-components
+ chart.dispatch = dispatch;
+ chart.legend = legend;
+ chart.lines = lines;
+ chart.lines2 = lines2;
+ chart.xAxis = xAxis;
+ chart.yAxis = yAxis;
+ chart.x2Axis = x2Axis;
+ chart.y2Axis = y2Axis;
+ chart.interactiveLayer = interactiveLayer;
+ chart.tooltip = tooltip;
+
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ focusHeight: {get: function(){return height2;}, set: function(_){height2=_;}},
+ showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}},
+ brushExtent: {get: function(){return brushExtent;}, set: function(_){brushExtent=_;}},
+ defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}},
+ noData: {get: function(){return noData;}, set: function(_){noData=_;}},
+
+ // deprecated options
+ tooltips: {get: function(){return tooltip.enabled();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead');
+ tooltip.enabled(!!_);
+ }},
+ tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead');
+ tooltip.contentGenerator(_);
+ }},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ focusMargin: {get: function(){return margin2;}, set: function(_){
+ margin2.top = _.top !== undefined ? _.top : margin2.top;
+ margin2.right = _.right !== undefined ? _.right : margin2.right;
+ margin2.bottom = _.bottom !== undefined ? _.bottom : margin2.bottom;
+ margin2.left = _.left !== undefined ? _.left : margin2.left;
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ legend.color(color);
+ // line color is handled above?
+ }},
+ interpolate: {get: function(){return lines.interpolate();}, set: function(_){
+ lines.interpolate(_);
+ lines2.interpolate(_);
+ }},
+ xTickFormat: {get: function(){return xAxis.tickFormat();}, set: function(_){
+ xAxis.tickFormat(_);
+ x2Axis.tickFormat(_);
+ }},
+ yTickFormat: {get: function(){return yAxis.tickFormat();}, set: function(_){
+ yAxis.tickFormat(_);
+ y2Axis.tickFormat(_);
+ }},
+ duration: {get: function(){return transitionDuration;}, set: function(_){
+ transitionDuration=_;
+ yAxis.duration(transitionDuration);
+ y2Axis.duration(transitionDuration);
+ xAxis.duration(transitionDuration);
+ x2Axis.duration(transitionDuration);
+ }},
+ x: {get: function(){return lines.x();}, set: function(_){
+ lines.x(_);
+ lines2.x(_);
+ }},
+ y: {get: function(){return lines.y();}, set: function(_){
+ lines.y(_);
+ lines2.y(_);
+ }},
+ useInteractiveGuideline: {get: function(){return useInteractiveGuideline;}, set: function(_){
+ useInteractiveGuideline = _;
+ if (useInteractiveGuideline) {
+ lines.interactive(false);
+ lines.useVoronoi(false);
+ }
+ }}
+ });
+
+ nv.utils.inheritOptions(chart, lines);
+ nv.utils.initOptions(chart);
+
+ return chart;
+ };
+ nv.models.multiBar = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var margin = {top: 0, right: 0, bottom: 0, left: 0}
+ , width = 960
+ , height = 500
+ , x = d3.scale.ordinal()
+ , y = d3.scale.linear()
+ , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one
+ , container = null
+ , getX = function(d) { return d.x }
+ , getY = function(d) { return d.y }
+ , getYerr = function(d) { return d.yErr }
+ , forceY = [0] // 0 is forced by default.. this makes sense for the majority of bar graphs... user can always do chart.forceY([]) to remove
+ , clipEdge = true
+ , stacked = false
+ , stackOffset = 'zero' // options include 'silhouette', 'wiggle', 'expand', 'zero', or a custom function
+ , color = nv.utils.defaultColor()
+ , errorBarColor = nv.utils.defaultColor()
+ , hideable = false
+ , barColor = null // adding the ability to set the color for each rather than the whole group
+ , disabled // used in conjunction with barColor to communicate from multiBarHorizontalChart what series are disabled
+ , duration = 500
+ , xDomain
+ , yDomain
+ , xRange
+ , yRange
+ , groupSpacing = 0.1
+ , dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd')
+ ;
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ var x0, y0 //used to store previous scales
+ , renderWatch = nv.utils.renderWatch(dispatch, duration)
+ ;
+
+ var last_datalength = 0;
+
+ function chart(selection) {
+ renderWatch.reset();
+ selection.each(function(data) {
+ var availableWidth = width - margin.left - margin.right,
+ availableHeight = height - margin.top - margin.bottom;
+
+ container = d3.select(this);
+ nv.utils.initSVG(container);
+ var nonStackableCount = 0;
+ // This function defines the requirements for render complete
+ var endFn = function(d, i) {
+ if (d.series === data.length - 1 && i === data[0].values.length - 1)
+ return true;
+ return false;
+ };
+
+ if(hideable && data.length) hideable = [{
+ values: data[0].values.map(function(d) {
+ return {
+ x: d.x,
+ y: 0,
+ series: d.series,
+ size: 0.01
+ };}
+ )}];
+
+ if (stacked) {
+ var parsed = d3.layout.stack()
+ .offset(stackOffset)
+ .values(function(d){ return d.values })
+ .y(getY)
+ (!data.length && hideable ? hideable : data);
+
+ parsed.forEach(function(series, i){
+ // if series is non-stackable, use un-parsed data
+ if (series.nonStackable) {
+ data[i].nonStackableSeries = nonStackableCount++;
+ parsed[i] = data[i];
+ } else {
+ // don't stack this seires on top of the nonStackable seriees
+ if (i > 0 && parsed[i - 1].nonStackable){
+ parsed[i].values.map(function(d,j){
+ d.y0 -= parsed[i - 1].values[j].y;
+ d.y1 = d.y0 + d.y;
+ });
+ }
+ }
+ });
+ data = parsed;
+ }
+ //add series index and key to each data point for reference
+ data.forEach(function(series, i) {
+ series.values.forEach(function(point) {
+ point.series = i;
+ point.key = series.key;
+ });
+ });
+
+ // HACK for negative value stacking
+ if (stacked) {
+ data[0].values.map(function(d,i) {
+ var posBase = 0, negBase = 0;
+ data.map(function(d, idx) {
+ if (!data[idx].nonStackable) {
+ var f = d.values[i]
+ f.size = Math.abs(f.y);
+ if (f.y<0) {
+ f.y1 = negBase;
+ negBase = negBase - f.size;
+ } else
+ {
+ f.y1 = f.size + posBase;
+ posBase = posBase + f.size;
+ }
+ }
+
+ });
+ });
+ }
+ // Setup Scales
+ // remap and flatten the data for use in calculating the scales' domains
+ var seriesData = (xDomain && yDomain) ? [] : // if we know xDomain and yDomain, no need to calculate
+ data.map(function(d, idx) {
+ return d.values.map(function(d,i) {
+ return { x: getX(d,i), y: getY(d,i), y0: d.y0, y1: d.y1, idx:idx, yErr: getYerr(d,i)}
+ })
+ });
+
+ x.domain(xDomain || d3.merge(seriesData).map(function(d) { return d.x }))
+ .rangeBands(xRange || [0, availableWidth], groupSpacing);
+
+ y.domain(yDomain || d3.extent(d3.merge(
+ d3.merge(seriesData).map(function(d) {
+ var domain = d.y;
+ // increase the domain range if this series is stackable
+ if (stacked && !data[d.idx].nonStackable) {
+ if (d.y > 0){
+ domain = d.y1
+ } else {
+ domain = d.y1 + d.y
+ }
+ }
+ var yerr = d.yErr;
+ if (yerr) {
+ if (yerr.length) {
+ return [domain + yerr[0], domain + yerr[1]];
+ } else {
+ yerr = Math.abs(yerr)
+ return [domain - yerr, domain + yerr];
+ }
+ } else {
+ return [domain];
+ }
+ })).concat(forceY)))
+ .range(yRange || [availableHeight, 0]);
+
+ // If scale's domain don't have a range, slightly adjust to make one... so a chart can show a single data point
+ if (x.domain()[0] === x.domain()[1])
+ x.domain()[0] ?
+ x.domain([x.domain()[0] - x.domain()[0] * 0.01, x.domain()[1] + x.domain()[1] * 0.01])
+ : x.domain([-1,1]);
+
+ if (y.domain()[0] === y.domain()[1])
+ y.domain()[0] ?
+ y.domain([y.domain()[0] + y.domain()[0] * 0.01, y.domain()[1] - y.domain()[1] * 0.01])
+ : y.domain([-1,1]);
+
+ x0 = x0 || x;
+ y0 = y0 || y;
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-multibar').data([data]);
+ var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-multibar');
+ var defsEnter = wrapEnter.append('defs');
+ var gEnter = wrapEnter.append('g');
+ var g = wrap.select('g');
+
+ gEnter.append('g').attr('class', 'nv-groups');
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ defsEnter.append('clipPath')
+ .attr('id', 'nv-edge-clip-' + id)
+ .append('rect');
+ wrap.select('#nv-edge-clip-' + id + ' rect')
+ .attr('width', availableWidth)
+ .attr('height', availableHeight);
+
+ g.attr('clip-path', clipEdge ? 'url(#nv-edge-clip-' + id + ')' : '');
+
+ var groups = wrap.select('.nv-groups').selectAll('.nv-group')
+ .data(function(d) { return d }, function(d,i) { return i });
+ groups.enter().append('g')
+ .style('stroke-opacity', 1e-6)
+ .style('fill-opacity', 1e-6);
+
+ var exitTransition = renderWatch
+ .transition(groups.exit().selectAll('g.nv-bar'), 'multibarExit', Math.min(100, duration))
+ .attr('y', function(d, i, j) {
+ var yVal = y0(0) || 0;
+ if (stacked) {
+ if (data[d.series] && !data[d.series].nonStackable) {
+ yVal = y0(d.y0);
+ }
+ }
+ return yVal;
+ })
+ .attr('height', 0)
+ .remove();
+ if (exitTransition.delay)
+ exitTransition.delay(function(d,i) {
+ var delay = i * (duration / (last_datalength + 1)) - i;
+ return delay;
+ });
+ groups
+ .attr('class', function(d,i) { return 'nv-group nv-series-' + i })
+ .classed('hover', function(d) { return d.hover })
+ .style('fill', function(d,i){ return color(d, i) })
+ .style('stroke', function(d,i){ return color(d, i) });
+ groups
+ .style('stroke-opacity', 1)
+ .style('fill-opacity', 0.75);
+
+ var bars = groups.selectAll('g.nv-bar')
+ .data(function(d) { return (hideable && !data.length) ? hideable.values : d.values });
+ bars.exit().remove();
+
+ var barsEnter = bars.enter().append('g')
+ .attr('class', function(d,i) { return getY(d,i) < 0 ? 'nv-bar negative' : 'nv-bar positive'})
+ .attr('transform', function(d,i,j) {
+ var _x = stacked && !data[j].nonStackable ? 0 : (j * x.rangeBand() / data.length );
+ var _y = y0(stacked && !data[j].nonStackable ? d.y0 : 0) || 0;
+ return 'translate(' + _x + ',' + _y + ')';
+ })
+ ;
+
+ barsEnter.append('rect')
+ .attr('height', 0)
+ .attr('width', function(d,i,j) { return x.rangeBand() / (stacked && !data[j].nonStackable ? 1 : data.length) })
+ .style('fill', function(d,i,j){ return color(d, j, i); })
+ .style('stroke', function(d,i,j){ return color(d, j, i); })
+ ;
+ bars
+ .on('mouseover', function(d,i) { //TODO: figure out why j works above, but not here
+ d3.select(this).classed('hover', true);
+ dispatch.elementMouseover({
+ data: d,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+ })
+ .on('mouseout', function(d,i) {
+ d3.select(this).classed('hover', false);
+ dispatch.elementMouseout({
+ data: d,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+ })
+ .on('mousemove', function(d,i) {
+ dispatch.elementMousemove({
+ data: d,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+ })
+ .on('click', function(d,i) {
+ dispatch.elementClick({
+ data: d,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+ d3.event.stopPropagation();
+ })
+ .on('dblclick', function(d,i) {
+ dispatch.elementDblClick({
+ data: d,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+ d3.event.stopPropagation();
+ });
+
+ if (getYerr(data[0].values[0], 0)) {
+ barsEnter.append('polyline');
+
+ bars.select('polyline')
+ .attr('fill', 'none')
+ .attr('stroke', function(d, i, j) { return errorBarColor(d, j, i); })
+ .attr('points', function(d,i) {
+ var yerr = getYerr(d,i)
+ , mid = 0.8 * x.rangeBand() / ((stacked ? 1 : data.length) * 2);
+ yerr = yerr.length ? yerr : [-Math.abs(yerr), Math.abs(yerr)];
+ yerr = yerr.map(function(e) { return y(e) - y(0); });
+ var a = [[-mid, yerr[0]], [mid, yerr[0]], [0, yerr[0]], [0, yerr[1]], [-mid, yerr[1]], [mid, yerr[1]]];
+ return a.map(function (path) { return path.join(',') }).join(' ');
+ })
+ .attr('transform', function(d, i) {
+ var xOffset = x.rangeBand() / ((stacked ? 1 : data.length) * 2);
+ var yOffset = getY(d,i) < 0 ? y(getY(d, i)) - y(0) : 0;
+ return 'translate(' + xOffset + ', ' + yOffset + ')';
+ })
+ }
+
+ bars
+ .attr('class', function(d,i) { return getY(d,i) < 0 ? 'nv-bar negative' : 'nv-bar positive'})
+
+ if (barColor) {
+ if (!disabled) disabled = data.map(function() { return true });
+ bars.select('rect')
+ .style('fill', function(d,i,j) { return d3.rgb(barColor(d,i)).darker( disabled.map(function(d,i) { return i }).filter(function(d,i){ return !disabled[i] })[j] ).toString(); })
+ .style('stroke', function(d,i,j) { return d3.rgb(barColor(d,i)).darker( disabled.map(function(d,i) { return i }).filter(function(d,i){ return !disabled[i] })[j] ).toString(); });
+ }
+
+ var barSelection =
+ bars.watchTransition(renderWatch, 'multibar', Math.min(250, duration))
+ .delay(function(d,i) {
+ return i * duration / data[0].values.length;
+ });
+ if (stacked){
+ barSelection
+ .attr('transform', function(d,i,j) {
+ var yVal = 0;
+ // if stackable, stack it on top of the previous series
+ if (!data[j].nonStackable) {
+ yVal = y(d.y1);
+ } else {
+ if (getY(d,i) < 0){
+ yVal = y(0);
+ } else {
+ if (y(0) - y(getY(d,i)) < -1){
+ yVal = y(0) - 1;
+ } else {
+ yVal = y(getY(d, i)) || 0;
+ }
+ }
+ }
+ var width = 0;
+ if (data[j].nonStackable) {
+ width = d.series * x.rangeBand() / data.length;
+ if (data.length !== nonStackableCount){
+ width = data[j].nonStackableSeries * x.rangeBand()/(nonStackableCount*2);
+ }
+ }
+ var xVal = width + x(getX(d, i));
+ return 'translate(' + xVal + ',' + yVal + ')';
+ })
+ .select('rect')
+ .attr('height', function(d,i,j) {
+ if (!data[j].nonStackable) {
+ return Math.max(Math.abs(y(d.y+d.y0) - y(d.y0)), 1);
+ } else {
+ return Math.max(Math.abs(y(getY(d,i)) - y(0)),1) || 0;
+ }
+ })
+ .attr('width', function(d,i,j){
+ if (!data[j].nonStackable) {
+ return x.rangeBand();
+ } else {
+ // if all series are nonStacable, take the full width
+ var width = (x.rangeBand() / nonStackableCount);
+ // otherwise, nonStackable graph will be only taking the half-width
+ // of the x rangeBand
+ if (data.length !== nonStackableCount) {
+ width = x.rangeBand()/(nonStackableCount*2);
+ }
+ return width;
+ }
+ });
+ }
+ else {
+ barSelection.attr('transform', function(d,i) {
+ var xVal = d.series * x.rangeBand() / data.length + x(getX(d, i));
+ var yVal = getY(d,i) < 0 ?
+ y(0) :
+ y(0) - y(getY(d,i)) < 1 ?
+ y(0) - 1 :
+ y(getY(d,i)) || 0;
+ return 'translate(' + xVal + ',' + yVal + ')';
+ })
+ .select('rect')
+ .attr('width', x.rangeBand() / data.length)
+ .attr('height', function(d,i) {
+ return Math.max(Math.abs(y(getY(d,i)) - y(0)),1) || 0;
+ });
+ }
+
+ //store old scales for use in transitions on update
+ x0 = x.copy();
+ y0 = y.copy();
+
+ // keep track of the last data value length for transition calculations
+ if (data[0] && data[0].values) {
+ last_datalength = data[0].values.length;
+ }
+
+ });
+
+ renderWatch.renderEnd('multibar immediate');
+
+ return chart;
+ }
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ chart.dispatch = dispatch;
+
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ x: {get: function(){return getX;}, set: function(_){getX=_;}},
+ y: {get: function(){return getY;}, set: function(_){getY=_;}},
+ yErr: {get: function(){return getYerr;}, set: function(_){getYerr=_;}},
+ xScale: {get: function(){return x;}, set: function(_){x=_;}},
+ yScale: {get: function(){return y;}, set: function(_){y=_;}},
+ xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}},
+ yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}},
+ xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}},
+ yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}},
+ forceY: {get: function(){return forceY;}, set: function(_){forceY=_;}},
+ stacked: {get: function(){return stacked;}, set: function(_){stacked=_;}},
+ stackOffset: {get: function(){return stackOffset;}, set: function(_){stackOffset=_;}},
+ clipEdge: {get: function(){return clipEdge;}, set: function(_){clipEdge=_;}},
+ disabled: {get: function(){return disabled;}, set: function(_){disabled=_;}},
+ id: {get: function(){return id;}, set: function(_){id=_;}},
+ hideable: {get: function(){return hideable;}, set: function(_){hideable=_;}},
+ groupSpacing:{get: function(){return groupSpacing;}, set: function(_){groupSpacing=_;}},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ duration: {get: function(){return duration;}, set: function(_){
+ duration = _;
+ renderWatch.reset(duration);
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ }},
+ barColor: {get: function(){return barColor;}, set: function(_){
+ barColor = _ ? nv.utils.getColor(_) : null;
+ }},
+ errorBarColor: {get: function(){return errorBarColor;}, set: function(_){
+ errorBarColor = _ ? nv.utils.getColor(_) : null;
+ }}
+ });
+
+ nv.utils.initOptions(chart);
+
+ return chart;
+ };
+
+ nv.models.multiBarChart = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var multibar = nv.models.multiBar()
+ , xAxis = nv.models.axis()
+ , yAxis = nv.models.axis()
+ , legend = nv.models.legend()
+ , controls = nv.models.legend()
+ , tooltip = nv.models.tooltip()
+ ;
+
+ var margin = {top: 30, right: 20, bottom: 50, left: 60}
+ , width = null
+ , height = null
+ , color = nv.utils.defaultColor()
+ , showControls = true
+ , controlLabels = {}
+ , showLegend = true
+ , showXAxis = true
+ , showYAxis = true
+ , rightAlignYAxis = false
+ , reduceXTicks = true // if false a tick will show for every data point
+ , staggerLabels = false
+ , rotateLabels = 0
+ , x //can be accessed via chart.xScale()
+ , y //can be accessed via chart.yScale()
+ , state = nv.utils.state()
+ , defaultState = null
+ , noData = null
+ , dispatch = d3.dispatch('stateChange', 'changeState', 'renderEnd')
+ , controlWidth = function() { return showControls ? 180 : 0 }
+ , duration = 250
+ ;
+
+ state.stacked = false // DEPRECATED Maintained for backward compatibility
+
+ multibar.stacked(false);
+ xAxis
+ .orient('bottom')
+ .tickPadding(7)
+ .showMaxMin(false)
+ .tickFormat(function(d) { return d })
+ ;
+ yAxis
+ .orient((rightAlignYAxis) ? 'right' : 'left')
+ .tickFormat(d3.format(',.1f'))
+ ;
+
+ tooltip
+ .duration(0)
+ .valueFormatter(function(d, i) {
+ return yAxis.tickFormat()(d, i);
+ })
+ .headerFormatter(function(d, i) {
+ return xAxis.tickFormat()(d, i);
+ });
+
+ controls.updateState(false);
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ var renderWatch = nv.utils.renderWatch(dispatch);
+ var stacked = false;
+
+ var stateGetter = function(data) {
+ return function(){
+ return {
+ active: data.map(function(d) { return !d.disabled }),
+ stacked: stacked
+ };
+ }
+ };
+
+ var stateSetter = function(data) {
+ return function(state) {
+ if (state.stacked !== undefined)
+ stacked = state.stacked;
+ if (state.active !== undefined)
+ data.forEach(function(series,i) {
+ series.disabled = !state.active[i];
+ });
+ }
+ };
+
+ function chart(selection) {
+ renderWatch.reset();
+ renderWatch.models(multibar);
+ if (showXAxis) renderWatch.models(xAxis);
+ if (showYAxis) renderWatch.models(yAxis);
+
+ selection.each(function(data) {
+ var container = d3.select(this),
+ that = this;
+ nv.utils.initSVG(container);
+ var availableWidth = nv.utils.availableWidth(width, container, margin),
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+
+ chart.update = function() {
+ if (duration === 0)
+ container.call(chart);
+ else
+ container.transition()
+ .duration(duration)
+ .call(chart);
+ };
+ chart.container = this;
+
+ state
+ .setter(stateSetter(data), chart.update)
+ .getter(stateGetter(data))
+ .update();
+
+ // DEPRECATED set state.disableddisabled
+ state.disabled = data.map(function(d) { return !!d.disabled });
+
+ if (!defaultState) {
+ var key;
+ defaultState = {};
+ for (key in state) {
+ if (state[key] instanceof Array)
+ defaultState[key] = state[key].slice(0);
+ else
+ defaultState[key] = state[key];
+ }
+ }
+
+ // Display noData message if there's nothing to show.
+ if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) {
+ nv.utils.noData(chart, container)
+ return chart;
+ } else {
+ container.selectAll('.nv-noData').remove();
+ }
+
+ // Setup Scales
+ x = multibar.xScale();
+ y = multibar.yScale();
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-multiBarWithLegend').data([data]);
+ var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-multiBarWithLegend').append('g');
+ var g = wrap.select('g');
+
+ gEnter.append('g').attr('class', 'nv-x nv-axis');
+ gEnter.append('g').attr('class', 'nv-y nv-axis');
+ gEnter.append('g').attr('class', 'nv-barsWrap');
+ gEnter.append('g').attr('class', 'nv-legendWrap');
+ gEnter.append('g').attr('class', 'nv-controlsWrap');
+
+ // Legend
+ if (showLegend) {
+ legend.width(availableWidth - controlWidth());
+
+ g.select('.nv-legendWrap')
+ .datum(data)
+ .call(legend);
+
+ if ( margin.top != legend.height()) {
+ margin.top = legend.height();
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+ }
+
+ g.select('.nv-legendWrap')
+ .attr('transform', 'translate(' + controlWidth() + ',' + (-margin.top) +')');
+ }
+
+ // Controls
+ if (showControls) {
+ var controlsData = [
+ { key: controlLabels.grouped || 'Grouped', disabled: multibar.stacked() },
+ { key: controlLabels.stacked || 'Stacked', disabled: !multibar.stacked() }
+ ];
+
+ controls.width(controlWidth()).color(['#444', '#444', '#444']);
+ g.select('.nv-controlsWrap')
+ .datum(controlsData)
+ .attr('transform', 'translate(0,' + (-margin.top) +')')
+ .call(controls);
+ }
+
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+ if (rightAlignYAxis) {
+ g.select(".nv-y.nv-axis")
+ .attr("transform", "translate(" + availableWidth + ",0)");
+ }
+
+ // Main Chart Component(s)
+ multibar
+ .disabled(data.map(function(series) { return series.disabled }))
+ .width(availableWidth)
+ .height(availableHeight)
+ .color(data.map(function(d,i) {
+ return d.color || color(d, i);
+ }).filter(function(d,i) { return !data[i].disabled }));
+
+
+ var barsWrap = g.select('.nv-barsWrap')
+ .datum(data.filter(function(d) { return !d.disabled }));
+
+ barsWrap.call(multibar);
+
+ // Setup Axes
+ if (showXAxis) {
+ xAxis
+ .scale(x)
+ ._ticks( nv.utils.calcTicksX(availableWidth/100, data) )
+ .tickSize(-availableHeight, 0);
+
+ g.select('.nv-x.nv-axis')
+ .attr('transform', 'translate(0,' + y.range()[0] + ')');
+ g.select('.nv-x.nv-axis')
+ .call(xAxis);
+
+ var xTicks = g.select('.nv-x.nv-axis > g').selectAll('g');
+
+ xTicks
+ .selectAll('line, text')
+ .style('opacity', 1)
+
+ if (staggerLabels) {
+ var getTranslate = function(x,y) {
+ return "translate(" + x + "," + y + ")";
+ };
+
+ var staggerUp = 5, staggerDown = 17; //pixels to stagger by
+ // Issue #140
+ xTicks
+ .selectAll("text")
+ .attr('transform', function(d,i,j) {
+ return getTranslate(0, (j % 2 == 0 ? staggerUp : staggerDown));
+ });
+
+ var totalInBetweenTicks = d3.selectAll(".nv-x.nv-axis .nv-wrap g g text")[0].length;
+ g.selectAll(".nv-x.nv-axis .nv-axisMaxMin text")
+ .attr("transform", function(d,i) {
+ return getTranslate(0, (i === 0 || totalInBetweenTicks % 2 !== 0) ? staggerDown : staggerUp);
+ });
+ }
+
+ if (reduceXTicks)
+ xTicks
+ .filter(function(d,i) {
+ return i % Math.ceil(data[0].values.length / (availableWidth / 100)) !== 0;
+ })
+ .selectAll('text, line')
+ .style('opacity', 0);
+
+ if(rotateLabels)
+ xTicks
+ .selectAll('.tick text')
+ .attr('transform', 'rotate(' + rotateLabels + ' 0,0)')
+ .style('text-anchor', rotateLabels > 0 ? 'start' : 'end');
+
+ g.select('.nv-x.nv-axis').selectAll('g.nv-axisMaxMin text')
+ .style('opacity', 1);
+ }
+
+ if (showYAxis) {
+ yAxis
+ .scale(y)
+ ._ticks( nv.utils.calcTicksY(availableHeight/36, data) )
+ .tickSize( -availableWidth, 0);
+
+ g.select('.nv-y.nv-axis')
+ .call(yAxis);
+ }
+
+ //============================================================
+ // Event Handling/Dispatching (in chart's scope)
+ //------------------------------------------------------------
+
+ legend.dispatch.on('stateChange', function(newState) {
+ for (var key in newState)
+ state[key] = newState[key];
+ dispatch.stateChange(state);
+ chart.update();
+ });
+
+ controls.dispatch.on('legendClick', function(d,i) {
+ if (!d.disabled) return;
+ controlsData = controlsData.map(function(s) {
+ s.disabled = true;
+ return s;
+ });
+ d.disabled = false;
+
+ switch (d.key) {
+ case 'Grouped':
+ case controlLabels.grouped:
+ multibar.stacked(false);
+ break;
+ case 'Stacked':
+ case controlLabels.stacked:
+ multibar.stacked(true);
+ break;
+ }
+
+ state.stacked = multibar.stacked();
+ dispatch.stateChange(state);
+ chart.update();
+ });
+
+ // Update chart from a state object passed to event handler
+ dispatch.on('changeState', function(e) {
+ if (typeof e.disabled !== 'undefined') {
+ data.forEach(function(series,i) {
+ series.disabled = e.disabled[i];
+ });
+ state.disabled = e.disabled;
+ }
+ if (typeof e.stacked !== 'undefined') {
+ multibar.stacked(e.stacked);
+ state.stacked = e.stacked;
+ stacked = e.stacked;
+ }
+ chart.update();
+ });
+ });
+
+ renderWatch.renderEnd('multibarchart immediate');
+ return chart;
+ }
+
+ //============================================================
+ // Event Handling/Dispatching (out of chart's scope)
+ //------------------------------------------------------------
+
+ multibar.dispatch.on('elementMouseover.tooltip', function(evt) {
+ evt.value = chart.x()(evt.data);
+ evt['series'] = {
+ key: evt.data.key,
+ value: chart.y()(evt.data),
+ color: evt.color
+ };
+ tooltip.data(evt).hidden(false);
+ });
+
+ multibar.dispatch.on('elementMouseout.tooltip', function(evt) {
+ tooltip.hidden(true);
+ });
+
+ multibar.dispatch.on('elementMousemove.tooltip', function(evt) {
+ tooltip.position({top: d3.event.pageY, left: d3.event.pageX})();
+ });
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ // expose chart's sub-components
+ chart.dispatch = dispatch;
+ chart.multibar = multibar;
+ chart.legend = legend;
+ chart.controls = controls;
+ chart.xAxis = xAxis;
+ chart.yAxis = yAxis;
+ chart.state = state;
+ chart.tooltip = tooltip;
+
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}},
+ showControls: {get: function(){return showControls;}, set: function(_){showControls=_;}},
+ controlLabels: {get: function(){return controlLabels;}, set: function(_){controlLabels=_;}},
+ showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}},
+ showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}},
+ defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}},
+ noData: {get: function(){return noData;}, set: function(_){noData=_;}},
+ reduceXTicks: {get: function(){return reduceXTicks;}, set: function(_){reduceXTicks=_;}},
+ rotateLabels: {get: function(){return rotateLabels;}, set: function(_){rotateLabels=_;}},
+ staggerLabels: {get: function(){return staggerLabels;}, set: function(_){staggerLabels=_;}},
+
+ // deprecated options
+ tooltips: {get: function(){return tooltip.enabled();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead');
+ tooltip.enabled(!!_);
+ }},
+ tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead');
+ tooltip.contentGenerator(_);
+ }},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ duration: {get: function(){return duration;}, set: function(_){
+ duration = _;
+ multibar.duration(duration);
+ xAxis.duration(duration);
+ yAxis.duration(duration);
+ renderWatch.reset(duration);
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ legend.color(color);
+ }},
+ rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){
+ rightAlignYAxis = _;
+ yAxis.orient( rightAlignYAxis ? 'right' : 'left');
+ }},
+ barColor: {get: function(){return multibar.barColor;}, set: function(_){
+ multibar.barColor(_);
+ legend.color(function(d,i) {return d3.rgb('#ccc').darker(i * 1.5).toString();})
+ }}
+ });
+
+ nv.utils.inheritOptions(chart, multibar);
+ nv.utils.initOptions(chart);
+
+ return chart;
+ };
+
+ nv.models.multiBarHorizontal = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var margin = {top: 0, right: 0, bottom: 0, left: 0}
+ , width = 960
+ , height = 500
+ , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one
+ , container = null
+ , x = d3.scale.ordinal()
+ , y = d3.scale.linear()
+ , getX = function(d) { return d.x }
+ , getY = function(d) { return d.y }
+ , getYerr = function(d) { return d.yErr }
+ , forceY = [0] // 0 is forced by default.. this makes sense for the majority of bar graphs... user can always do chart.forceY([]) to remove
+ , color = nv.utils.defaultColor()
+ , barColor = null // adding the ability to set the color for each rather than the whole group
+ , errorBarColor = nv.utils.defaultColor()
+ , disabled // used in conjunction with barColor to communicate from multiBarHorizontalChart what series are disabled
+ , stacked = false
+ , showValues = false
+ , showBarLabels = false
+ , valuePadding = 60
+ , groupSpacing = 0.1
+ , valueFormat = d3.format(',.2f')
+ , delay = 1200
+ , xDomain
+ , yDomain
+ , xRange
+ , yRange
+ , duration = 250
+ , dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd')
+ ;
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ var x0, y0; //used to store previous scales
+ var renderWatch = nv.utils.renderWatch(dispatch, duration);
+
+ function chart(selection) {
+ renderWatch.reset();
+ selection.each(function(data) {
+ var availableWidth = width - margin.left - margin.right,
+ availableHeight = height - margin.top - margin.bottom;
+
+ container = d3.select(this);
+ nv.utils.initSVG(container);
+
+ if (stacked)
+ data = d3.layout.stack()
+ .offset('zero')
+ .values(function(d){ return d.values })
+ .y(getY)
+ (data);
+
+ //add series index and key to each data point for reference
+ data.forEach(function(series, i) {
+ series.values.forEach(function(point) {
+ point.series = i;
+ point.key = series.key;
+ });
+ });
+
+ // HACK for negative value stacking
+ if (stacked)
+ data[0].values.map(function(d,i) {
+ var posBase = 0, negBase = 0;
+ data.map(function(d) {
+ var f = d.values[i]
+ f.size = Math.abs(f.y);
+ if (f.y<0) {
+ f.y1 = negBase - f.size;
+ negBase = negBase - f.size;
+ } else
+ {
+ f.y1 = posBase;
+ posBase = posBase + f.size;
+ }
+ });
+ });
+
+ // Setup Scales
+ // remap and flatten the data for use in calculating the scales' domains
+ var seriesData = (xDomain && yDomain) ? [] : // if we know xDomain and yDomain, no need to calculate
+ data.map(function(d) {
+ return d.values.map(function(d,i) {
+ return { x: getX(d,i), y: getY(d,i), y0: d.y0, y1: d.y1, yErr: getYerr(d,i) }
+ })
+ });
+
+ x.domain(xDomain || d3.merge(seriesData).map(function(d) { return d.x }))
+ .rangeBands(xRange || [0, availableHeight], groupSpacing);
+
+ y.domain(yDomain || d3.extent(d3.merge(
+ d3.merge(seriesData).map(function(d) {
+ var domain = d.y;
+ if (stacked) {
+ if (d.y > 0){
+ domain = d.y1 + d.y
+ } else {
+ domain = d.y1
+ }
+ }
+ var yerr = d.yErr;
+ if (yerr) {
+ if (yerr.length) {
+ return [domain + yerr[0], domain + yerr[1]];
+ } else {
+ yerr = Math.abs(yerr)
+ return [domain - yerr, domain + yerr];
+ }
+ } else {
+ return [domain];
+ }
+ })).concat(forceY)))
+
+ if (showValues && !stacked)
+ y.range(yRange || [(y.domain()[0] < 0 ? valuePadding : 0), availableWidth - (y.domain()[1] > 0 ? valuePadding : 0) ]);
+ else
+ y.range(yRange || [0, availableWidth]);
+
+ x0 = x0 || x;
+ y0 = y0 || d3.scale.linear().domain(y.domain()).range([y(0),y(0)]);
+
+ // Setup containers and skeleton of chart
+ var wrap = d3.select(this).selectAll('g.nv-wrap.nv-multibarHorizontal').data([data]);
+ var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-multibarHorizontal');
+ var defsEnter = wrapEnter.append('defs');
+ var gEnter = wrapEnter.append('g');
+ var g = wrap.select('g');
+
+ gEnter.append('g').attr('class', 'nv-groups');
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ var groups = wrap.select('.nv-groups').selectAll('.nv-group')
+ .data(function(d) { return d }, function(d,i) { return i });
+ groups.enter().append('g')
+ .style('stroke-opacity', 1e-6)
+ .style('fill-opacity', 1e-6);
+ groups.exit().watchTransition(renderWatch, 'multibarhorizontal: exit groups')
+ .style('stroke-opacity', 1e-6)
+ .style('fill-opacity', 1e-6)
+ .remove();
+ groups
+ .attr('class', function(d,i) { return 'nv-group nv-series-' + i })
+ .classed('hover', function(d) { return d.hover })
+ .style('fill', function(d,i){ return color(d, i) })
+ .style('stroke', function(d,i){ return color(d, i) });
+ groups.watchTransition(renderWatch, 'multibarhorizontal: groups')
+ .style('stroke-opacity', 1)
+ .style('fill-opacity', .75);
+
+ var bars = groups.selectAll('g.nv-bar')
+ .data(function(d) { return d.values });
+ bars.exit().remove();
+
+ var barsEnter = bars.enter().append('g')
+ .attr('transform', function(d,i,j) {
+ return 'translate(' + y0(stacked ? d.y0 : 0) + ',' + (stacked ? 0 : (j * x.rangeBand() / data.length ) + x(getX(d,i))) + ')'
+ });
+
+ barsEnter.append('rect')
+ .attr('width', 0)
+ .attr('height', x.rangeBand() / (stacked ? 1 : data.length) )
+
+ bars
+ .on('mouseover', function(d,i) { //TODO: figure out why j works above, but not here
+ d3.select(this).classed('hover', true);
+ dispatch.elementMouseover({
+ data: d,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+ })
+ .on('mouseout', function(d,i) {
+ d3.select(this).classed('hover', false);
+ dispatch.elementMouseout({
+ data: d,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+ })
+ .on('mouseout', function(d,i) {
+ dispatch.elementMouseout({
+ data: d,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+ })
+ .on('mousemove', function(d,i) {
+ dispatch.elementMousemove({
+ data: d,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+ })
+ .on('click', function(d,i) {
+ dispatch.elementClick({
+ data: d,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+ d3.event.stopPropagation();
+ })
+ .on('dblclick', function(d,i) {
+ dispatch.elementDblClick({
+ data: d,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+ d3.event.stopPropagation();
+ });
+
+ if (getYerr(data[0].values[0], 0)) {
+ barsEnter.append('polyline');
+
+ bars.select('polyline')
+ .attr('fill', 'none')
+ .attr('stroke', function(d,i,j) { return errorBarColor(d, j, i); })
+ .attr('points', function(d,i) {
+ var xerr = getYerr(d,i)
+ , mid = 0.8 * x.rangeBand() / ((stacked ? 1 : data.length) * 2);
+ xerr = xerr.length ? xerr : [-Math.abs(xerr), Math.abs(xerr)];
+ xerr = xerr.map(function(e) { return y(e) - y(0); });
+ var a = [[xerr[0],-mid], [xerr[0],mid], [xerr[0],0], [xerr[1],0], [xerr[1],-mid], [xerr[1],mid]];
+ return a.map(function (path) { return path.join(',') }).join(' ');
+ })
+ .attr('transform', function(d,i) {
+ var mid = x.rangeBand() / ((stacked ? 1 : data.length) * 2);
+ return 'translate(' + (getY(d,i) < 0 ? 0 : y(getY(d,i)) - y(0)) + ', ' + mid + ')'
+ });
+ }
+
+ barsEnter.append('text');
+
+ if (showValues && !stacked) {
+ bars.select('text')
+ .attr('text-anchor', function(d,i) { return getY(d,i) < 0 ? 'end' : 'start' })
+ .attr('y', x.rangeBand() / (data.length * 2))
+ .attr('dy', '.32em')
+ .text(function(d,i) {
+ var t = valueFormat(getY(d,i))
+ , yerr = getYerr(d,i);
+ if (yerr === undefined)
+ return t;
+ if (!yerr.length)
+ return t + '±' + valueFormat(Math.abs(yerr));
+ return t + '+' + valueFormat(Math.abs(yerr[1])) + '-' + valueFormat(Math.abs(yerr[0]));
+ });
+ bars.watchTransition(renderWatch, 'multibarhorizontal: bars')
+ .select('text')
+ .attr('x', function(d,i) { return getY(d,i) < 0 ? -4 : y(getY(d,i)) - y(0) + 4 })
+ } else {
+ bars.selectAll('text').text('');
+ }
+
+ if (showBarLabels && !stacked) {
+ barsEnter.append('text').classed('nv-bar-label',true);
+ bars.select('text.nv-bar-label')
+ .attr('text-anchor', function(d,i) { return getY(d,i) < 0 ? 'start' : 'end' })
+ .attr('y', x.rangeBand() / (data.length * 2))
+ .attr('dy', '.32em')
+ .text(function(d,i) { return getX(d,i) });
+ bars.watchTransition(renderWatch, 'multibarhorizontal: bars')
+ .select('text.nv-bar-label')
+ .attr('x', function(d,i) { return getY(d,i) < 0 ? y(0) - y(getY(d,i)) + 4 : -4 });
+ }
+ else {
+ bars.selectAll('text.nv-bar-label').text('');
+ }
+
+ bars
+ .attr('class', function(d,i) { return getY(d,i) < 0 ? 'nv-bar negative' : 'nv-bar positive'})
+
+ if (barColor) {
+ if (!disabled) disabled = data.map(function() { return true });
+ bars
+ .style('fill', function(d,i,j) { return d3.rgb(barColor(d,i)).darker( disabled.map(function(d,i) { return i }).filter(function(d,i){ return !disabled[i] })[j] ).toString(); })
+ .style('stroke', function(d,i,j) { return d3.rgb(barColor(d,i)).darker( disabled.map(function(d,i) { return i }).filter(function(d,i){ return !disabled[i] })[j] ).toString(); });
+ }
+
+ if (stacked)
+ bars.watchTransition(renderWatch, 'multibarhorizontal: bars')
+ .attr('transform', function(d,i) {
+ return 'translate(' + y(d.y1) + ',' + x(getX(d,i)) + ')'
+ })
+ .select('rect')
+ .attr('width', function(d,i) {
+ return Math.abs(y(getY(d,i) + d.y0) - y(d.y0))
+ })
+ .attr('height', x.rangeBand() );
+ else
+ bars.watchTransition(renderWatch, 'multibarhorizontal: bars')
+ .attr('transform', function(d,i) {
+ //TODO: stacked must be all positive or all negative, not both?
+ return 'translate(' +
+ (getY(d,i) < 0 ? y(getY(d,i)) : y(0))
+ + ',' +
+ (d.series * x.rangeBand() / data.length
+ +
+ x(getX(d,i)) )
+ + ')'
+ })
+ .select('rect')
+ .attr('height', x.rangeBand() / data.length )
+ .attr('width', function(d,i) {
+ return Math.max(Math.abs(y(getY(d,i)) - y(0)),1)
+ });
+
+ //store old scales for use in transitions on update
+ x0 = x.copy();
+ y0 = y.copy();
+
+ });
+
+ renderWatch.renderEnd('multibarHorizontal immediate');
+ return chart;
+ }
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ chart.dispatch = dispatch;
+
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ x: {get: function(){return getX;}, set: function(_){getX=_;}},
+ y: {get: function(){return getY;}, set: function(_){getY=_;}},
+ yErr: {get: function(){return getYerr;}, set: function(_){getYerr=_;}},
+ xScale: {get: function(){return x;}, set: function(_){x=_;}},
+ yScale: {get: function(){return y;}, set: function(_){y=_;}},
+ xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}},
+ yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}},
+ xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}},
+ yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}},
+ forceY: {get: function(){return forceY;}, set: function(_){forceY=_;}},
+ stacked: {get: function(){return stacked;}, set: function(_){stacked=_;}},
+ showValues: {get: function(){return showValues;}, set: function(_){showValues=_;}},
+ // this shows the group name, seems pointless?
+ //showBarLabels: {get: function(){return showBarLabels;}, set: function(_){showBarLabels=_;}},
+ disabled: {get: function(){return disabled;}, set: function(_){disabled=_;}},
+ id: {get: function(){return id;}, set: function(_){id=_;}},
+ valueFormat: {get: function(){return valueFormat;}, set: function(_){valueFormat=_;}},
+ valuePadding: {get: function(){return valuePadding;}, set: function(_){valuePadding=_;}},
+ groupSpacing:{get: function(){return groupSpacing;}, set: function(_){groupSpacing=_;}},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ duration: {get: function(){return duration;}, set: function(_){
+ duration = _;
+ renderWatch.reset(duration);
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ }},
+ barColor: {get: function(){return barColor;}, set: function(_){
+ barColor = _ ? nv.utils.getColor(_) : null;
+ }},
+ errorBarColor: {get: function(){return errorBarColor;}, set: function(_){
+ errorBarColor = _ ? nv.utils.getColor(_) : null;
+ }}
+ });
+
+ nv.utils.initOptions(chart);
+
+ return chart;
+ };
+
+ nv.models.multiBarHorizontalChart = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var multibar = nv.models.multiBarHorizontal()
+ , xAxis = nv.models.axis()
+ , yAxis = nv.models.axis()
+ , legend = nv.models.legend().height(30)
+ , controls = nv.models.legend().height(30)
+ , tooltip = nv.models.tooltip()
+ ;
+
+ var margin = {top: 30, right: 20, bottom: 50, left: 60}
+ , width = null
+ , height = null
+ , color = nv.utils.defaultColor()
+ , showControls = true
+ , controlLabels = {}
+ , showLegend = true
+ , showXAxis = true
+ , showYAxis = true
+ , stacked = false
+ , x //can be accessed via chart.xScale()
+ , y //can be accessed via chart.yScale()
+ , state = nv.utils.state()
+ , defaultState = null
+ , noData = null
+ , dispatch = d3.dispatch('stateChange', 'changeState','renderEnd')
+ , controlWidth = function() { return showControls ? 180 : 0 }
+ , duration = 250
+ ;
+
+ state.stacked = false; // DEPRECATED Maintained for backward compatibility
+
+ multibar.stacked(stacked);
+
+ xAxis
+ .orient('left')
+ .tickPadding(5)
+ .showMaxMin(false)
+ .tickFormat(function(d) { return d })
+ ;
+ yAxis
+ .orient('bottom')
+ .tickFormat(d3.format(',.1f'))
+ ;
+
+ tooltip
+ .duration(0)
+ .valueFormatter(function(d, i) {
+ return yAxis.tickFormat()(d, i);
+ })
+ .headerFormatter(function(d, i) {
+ return xAxis.tickFormat()(d, i);
+ });
+
+ controls.updateState(false);
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ var stateGetter = function(data) {
+ return function(){
+ return {
+ active: data.map(function(d) { return !d.disabled }),
+ stacked: stacked
+ };
+ }
+ };
+
+ var stateSetter = function(data) {
+ return function(state) {
+ if (state.stacked !== undefined)
+ stacked = state.stacked;
+ if (state.active !== undefined)
+ data.forEach(function(series,i) {
+ series.disabled = !state.active[i];
+ });
+ }
+ };
+
+ var renderWatch = nv.utils.renderWatch(dispatch, duration);
+
+ function chart(selection) {
+ renderWatch.reset();
+ renderWatch.models(multibar);
+ if (showXAxis) renderWatch.models(xAxis);
+ if (showYAxis) renderWatch.models(yAxis);
+
+ selection.each(function(data) {
+ var container = d3.select(this),
+ that = this;
+ nv.utils.initSVG(container);
+ var availableWidth = nv.utils.availableWidth(width, container, margin),
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+
+ chart.update = function() { container.transition().duration(duration).call(chart) };
+ chart.container = this;
+
+ stacked = multibar.stacked();
+
+ state
+ .setter(stateSetter(data), chart.update)
+ .getter(stateGetter(data))
+ .update();
+
+ // DEPRECATED set state.disableddisabled
+ state.disabled = data.map(function(d) { return !!d.disabled });
+
+ if (!defaultState) {
+ var key;
+ defaultState = {};
+ for (key in state) {
+ if (state[key] instanceof Array)
+ defaultState[key] = state[key].slice(0);
+ else
+ defaultState[key] = state[key];
+ }
+ }
+
+ // Display No Data message if there's nothing to show.
+ if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) {
+ nv.utils.noData(chart, container)
+ return chart;
+ } else {
+ container.selectAll('.nv-noData').remove();
+ }
+
+ // Setup Scales
+ x = multibar.xScale();
+ y = multibar.yScale();
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-multiBarHorizontalChart').data([data]);
+ var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-multiBarHorizontalChart').append('g');
+ var g = wrap.select('g');
+
+ gEnter.append('g').attr('class', 'nv-x nv-axis');
+ gEnter.append('g').attr('class', 'nv-y nv-axis')
+ .append('g').attr('class', 'nv-zeroLine')
+ .append('line');
+ gEnter.append('g').attr('class', 'nv-barsWrap');
+ gEnter.append('g').attr('class', 'nv-legendWrap');
+ gEnter.append('g').attr('class', 'nv-controlsWrap');
+
+ // Legend
+ if (showLegend) {
+ legend.width(availableWidth - controlWidth());
+
+ g.select('.nv-legendWrap')
+ .datum(data)
+ .call(legend);
+
+ if ( margin.top != legend.height()) {
+ margin.top = legend.height();
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+ }
+
+ g.select('.nv-legendWrap')
+ .attr('transform', 'translate(' + controlWidth() + ',' + (-margin.top) +')');
+ }
+
+ // Controls
+ if (showControls) {
+ var controlsData = [
+ { key: controlLabels.grouped || 'Grouped', disabled: multibar.stacked() },
+ { key: controlLabels.stacked || 'Stacked', disabled: !multibar.stacked() }
+ ];
+
+ controls.width(controlWidth()).color(['#444', '#444', '#444']);
+ g.select('.nv-controlsWrap')
+ .datum(controlsData)
+ .attr('transform', 'translate(0,' + (-margin.top) +')')
+ .call(controls);
+ }
+
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ // Main Chart Component(s)
+ multibar
+ .disabled(data.map(function(series) { return series.disabled }))
+ .width(availableWidth)
+ .height(availableHeight)
+ .color(data.map(function(d,i) {
+ return d.color || color(d, i);
+ }).filter(function(d,i) { return !data[i].disabled }));
+
+ var barsWrap = g.select('.nv-barsWrap')
+ .datum(data.filter(function(d) { return !d.disabled }));
+
+ barsWrap.transition().call(multibar);
+
+ // Setup Axes
+ if (showXAxis) {
+ xAxis
+ .scale(x)
+ ._ticks( nv.utils.calcTicksY(availableHeight/24, data) )
+ .tickSize(-availableWidth, 0);
+
+ g.select('.nv-x.nv-axis').call(xAxis);
+
+ var xTicks = g.select('.nv-x.nv-axis').selectAll('g');
+
+ xTicks
+ .selectAll('line, text');
+ }
+
+ if (showYAxis) {
+ yAxis
+ .scale(y)
+ ._ticks( nv.utils.calcTicksX(availableWidth/100, data) )
+ .tickSize( -availableHeight, 0);
+
+ g.select('.nv-y.nv-axis')
+ .attr('transform', 'translate(0,' + availableHeight + ')');
+ g.select('.nv-y.nv-axis').call(yAxis);
+ }
+
+ // Zero line
+ g.select(".nv-zeroLine line")
+ .attr("x1", y(0))
+ .attr("x2", y(0))
+ .attr("y1", 0)
+ .attr("y2", -availableHeight)
+ ;
+
+ //============================================================
+ // Event Handling/Dispatching (in chart's scope)
+ //------------------------------------------------------------
+
+ legend.dispatch.on('stateChange', function(newState) {
+ for (var key in newState)
+ state[key] = newState[key];
+ dispatch.stateChange(state);
+ chart.update();
+ });
+
+ controls.dispatch.on('legendClick', function(d,i) {
+ if (!d.disabled) return;
+ controlsData = controlsData.map(function(s) {
+ s.disabled = true;
+ return s;
+ });
+ d.disabled = false;
+
+ switch (d.key) {
+ case 'Grouped':
+ multibar.stacked(false);
+ break;
+ case 'Stacked':
+ multibar.stacked(true);
+ break;
+ }
+
+ state.stacked = multibar.stacked();
+ dispatch.stateChange(state);
+ stacked = multibar.stacked();
+
+ chart.update();
+ });
+
+ // Update chart from a state object passed to event handler
+ dispatch.on('changeState', function(e) {
+
+ if (typeof e.disabled !== 'undefined') {
+ data.forEach(function(series,i) {
+ series.disabled = e.disabled[i];
+ });
+
+ state.disabled = e.disabled;
+ }
+
+ if (typeof e.stacked !== 'undefined') {
+ multibar.stacked(e.stacked);
+ state.stacked = e.stacked;
+ stacked = e.stacked;
+ }
+
+ chart.update();
+ });
+ });
+ renderWatch.renderEnd('multibar horizontal chart immediate');
+ return chart;
+ }
+
+ //============================================================
+ // Event Handling/Dispatching (out of chart's scope)
+ //------------------------------------------------------------
+
+ multibar.dispatch.on('elementMouseover.tooltip', function(evt) {
+ evt.value = chart.x()(evt.data);
+ evt['series'] = {
+ key: evt.data.key,
+ value: chart.y()(evt.data),
+ color: evt.color
+ };
+ tooltip.data(evt).hidden(false);
+ });
+
+ multibar.dispatch.on('elementMouseout.tooltip', function(evt) {
+ tooltip.hidden(true);
+ });
+
+ multibar.dispatch.on('elementMousemove.tooltip', function(evt) {
+ tooltip.position({top: d3.event.pageY, left: d3.event.pageX})();
+ });
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ // expose chart's sub-components
+ chart.dispatch = dispatch;
+ chart.multibar = multibar;
+ chart.legend = legend;
+ chart.controls = controls;
+ chart.xAxis = xAxis;
+ chart.yAxis = yAxis;
+ chart.state = state;
+ chart.tooltip = tooltip;
+
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}},
+ showControls: {get: function(){return showControls;}, set: function(_){showControls=_;}},
+ controlLabels: {get: function(){return controlLabels;}, set: function(_){controlLabels=_;}},
+ showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}},
+ showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}},
+ defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}},
+ noData: {get: function(){return noData;}, set: function(_){noData=_;}},
+
+ // deprecated options
+ tooltips: {get: function(){return tooltip.enabled();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead');
+ tooltip.enabled(!!_);
+ }},
+ tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead');
+ tooltip.contentGenerator(_);
+ }},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ duration: {get: function(){return duration;}, set: function(_){
+ duration = _;
+ renderWatch.reset(duration);
+ multibar.duration(duration);
+ xAxis.duration(duration);
+ yAxis.duration(duration);
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ legend.color(color);
+ }},
+ barColor: {get: function(){return multibar.barColor;}, set: function(_){
+ multibar.barColor(_);
+ legend.color(function(d,i) {return d3.rgb('#ccc').darker(i * 1.5).toString();})
+ }}
+ });
+
+ nv.utils.inheritOptions(chart, multibar);
+ nv.utils.initOptions(chart);
+
+ return chart;
+ };
+ nv.models.multiChart = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var margin = {top: 30, right: 20, bottom: 50, left: 60},
+ color = nv.utils.defaultColor(),
+ width = null,
+ height = null,
+ showLegend = true,
+ noData = null,
+ yDomain1,
+ yDomain2,
+ getX = function(d) { return d.x },
+ getY = function(d) { return d.y},
+ interpolate = 'monotone',
+ useVoronoi = true
+ ;
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ var x = d3.scale.linear(),
+ yScale1 = d3.scale.linear(),
+ yScale2 = d3.scale.linear(),
+
+ lines1 = nv.models.line().yScale(yScale1),
+ lines2 = nv.models.line().yScale(yScale2),
+
+ scatters1 = nv.models.scatter().yScale(yScale1),
+ scatters2 = nv.models.scatter().yScale(yScale2),
+
+ bars1 = nv.models.multiBar().stacked(false).yScale(yScale1),
+ bars2 = nv.models.multiBar().stacked(false).yScale(yScale2),
+
+ stack1 = nv.models.stackedArea().yScale(yScale1),
+ stack2 = nv.models.stackedArea().yScale(yScale2),
+
+ xAxis = nv.models.axis().scale(x).orient('bottom').tickPadding(5),
+ yAxis1 = nv.models.axis().scale(yScale1).orient('left'),
+ yAxis2 = nv.models.axis().scale(yScale2).orient('right'),
+
+ legend = nv.models.legend().height(30),
+ tooltip = nv.models.tooltip(),
+ dispatch = d3.dispatch();
+
+ function chart(selection) {
+ selection.each(function(data) {
+ var container = d3.select(this),
+ that = this;
+ nv.utils.initSVG(container);
+
+ chart.update = function() { container.transition().call(chart); };
+ chart.container = this;
+
+ var availableWidth = nv.utils.availableWidth(width, container, margin),
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+
+ var dataLines1 = data.filter(function(d) {return d.type == 'line' && d.yAxis == 1});
+ var dataLines2 = data.filter(function(d) {return d.type == 'line' && d.yAxis == 2});
+ var dataScatters1 = data.filter(function(d) {return d.type == 'scatter' && d.yAxis == 1});
+ var dataScatters2 = data.filter(function(d) {return d.type == 'scatter' && d.yAxis == 2});
+ var dataBars1 = data.filter(function(d) {return d.type == 'bar' && d.yAxis == 1});
+ var dataBars2 = data.filter(function(d) {return d.type == 'bar' && d.yAxis == 2});
+ var dataStack1 = data.filter(function(d) {return d.type == 'area' && d.yAxis == 1});
+ var dataStack2 = data.filter(function(d) {return d.type == 'area' && d.yAxis == 2});
+
+ // Display noData message if there's nothing to show.
+ if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) {
+ nv.utils.noData(chart, container);
+ return chart;
+ } else {
+ container.selectAll('.nv-noData').remove();
+ }
+
+ var series1 = data.filter(function(d) {return !d.disabled && d.yAxis == 1})
+ .map(function(d) {
+ return d.values.map(function(d,i) {
+ return { x: d.x, y: d.y }
+ })
+ });
+
+ var series2 = data.filter(function(d) {return !d.disabled && d.yAxis == 2})
+ .map(function(d) {
+ return d.values.map(function(d,i) {
+ return { x: d.x, y: d.y }
+ })
+ });
+
+ x .domain(d3.extent(d3.merge(series1.concat(series2)), function(d) { return d.x } ))
+ .range([0, availableWidth]);
+
+ var wrap = container.selectAll('g.wrap.multiChart').data([data]);
+ var gEnter = wrap.enter().append('g').attr('class', 'wrap nvd3 multiChart').append('g');
+
+ gEnter.append('g').attr('class', 'nv-x nv-axis');
+ gEnter.append('g').attr('class', 'nv-y1 nv-axis');
+ gEnter.append('g').attr('class', 'nv-y2 nv-axis');
+ gEnter.append('g').attr('class', 'lines1Wrap');
+ gEnter.append('g').attr('class', 'lines2Wrap');
+ gEnter.append('g').attr('class', 'scatters1Wrap');
+ gEnter.append('g').attr('class', 'scatters2Wrap');
+ gEnter.append('g').attr('class', 'bars1Wrap');
+ gEnter.append('g').attr('class', 'bars2Wrap');
+ gEnter.append('g').attr('class', 'stack1Wrap');
+ gEnter.append('g').attr('class', 'stack2Wrap');
+ gEnter.append('g').attr('class', 'legendWrap');
+
+ var g = wrap.select('g');
+
+ var color_array = data.map(function(d,i) {
+ return data[i].color || color(d, i);
+ });
+
+ if (showLegend) {
+ var legendWidth = legend.align() ? availableWidth / 2 : availableWidth;
+ var legendXPosition = legend.align() ? legendWidth : 0;
+
+ legend.width(legendWidth);
+ legend.color(color_array);
+
+ g.select('.legendWrap')
+ .datum(data.map(function(series) {
+ series.originalKey = series.originalKey === undefined ? series.key : series.originalKey;
+ series.key = series.originalKey + (series.yAxis == 1 ? '' : ' (right axis)');
+ return series;
+ }))
+ .call(legend);
+
+ if ( margin.top != legend.height()) {
+ margin.top = legend.height();
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+ }
+
+ g.select('.legendWrap')
+ .attr('transform', 'translate(' + legendXPosition + ',' + (-margin.top) +')');
+ }
+
+ lines1
+ .width(availableWidth)
+ .height(availableHeight)
+ .interpolate(interpolate)
+ .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 1 && data[i].type == 'line'}));
+ lines2
+ .width(availableWidth)
+ .height(availableHeight)
+ .interpolate(interpolate)
+ .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 2 && data[i].type == 'line'}));
+ scatters1
+ .width(availableWidth)
+ .height(availableHeight)
+ .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 1 && data[i].type == 'scatter'}));
+ scatters2
+ .width(availableWidth)
+ .height(availableHeight)
+ .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 2 && data[i].type == 'scatter'}));
+ bars1
+ .width(availableWidth)
+ .height(availableHeight)
+ .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 1 && data[i].type == 'bar'}));
+ bars2
+ .width(availableWidth)
+ .height(availableHeight)
+ .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 2 && data[i].type == 'bar'}));
+ stack1
+ .width(availableWidth)
+ .height(availableHeight)
+ .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 1 && data[i].type == 'area'}));
+ stack2
+ .width(availableWidth)
+ .height(availableHeight)
+ .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 2 && data[i].type == 'area'}));
+
+ g.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ var lines1Wrap = g.select('.lines1Wrap')
+ .datum(dataLines1.filter(function(d){return !d.disabled}));
+ var scatters1Wrap = g.select('.scatters1Wrap')
+ .datum(dataScatters1.filter(function(d){return !d.disabled}));
+ var bars1Wrap = g.select('.bars1Wrap')
+ .datum(dataBars1.filter(function(d){return !d.disabled}));
+ var stack1Wrap = g.select('.stack1Wrap')
+ .datum(dataStack1.filter(function(d){return !d.disabled}));
+ var lines2Wrap = g.select('.lines2Wrap')
+ .datum(dataLines2.filter(function(d){return !d.disabled}));
+ var scatters2Wrap = g.select('.scatters2Wrap')
+ .datum(dataScatters2.filter(function(d){return !d.disabled}));
+ var bars2Wrap = g.select('.bars2Wrap')
+ .datum(dataBars2.filter(function(d){return !d.disabled}));
+ var stack2Wrap = g.select('.stack2Wrap')
+ .datum(dataStack2.filter(function(d){return !d.disabled}));
+
+ var extraValue1 = dataStack1.length ? dataStack1.map(function(a){return a.values}).reduce(function(a,b){
+ return a.map(function(aVal,i){return {x: aVal.x, y: aVal.y + b[i].y}})
+ }).concat([{x:0, y:0}]) : [];
+ var extraValue2 = dataStack2.length ? dataStack2.map(function(a){return a.values}).reduce(function(a,b){
+ return a.map(function(aVal,i){return {x: aVal.x, y: aVal.y + b[i].y}})
+ }).concat([{x:0, y:0}]) : [];
+
+ yScale1 .domain(yDomain1 || d3.extent(d3.merge(series1).concat(extraValue1), function(d) { return d.y } ))
+ .range([0, availableHeight]);
+
+ yScale2 .domain(yDomain2 || d3.extent(d3.merge(series2).concat(extraValue2), function(d) { return d.y } ))
+ .range([0, availableHeight]);
+
+ lines1.yDomain(yScale1.domain());
+ scatters1.yDomain(yScale1.domain());
+ bars1.yDomain(yScale1.domain());
+ stack1.yDomain(yScale1.domain());
+
+ lines2.yDomain(yScale2.domain());
+ scatters2.yDomain(yScale2.domain());
+ bars2.yDomain(yScale2.domain());
+ stack2.yDomain(yScale2.domain());
+
+ if(dataStack1.length){d3.transition(stack1Wrap).call(stack1);}
+ if(dataStack2.length){d3.transition(stack2Wrap).call(stack2);}
+
+ if(dataBars1.length){d3.transition(bars1Wrap).call(bars1);}
+ if(dataBars2.length){d3.transition(bars2Wrap).call(bars2);}
+
+ if(dataLines1.length){d3.transition(lines1Wrap).call(lines1);}
+ if(dataLines2.length){d3.transition(lines2Wrap).call(lines2);}
+
+ if(dataScatters1.length){d3.transition(scatters1Wrap).call(scatters1);}
+ if(dataScatters2.length){d3.transition(scatters2Wrap).call(scatters2);}
+
+ xAxis
+ ._ticks( nv.utils.calcTicksX(availableWidth/100, data) )
+ .tickSize(-availableHeight, 0);
+
+ g.select('.nv-x.nv-axis')
+ .attr('transform', 'translate(0,' + availableHeight + ')');
+ d3.transition(g.select('.nv-x.nv-axis'))
+ .call(xAxis);
+
+ yAxis1
+ ._ticks( nv.utils.calcTicksY(availableHeight/36, data) )
+ .tickSize( -availableWidth, 0);
+
+
+ d3.transition(g.select('.nv-y1.nv-axis'))
+ .call(yAxis1);
+
+ yAxis2
+ ._ticks( nv.utils.calcTicksY(availableHeight/36, data) )
+ .tickSize( -availableWidth, 0);
+
+ d3.transition(g.select('.nv-y2.nv-axis'))
+ .call(yAxis2);
+
+ g.select('.nv-y1.nv-axis')
+ .classed('nv-disabled', series1.length ? false : true)
+ .attr('transform', 'translate(' + x.range()[0] + ',0)');
+
+ g.select('.nv-y2.nv-axis')
+ .classed('nv-disabled', series2.length ? false : true)
+ .attr('transform', 'translate(' + x.range()[1] + ',0)');
+
+ legend.dispatch.on('stateChange', function(newState) {
+ chart.update();
+ });
+
+ //============================================================
+ // Event Handling/Dispatching
+ //------------------------------------------------------------
+
+ function mouseover_line(evt) {
+ var yaxis = data[evt.seriesIndex].yAxis === 2 ? yAxis2 : yAxis1;
+ evt.value = evt.point.x;
+ evt.series = {
+ value: evt.point.y,
+ color: evt.point.color
+ };
+ tooltip
+ .duration(100)
+ .valueFormatter(function(d, i) {
+ return yaxis.tickFormat()(d, i);
+ })
+ .data(evt)
+ .position(evt.pos)
+ .hidden(false);
+ }
+
+ function mouseover_scatter(evt) {
+ var yaxis = data[evt.seriesIndex].yAxis === 2 ? yAxis2 : yAxis1;
+ evt.value = evt.point.x;
+ evt.series = {
+ value: evt.point.y,
+ color: evt.point.color
+ };
+ tooltip
+ .duration(100)
+ .valueFormatter(function(d, i) {
+ return yaxis.tickFormat()(d, i);
+ })
+ .data(evt)
+ .position(evt.pos)
+ .hidden(false);
+ }
+
+ function mouseover_stack(evt) {
+ var yaxis = data[evt.seriesIndex].yAxis === 2 ? yAxis2 : yAxis1;
+ evt.point['x'] = stack1.x()(evt.point);
+ evt.point['y'] = stack1.y()(evt.point);
+ tooltip
+ .duration(100)
+ .valueFormatter(function(d, i) {
+ return yaxis.tickFormat()(d, i);
+ })
+ .data(evt)
+ .position(evt.pos)
+ .hidden(false);
+ }
+
+ function mouseover_bar(evt) {
+ var yaxis = data[evt.data.series].yAxis === 2 ? yAxis2 : yAxis1;
+
+ evt.value = bars1.x()(evt.data);
+ evt['series'] = {
+ value: bars1.y()(evt.data),
+ color: evt.color
+ };
+ tooltip
+ .duration(0)
+ .valueFormatter(function(d, i) {
+ return yaxis.tickFormat()(d, i);
+ })
+ .data(evt)
+ .hidden(false);
+ }
+
+ lines1.dispatch.on('elementMouseover.tooltip', mouseover_line);
+ lines2.dispatch.on('elementMouseover.tooltip', mouseover_line);
+ lines1.dispatch.on('elementMouseout.tooltip', function(evt) {
+ tooltip.hidden(true)
+ });
+ lines2.dispatch.on('elementMouseout.tooltip', function(evt) {
+ tooltip.hidden(true)
+ });
+
+ scatters1.dispatch.on('elementMouseover.tooltip', mouseover_scatter);
+ scatters2.dispatch.on('elementMouseover.tooltip', mouseover_scatter);
+ scatters1.dispatch.on('elementMouseout.tooltip', function(evt) {
+ tooltip.hidden(true)
+ });
+ scatters2.dispatch.on('elementMouseout.tooltip', function(evt) {
+ tooltip.hidden(true)
+ });
+
+ stack1.dispatch.on('elementMouseover.tooltip', mouseover_stack);
+ stack2.dispatch.on('elementMouseover.tooltip', mouseover_stack);
+ stack1.dispatch.on('elementMouseout.tooltip', function(evt) {
+ tooltip.hidden(true)
+ });
+ stack2.dispatch.on('elementMouseout.tooltip', function(evt) {
+ tooltip.hidden(true)
+ });
+
+ bars1.dispatch.on('elementMouseover.tooltip', mouseover_bar);
+ bars2.dispatch.on('elementMouseover.tooltip', mouseover_bar);
+
+ bars1.dispatch.on('elementMouseout.tooltip', function(evt) {
+ tooltip.hidden(true);
+ });
+ bars2.dispatch.on('elementMouseout.tooltip', function(evt) {
+ tooltip.hidden(true);
+ });
+ bars1.dispatch.on('elementMousemove.tooltip', function(evt) {
+ tooltip.position({top: d3.event.pageY, left: d3.event.pageX})();
+ });
+ bars2.dispatch.on('elementMousemove.tooltip', function(evt) {
+ tooltip.position({top: d3.event.pageY, left: d3.event.pageX})();
+ });
+
+ });
+
+ return chart;
+ }
+
+ //============================================================
+ // Global getters and setters
+ //------------------------------------------------------------
+
+ chart.dispatch = dispatch;
+ chart.lines1 = lines1;
+ chart.lines2 = lines2;
+ chart.scatters1 = scatters1;
+ chart.scatters2 = scatters2;
+ chart.bars1 = bars1;
+ chart.bars2 = bars2;
+ chart.stack1 = stack1;
+ chart.stack2 = stack2;
+ chart.xAxis = xAxis;
+ chart.yAxis1 = yAxis1;
+ chart.yAxis2 = yAxis2;
+ chart.tooltip = tooltip;
+
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}},
+ yDomain1: {get: function(){return yDomain1;}, set: function(_){yDomain1=_;}},
+ yDomain2: {get: function(){return yDomain2;}, set: function(_){yDomain2=_;}},
+ noData: {get: function(){return noData;}, set: function(_){noData=_;}},
+ interpolate: {get: function(){return interpolate;}, set: function(_){interpolate=_;}},
+
+ // deprecated options
+ tooltips: {get: function(){return tooltip.enabled();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead');
+ tooltip.enabled(!!_);
+ }},
+ tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead');
+ tooltip.contentGenerator(_);
+ }},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ }},
+ x: {get: function(){return getX;}, set: function(_){
+ getX = _;
+ lines1.x(_);
+ lines2.x(_);
+ scatters1.x(_);
+ scatters2.x(_);
+ bars1.x(_);
+ bars2.x(_);
+ stack1.x(_);
+ stack2.x(_);
+ }},
+ y: {get: function(){return getY;}, set: function(_){
+ getY = _;
+ lines1.y(_);
+ lines2.y(_);
+ scatters1.y(_);
+ scatters2.y(_);
+ stack1.y(_);
+ stack2.y(_);
+ bars1.y(_);
+ bars2.y(_);
+ }},
+ useVoronoi: {get: function(){return useVoronoi;}, set: function(_){
+ useVoronoi=_;
+ lines1.useVoronoi(_);
+ lines2.useVoronoi(_);
+ stack1.useVoronoi(_);
+ stack2.useVoronoi(_);
+ }}
+ });
+
+ nv.utils.initOptions(chart);
+
+ return chart;
+ };
+
+
+ nv.models.ohlcBar = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var margin = {top: 0, right: 0, bottom: 0, left: 0}
+ , width = null
+ , height = null
+ , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one
+ , container = null
+ , x = d3.scale.linear()
+ , y = d3.scale.linear()
+ , getX = function(d) { return d.x }
+ , getY = function(d) { return d.y }
+ , getOpen = function(d) { return d.open }
+ , getClose = function(d) { return d.close }
+ , getHigh = function(d) { return d.high }
+ , getLow = function(d) { return d.low }
+ , forceX = []
+ , forceY = []
+ , padData = false // If true, adds half a data points width to front and back, for lining up a line chart with a bar chart
+ , clipEdge = true
+ , color = nv.utils.defaultColor()
+ , interactive = false
+ , xDomain
+ , yDomain
+ , xRange
+ , yRange
+ , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'stateChange', 'changeState', 'renderEnd', 'chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'elementMousemove')
+ ;
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ function chart(selection) {
+ selection.each(function(data) {
+ container = d3.select(this);
+ var availableWidth = nv.utils.availableWidth(width, container, margin),
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+
+ nv.utils.initSVG(container);
+
+ // ohlc bar width.
+ var w = (availableWidth / data[0].values.length) * .9;
+
+ // Setup Scales
+ x.domain(xDomain || d3.extent(data[0].values.map(getX).concat(forceX) ));
+
+ if (padData)
+ x.range(xRange || [availableWidth * .5 / data[0].values.length, availableWidth * (data[0].values.length - .5) / data[0].values.length ]);
+ else
+ x.range(xRange || [5 + w/2, availableWidth - w/2 - 5]);
+
+ y.domain(yDomain || [
+ d3.min(data[0].values.map(getLow).concat(forceY)),
+ d3.max(data[0].values.map(getHigh).concat(forceY))
+ ]
+ ).range(yRange || [availableHeight, 0]);
+
+ // If scale's domain don't have a range, slightly adjust to make one... so a chart can show a single data point
+ if (x.domain()[0] === x.domain()[1])
+ x.domain()[0] ?
+ x.domain([x.domain()[0] - x.domain()[0] * 0.01, x.domain()[1] + x.domain()[1] * 0.01])
+ : x.domain([-1,1]);
+
+ if (y.domain()[0] === y.domain()[1])
+ y.domain()[0] ?
+ y.domain([y.domain()[0] + y.domain()[0] * 0.01, y.domain()[1] - y.domain()[1] * 0.01])
+ : y.domain([-1,1]);
+
+ // Setup containers and skeleton of chart
+ var wrap = d3.select(this).selectAll('g.nv-wrap.nv-ohlcBar').data([data[0].values]);
+ var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-ohlcBar');
+ var defsEnter = wrapEnter.append('defs');
+ var gEnter = wrapEnter.append('g');
+ var g = wrap.select('g');
+
+ gEnter.append('g').attr('class', 'nv-ticks');
+
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ container
+ .on('click', function(d,i) {
+ dispatch.chartClick({
+ data: d,
+ index: i,
+ pos: d3.event,
+ id: id
+ });
+ });
+
+ defsEnter.append('clipPath')
+ .attr('id', 'nv-chart-clip-path-' + id)
+ .append('rect');
+
+ wrap.select('#nv-chart-clip-path-' + id + ' rect')
+ .attr('width', availableWidth)
+ .attr('height', availableHeight);
+
+ g .attr('clip-path', clipEdge ? 'url(#nv-chart-clip-path-' + id + ')' : '');
+
+ var ticks = wrap.select('.nv-ticks').selectAll('.nv-tick')
+ .data(function(d) { return d });
+ ticks.exit().remove();
+
+ ticks.enter().append('path')
+ .attr('class', function(d,i,j) { return (getOpen(d,i) > getClose(d,i) ? 'nv-tick negative' : 'nv-tick positive') + ' nv-tick-' + j + '-' + i })
+ .attr('d', function(d,i) {
+ return 'm0,0l0,'
+ + (y(getOpen(d,i))
+ - y(getHigh(d,i)))
+ + 'l'
+ + (-w/2)
+ + ',0l'
+ + (w/2)
+ + ',0l0,'
+ + (y(getLow(d,i)) - y(getOpen(d,i)))
+ + 'l0,'
+ + (y(getClose(d,i))
+ - y(getLow(d,i)))
+ + 'l'
+ + (w/2)
+ + ',0l'
+ + (-w/2)
+ + ',0z';
+ })
+ .attr('transform', function(d,i) { return 'translate(' + x(getX(d,i)) + ',' + y(getHigh(d,i)) + ')'; })
+ .attr('fill', function(d,i) { return color[0]; })
+ .attr('stroke', function(d,i) { return color[0]; })
+ .attr('x', 0 )
+ .attr('y', function(d,i) { return y(Math.max(0, getY(d,i))) })
+ .attr('height', function(d,i) { return Math.abs(y(getY(d,i)) - y(0)) });
+
+ // the bar colors are controlled by CSS currently
+ ticks.attr('class', function(d,i,j) {
+ return (getOpen(d,i) > getClose(d,i) ? 'nv-tick negative' : 'nv-tick positive') + ' nv-tick-' + j + '-' + i;
+ });
+
+ d3.transition(ticks)
+ .attr('transform', function(d,i) { return 'translate(' + x(getX(d,i)) + ',' + y(getHigh(d,i)) + ')'; })
+ .attr('d', function(d,i) {
+ var w = (availableWidth / data[0].values.length) * .9;
+ return 'm0,0l0,'
+ + (y(getOpen(d,i))
+ - y(getHigh(d,i)))
+ + 'l'
+ + (-w/2)
+ + ',0l'
+ + (w/2)
+ + ',0l0,'
+ + (y(getLow(d,i))
+ - y(getOpen(d,i)))
+ + 'l0,'
+ + (y(getClose(d,i))
+ - y(getLow(d,i)))
+ + 'l'
+ + (w/2)
+ + ',0l'
+ + (-w/2)
+ + ',0z';
+ });
+ });
+
+ return chart;
+ }
+
+
+ //Create methods to allow outside functions to highlight a specific bar.
+ chart.highlightPoint = function(pointIndex, isHoverOver) {
+ chart.clearHighlights();
+ container.select(".nv-ohlcBar .nv-tick-0-" + pointIndex)
+ .classed("hover", isHoverOver)
+ ;
+ };
+
+ chart.clearHighlights = function() {
+ container.select(".nv-ohlcBar .nv-tick.hover")
+ .classed("hover", false)
+ ;
+ };
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ chart.dispatch = dispatch;
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ xScale: {get: function(){return x;}, set: function(_){x=_;}},
+ yScale: {get: function(){return y;}, set: function(_){y=_;}},
+ xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}},
+ yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}},
+ xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}},
+ yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}},
+ forceX: {get: function(){return forceX;}, set: function(_){forceX=_;}},
+ forceY: {get: function(){return forceY;}, set: function(_){forceY=_;}},
+ padData: {get: function(){return padData;}, set: function(_){padData=_;}},
+ clipEdge: {get: function(){return clipEdge;}, set: function(_){clipEdge=_;}},
+ id: {get: function(){return id;}, set: function(_){id=_;}},
+ interactive: {get: function(){return interactive;}, set: function(_){interactive=_;}},
+
+ x: {get: function(){return getX;}, set: function(_){getX=_;}},
+ y: {get: function(){return getY;}, set: function(_){getY=_;}},
+ open: {get: function(){return getOpen();}, set: function(_){getOpen=_;}},
+ close: {get: function(){return getClose();}, set: function(_){getClose=_;}},
+ high: {get: function(){return getHigh;}, set: function(_){getHigh=_;}},
+ low: {get: function(){return getLow;}, set: function(_){getLow=_;}},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top != undefined ? _.top : margin.top;
+ margin.right = _.right != undefined ? _.right : margin.right;
+ margin.bottom = _.bottom != undefined ? _.bottom : margin.bottom;
+ margin.left = _.left != undefined ? _.left : margin.left;
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ }}
+ });
+
+ nv.utils.initOptions(chart);
+ return chart;
+ };
+// Code adapted from Jason Davies' "Parallel Coordinates"
+// http://bl.ocks.org/jasondavies/1341281
+ nv.models.parallelCoordinates = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var margin = {top: 30, right: 0, bottom: 10, left: 0}
+ , width = null
+ , height = null
+ , x = d3.scale.ordinal()
+ , y = {}
+ , dimensionNames = []
+ , dimensionFormats = []
+ , color = nv.utils.defaultColor()
+ , filters = []
+ , active = []
+ , dragging = []
+ , lineTension = 1
+ , dispatch = d3.dispatch('brush', 'elementMouseover', 'elementMouseout')
+ ;
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ function chart(selection) {
+ selection.each(function(data) {
+ var container = d3.select(this);
+ var availableWidth = nv.utils.availableWidth(width, container, margin),
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+
+ nv.utils.initSVG(container);
+
+ active = data; //set all active before first brush call
+
+ // Setup Scales
+ x.rangePoints([0, availableWidth], 1).domain(dimensionNames);
+
+ //Set as true if all values on an axis are missing.
+ var onlyNanValues = {};
+ // Extract the list of dimensions and create a scale for each.
+ dimensionNames.forEach(function(d) {
+ var extent = d3.extent(data, function(p) { return +p[d]; });
+ onlyNanValues[d] = false;
+ //If there is no values to display on an axis, set the extent to 0
+ if (extent[0] === undefined) {
+ onlyNanValues[d] = true;
+ extent[0] = 0;
+ extent[1] = 0;
+ }
+ //Scale axis if there is only one value
+ if (extent[0] === extent[1]) {
+ extent[0] = extent[0] - 1;
+ extent[1] = extent[1] + 1;
+ }
+ //Use 90% of (availableHeight - 12) for the axis range, 12 reprensenting the space necessary to display "undefined values" text.
+ //The remaining 10% are used to display the missingValue line.
+ y[d] = d3.scale.linear()
+ .domain(extent)
+ .range([(availableHeight - 12) * 0.9, 0]);
+
+ y[d].brush = d3.svg.brush().y(y[d]).on('brush', brush);
+
+ return d != 'name';
+ });
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-parallelCoordinates').data([data]);
+ var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-parallelCoordinates');
+ var gEnter = wrapEnter.append('g');
+ var g = wrap.select('g');
+
+ gEnter.append('g').attr('class', 'nv-parallelCoordinates background');
+ gEnter.append('g').attr('class', 'nv-parallelCoordinates foreground');
+ gEnter.append('g').attr('class', 'nv-parallelCoordinates missingValuesline');
+
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ var line = d3.svg.line().interpolate('cardinal').tension(lineTension),
+ axis = d3.svg.axis().orient('left'),
+ axisDrag = d3.behavior.drag()
+ .on('dragstart', dragStart)
+ .on('drag', dragMove)
+ .on('dragend', dragEnd);
+
+ //Add missing value line at the bottom of the chart
+ var missingValuesline, missingValueslineText;
+ var step = x.range()[1] - x.range()[0];
+ var axisWithMissingValues = [];
+ var lineData = [0 + step / 2, availableHeight - 12, availableWidth - step / 2, availableHeight - 12];
+ missingValuesline = wrap.select('.missingValuesline').selectAll('line').data([lineData]);
+ missingValuesline.enter().append('line');
+ missingValuesline.exit().remove();
+ missingValuesline.attr("x1", function(d) { return d[0]; })
+ .attr("y1", function(d) { return d[1]; })
+ .attr("x2", function(d) { return d[2]; })
+ .attr("y2", function(d) { return d[3]; });
+
+ //Add the text "undefined values" under the missing value line
+ missingValueslineText = wrap.select('.missingValuesline').selectAll('text').data(["undefined values"]);
+ missingValueslineText.append('text').data(["undefined values"]);
+ missingValueslineText.enter().append('text');
+ missingValueslineText.exit().remove();
+ missingValueslineText.attr("y", availableHeight)
+ //To have the text right align with the missingValues line, substract 92 representing the text size.
+ .attr("x", availableWidth - 92 - step / 2)
+ .text(function(d) { return d; });
+
+ // Add grey background lines for context.
+ var background = wrap.select('.background').selectAll('path').data(data);
+ background.enter().append('path');
+ background.exit().remove();
+ background.attr('d', path);
+
+ // Add blue foreground lines for focus.
+ var foreground = wrap.select('.foreground').selectAll('path').data(data);
+ foreground.enter().append('path')
+ foreground.exit().remove();
+ foreground.attr('d', path).attr('stroke', color);
+ foreground.on("mouseover", function (d, i) {
+ d3.select(this).classed('hover', true);
+ dispatch.elementMouseover({
+ label: d.name,
+ data: d.data,
+ index: i,
+ pos: [d3.mouse(this.parentNode)[0], d3.mouse(this.parentNode)[1]]
+ });
+
+ });
+ foreground.on("mouseout", function (d, i) {
+ d3.select(this).classed('hover', false);
+ dispatch.elementMouseout({
+ label: d.name,
+ data: d.data,
+ index: i
+ });
+ });
+
+ // Add a group element for each dimension.
+ var dimensions = g.selectAll('.dimension').data(dimensionNames);
+ var dimensionsEnter = dimensions.enter().append('g').attr('class', 'nv-parallelCoordinates dimension');
+ dimensionsEnter.append('g').attr('class', 'nv-parallelCoordinates nv-axis');
+ dimensionsEnter.append('g').attr('class', 'nv-parallelCoordinates-brush');
+ dimensionsEnter.append('text').attr('class', 'nv-parallelCoordinates nv-label');
+
+ dimensions.attr('transform', function(d) { return 'translate(' + x(d) + ',0)'; });
+ dimensions.exit().remove();
+
+ // Add an axis and title.
+ dimensions.select('.nv-label')
+ .style("cursor", "move")
+ .attr('dy', '-1em')
+ .attr('text-anchor', 'middle')
+ .text(String)
+ .on("mouseover", function(d, i) {
+ dispatch.elementMouseover({
+ dim: d,
+ pos: [d3.mouse(this.parentNode.parentNode)[0], d3.mouse(this.parentNode.parentNode)[1]]
+ });
+ })
+ .on("mouseout", function(d, i) {
+ dispatch.elementMouseout({
+ dim: d
+ });
+ })
+ .call(axisDrag);
+
+ dimensions.select('.nv-axis')
+ .each(function (d, i) {
+ d3.select(this).call(axis.scale(y[d]).tickFormat(d3.format(dimensionFormats[i])));
+ });
+
+ dimensions.select('.nv-parallelCoordinates-brush')
+ .each(function (d) {
+ d3.select(this).call(y[d].brush);
+ })
+ .selectAll('rect')
+ .attr('x', -8)
+ .attr('width', 16);
+
+ // Returns the path for a given data point.
+ function path(d) {
+ return line(dimensionNames.map(function (p) {
+ //If value if missing, put the value on the missing value line
+ if(isNaN(d[p]) || isNaN(parseFloat(d[p]))) {
+ var domain = y[p].domain();
+ var range = y[p].range();
+ var min = domain[0] - (domain[1] - domain[0]) / 9;
+
+ //If it's not already the case, allow brush to select undefined values
+ if(axisWithMissingValues.indexOf(p) < 0) {
+
+ var newscale = d3.scale.linear().domain([min, domain[1]]).range([availableHeight - 12, range[1]]);
+ y[p].brush.y(newscale);
+ axisWithMissingValues.push(p);
+ }
+
+ return [x(p), y[p](min)];
+ }
+
+ //If parallelCoordinate contain missing values show the missing values line otherwise, hide it.
+ if(axisWithMissingValues.length > 0) {
+ missingValuesline.style("display", "inline");
+ missingValueslineText.style("display", "inline");
+ } else {
+ missingValuesline.style("display", "none");
+ missingValueslineText.style("display", "none");
+ }
+
+ return [x(p), y[p](d[p])];
+ }));
+ }
+
+ // Handles a brush event, toggling the display of foreground lines.
+ function brush() {
+ var actives = dimensionNames.filter(function(p) { return !y[p].brush.empty(); }),
+ extents = actives.map(function(p) { return y[p].brush.extent(); });
+
+ filters = []; //erase current filters
+ actives.forEach(function(d,i) {
+ filters[i] = {
+ dimension: d,
+ extent: extents[i]
+ }
+ });
+
+ active = []; //erase current active list
+ foreground.style('display', function(d) {
+ var isActive = actives.every(function(p, i) {
+ if(isNaN(d[p]) && extents[i][0] == y[p].brush.y().domain()[0]) return true;
+ return extents[i][0] <= d[p] && d[p] <= extents[i][1];
+ });
+ if (isActive) active.push(d);
+ return isActive ? null : 'none';
+ });
+
+ dispatch.brush({
+ filters: filters,
+ active: active
+ });
+ }
+
+ function dragStart(d, i) {
+ dragging[d] = this.parentNode.__origin__ = x(d);
+ background.attr("visibility", "hidden");
+
+ }
+
+ function dragMove(d, i) {
+ dragging[d] = Math.min(availableWidth, Math.max(0, this.parentNode.__origin__ += d3.event.x));
+ foreground.attr("d", path);
+ dimensionNames.sort(function (a, b) { return position(a) - position(b); });
+ x.domain(dimensionNames);
+ dimensions.attr("transform", function(d) { return "translate(" + position(d) + ")"; });
+ }
+
+ function dragEnd(d, i) {
+ delete this.parentNode.__origin__;
+ delete dragging[d];
+ d3.select(this.parentNode).attr("transform", "translate(" + x(d) + ")");
+ foreground
+ .attr("d", path);
+ background
+ .attr("d", path)
+ .attr("visibility", null);
+
+ }
+
+ function position(d) {
+ var v = dragging[d];
+ return v == null ? x(d) : v;
+ }
+ });
+
+ return chart;
+ }
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ chart.dispatch = dispatch;
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width= _;}},
+ height: {get: function(){return height;}, set: function(_){height= _;}},
+ dimensionNames: {get: function() { return dimensionNames;}, set: function(_){dimensionNames= _;}},
+ dimensionFormats : {get: function(){return dimensionFormats;}, set: function (_){dimensionFormats=_;}},
+ lineTension: {get: function(){return lineTension;}, set: function(_){lineTension = _;}},
+
+ // deprecated options
+ dimensions: {get: function (){return dimensionNames;}, set: function(_){
+ // deprecated after 1.8.1
+ nv.deprecated('dimensions', 'use dimensionNames instead');
+ dimensionNames = _;
+ }},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ }}
+ });
+
+ nv.utils.initOptions(chart);
+ return chart;
+ };
+ nv.models.pie = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var margin = {top: 0, right: 0, bottom: 0, left: 0}
+ , width = 500
+ , height = 500
+ , getX = function(d) { return d.x }
+ , getY = function(d) { return d.y }
+ , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one
+ , container = null
+ , color = nv.utils.defaultColor()
+ , valueFormat = d3.format(',.2f')
+ , showLabels = true
+ , labelsOutside = false
+ , labelType = "key"
+ , labelThreshold = .02 //if slice percentage is under this, don't show label
+ , donut = false
+ , title = false
+ , growOnHover = true
+ , titleOffset = 0
+ , labelSunbeamLayout = false
+ , startAngle = false
+ , padAngle = false
+ , endAngle = false
+ , cornerRadius = 0
+ , donutRatio = 0.5
+ , arcsRadius = []
+ , dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd')
+ ;
+
+ var arcs = [];
+ var arcsOver = [];
+
+ //============================================================
+ // chart function
+ //------------------------------------------------------------
+
+ var renderWatch = nv.utils.renderWatch(dispatch);
+
+ function chart(selection) {
+ renderWatch.reset();
+ selection.each(function(data) {
+ var availableWidth = width - margin.left - margin.right
+ , availableHeight = height - margin.top - margin.bottom
+ , radius = Math.min(availableWidth, availableHeight) / 2
+ , arcsRadiusOuter = []
+ , arcsRadiusInner = []
+ ;
+
+ container = d3.select(this)
+ if (arcsRadius.length === 0) {
+ var outer = radius - radius / 5;
+ var inner = donutRatio * radius;
+ for (var i = 0; i < data[0].length; i++) {
+ arcsRadiusOuter.push(outer);
+ arcsRadiusInner.push(inner);
+ }
+ } else {
+ arcsRadiusOuter = arcsRadius.map(function (d) { return (d.outer - d.outer / 5) * radius; });
+ arcsRadiusInner = arcsRadius.map(function (d) { return (d.inner - d.inner / 5) * radius; });
+ donutRatio = d3.min(arcsRadius.map(function (d) { return (d.inner - d.inner / 5); }));
+ }
+ nv.utils.initSVG(container);
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('.nv-wrap.nv-pie').data(data);
+ var wrapEnter = wrap.enter().append('g').attr('class','nvd3 nv-wrap nv-pie nv-chart-' + id);
+ var gEnter = wrapEnter.append('g');
+ var g = wrap.select('g');
+ var g_pie = gEnter.append('g').attr('class', 'nv-pie');
+ gEnter.append('g').attr('class', 'nv-pieLabels');
+
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+ g.select('.nv-pie').attr('transform', 'translate(' + availableWidth / 2 + ',' + availableHeight / 2 + ')');
+ g.select('.nv-pieLabels').attr('transform', 'translate(' + availableWidth / 2 + ',' + availableHeight / 2 + ')');
+
+ //
+ container.on('click', function(d,i) {
+ dispatch.chartClick({
+ data: d,
+ index: i,
+ pos: d3.event,
+ id: id
+ });
+ });
+
+ arcs = [];
+ arcsOver = [];
+ for (var i = 0; i < data[0].length; i++) {
+
+ var arc = d3.svg.arc().outerRadius(arcsRadiusOuter[i]);
+ var arcOver = d3.svg.arc().outerRadius(arcsRadiusOuter[i] + 5);
+
+ if (startAngle !== false) {
+ arc.startAngle(startAngle);
+ arcOver.startAngle(startAngle);
+ }
+ if (endAngle !== false) {
+ arc.endAngle(endAngle);
+ arcOver.endAngle(endAngle);
+ }
+ if (donut) {
+ arc.innerRadius(arcsRadiusInner[i]);
+ arcOver.innerRadius(arcsRadiusInner[i]);
+ }
+
+ if (arc.cornerRadius && cornerRadius) {
+ arc.cornerRadius(cornerRadius);
+ arcOver.cornerRadius(cornerRadius);
+ }
+
+ arcs.push(arc);
+ arcsOver.push(arcOver);
+ }
+
+ // Setup the Pie chart and choose the data element
+ var pie = d3.layout.pie()
+ .sort(null)
+ .value(function(d) { return d.disabled ? 0 : getY(d) });
+
+ // padAngle added in d3 3.5
+ if (pie.padAngle && padAngle) {
+ pie.padAngle(padAngle);
+ }
+
+ // if title is specified and donut, put it in the middle
+ if (donut && title) {
+ g_pie.append("text").attr('class', 'nv-pie-title');
+
+ wrap.select('.nv-pie-title')
+ .style("text-anchor", "middle")
+ .text(function (d) {
+ return title;
+ })
+ .style("font-size", (Math.min(availableWidth, availableHeight)) * donutRatio * 2 / (title.length + 2) + "px")
+ .attr("dy", "0.35em") // trick to vertically center text
+ .attr('transform', function(d, i) {
+ return 'translate(0, '+ titleOffset + ')';
+ });
+ }
+
+ var slices = wrap.select('.nv-pie').selectAll('.nv-slice').data(pie);
+ var pieLabels = wrap.select('.nv-pieLabels').selectAll('.nv-label').data(pie);
+
+ slices.exit().remove();
+ pieLabels.exit().remove();
+
+ var ae = slices.enter().append('g');
+ ae.attr('class', 'nv-slice');
+ ae.on('mouseover', function(d, i) {
+ d3.select(this).classed('hover', true);
+ if (growOnHover) {
+ d3.select(this).select("path").transition()
+ .duration(70)
+ .attr("d", arcsOver[i]);
+ }
+ dispatch.elementMouseover({
+ data: d.data,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+ });
+ ae.on('mouseout', function(d, i) {
+ d3.select(this).classed('hover', false);
+ if (growOnHover) {
+ d3.select(this).select("path").transition()
+ .duration(50)
+ .attr("d", arcs[i]);
+ }
+ dispatch.elementMouseout({data: d.data, index: i});
+ });
+ ae.on('mousemove', function(d, i) {
+ dispatch.elementMousemove({data: d.data, index: i});
+ });
+ ae.on('click', function(d, i) {
+ dispatch.elementClick({
+ data: d.data,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+ });
+ ae.on('dblclick', function(d, i) {
+ dispatch.elementDblClick({
+ data: d.data,
+ index: i,
+ color: d3.select(this).style("fill")
+ });
+ });
+
+ slices.attr('fill', function(d,i) { return color(d.data, i); });
+ slices.attr('stroke', function(d,i) { return color(d.data, i); });
+
+ var paths = ae.append('path').each(function(d) {
+ this._current = d;
+ });
+
+ slices.select('path')
+ .transition()
+ .attr('d', function (d, i) { return arcs[i](d); })
+ .attrTween('d', arcTween);
+
+ if (showLabels) {
+ // This does the normal label
+ var labelsArc = [];
+ for (var i = 0; i < data[0].length; i++) {
+ labelsArc.push(arcs[i]);
+
+ if (labelsOutside) {
+ if (donut) {
+ labelsArc[i] = d3.svg.arc().outerRadius(arcs[i].outerRadius());
+ if (startAngle !== false) labelsArc[i].startAngle(startAngle);
+ if (endAngle !== false) labelsArc[i].endAngle(endAngle);
+ }
+ } else if (!donut) {
+ labelsArc[i].innerRadius(0);
+ }
+ }
+
+ pieLabels.enter().append("g").classed("nv-label",true).each(function(d,i) {
+ var group = d3.select(this);
+
+ group.attr('transform', function (d, i) {
+ if (labelSunbeamLayout) {
+ d.outerRadius = arcsRadiusOuter[i] + 10; // Set Outer Coordinate
+ d.innerRadius = arcsRadiusOuter[i] + 15; // Set Inner Coordinate
+ var rotateAngle = (d.startAngle + d.endAngle) / 2 * (180 / Math.PI);
+ if ((d.startAngle + d.endAngle) / 2 < Math.PI) {
+ rotateAngle -= 90;
+ } else {
+ rotateAngle += 90;
+ }
+ return 'translate(' + labelsArc[i].centroid(d) + ') rotate(' + rotateAngle + ')';
+ } else {
+ d.outerRadius = radius + 10; // Set Outer Coordinate
+ d.innerRadius = radius + 15; // Set Inner Coordinate
+ return 'translate(' + labelsArc[i].centroid(d) + ')'
+ }
+ });
+
+ group.append('rect')
+ .style('stroke', '#fff')
+ .style('fill', '#fff')
+ .attr("rx", 3)
+ .attr("ry", 3);
+
+ group.append('text')
+ .style('text-anchor', labelSunbeamLayout ? ((d.startAngle + d.endAngle) / 2 < Math.PI ? 'start' : 'end') : 'middle') //center the text on it's origin or begin/end if orthogonal aligned
+ .style('fill', '#000')
+ });
+
+ var labelLocationHash = {};
+ var avgHeight = 14;
+ var avgWidth = 140;
+ var createHashKey = function(coordinates) {
+ return Math.floor(coordinates[0]/avgWidth) * avgWidth + ',' + Math.floor(coordinates[1]/avgHeight) * avgHeight;
+ };
+
+ pieLabels.watchTransition(renderWatch, 'pie labels').attr('transform', function (d, i) {
+ if (labelSunbeamLayout) {
+ d.outerRadius = arcsRadiusOuter[i] + 10; // Set Outer Coordinate
+ d.innerRadius = arcsRadiusOuter[i] + 15; // Set Inner Coordinate
+ var rotateAngle = (d.startAngle + d.endAngle) / 2 * (180 / Math.PI);
+ if ((d.startAngle + d.endAngle) / 2 < Math.PI) {
+ rotateAngle -= 90;
+ } else {
+ rotateAngle += 90;
+ }
+ return 'translate(' + labelsArc[i].centroid(d) + ') rotate(' + rotateAngle + ')';
+ } else {
+ d.outerRadius = radius + 10; // Set Outer Coordinate
+ d.innerRadius = radius + 15; // Set Inner Coordinate
+
+ /*
+ Overlapping pie labels are not good. What this attempts to do is, prevent overlapping.
+ Each label location is hashed, and if a hash collision occurs, we assume an overlap.
+ Adjust the label's y-position to remove the overlap.
+ */
+ var center = labelsArc[i].centroid(d);
+ if (d.value) {
+ var hashKey = createHashKey(center);
+ if (labelLocationHash[hashKey]) {
+ center[1] -= avgHeight;
+ }
+ labelLocationHash[createHashKey(center)] = true;
+ }
+ return 'translate(' + center + ')'
+ }
+ });
+
+ pieLabels.select(".nv-label text")
+ .style('text-anchor', function(d,i) {
+ //center the text on it's origin or begin/end if orthogonal aligned
+ return labelSunbeamLayout ? ((d.startAngle + d.endAngle) / 2 < Math.PI ? 'start' : 'end') : 'middle';
+ })
+ .text(function(d, i) {
+ var percent = (d.endAngle - d.startAngle) / (2 * Math.PI);
+ var label = '';
+ if (!d.value || percent < labelThreshold) return '';
+
+ if(typeof labelType === 'function') {
+ label = labelType(d, i, {
+ 'key': getX(d.data),
+ 'value': getY(d.data),
+ 'percent': valueFormat(percent)
+ });
+ } else {
+ switch (labelType) {
+ case 'key':
+ label = getX(d.data);
+ break;
+ case 'value':
+ label = valueFormat(getY(d.data));
+ break;
+ case 'percent':
+ label = d3.format('%')(percent);
+ break;
+ }
+ }
+ return label;
+ })
+ ;
+ }
+
+
+ // Computes the angle of an arc, converting from radians to degrees.
+ function angle(d) {
+ var a = (d.startAngle + d.endAngle) * 90 / Math.PI - 90;
+ return a > 90 ? a - 180 : a;
+ }
+
+ function arcTween(a, idx) {
+ a.endAngle = isNaN(a.endAngle) ? 0 : a.endAngle;
+ a.startAngle = isNaN(a.startAngle) ? 0 : a.startAngle;
+ if (!donut) a.innerRadius = 0;
+ var i = d3.interpolate(this._current, a);
+ this._current = i(0);
+ return function (t) {
+ return arcs[idx](i(t));
+ };
+ }
+ });
+
+ renderWatch.renderEnd('pie immediate');
+ return chart;
+ }
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ chart.dispatch = dispatch;
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ arcsRadius: { get: function () { return arcsRadius; }, set: function (_) { arcsRadius = _; } },
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ showLabels: {get: function(){return showLabels;}, set: function(_){showLabels=_;}},
+ title: {get: function(){return title;}, set: function(_){title=_;}},
+ titleOffset: {get: function(){return titleOffset;}, set: function(_){titleOffset=_;}},
+ labelThreshold: {get: function(){return labelThreshold;}, set: function(_){labelThreshold=_;}},
+ valueFormat: {get: function(){return valueFormat;}, set: function(_){valueFormat=_;}},
+ x: {get: function(){return getX;}, set: function(_){getX=_;}},
+ id: {get: function(){return id;}, set: function(_){id=_;}},
+ endAngle: {get: function(){return endAngle;}, set: function(_){endAngle=_;}},
+ startAngle: {get: function(){return startAngle;}, set: function(_){startAngle=_;}},
+ padAngle: {get: function(){return padAngle;}, set: function(_){padAngle=_;}},
+ cornerRadius: {get: function(){return cornerRadius;}, set: function(_){cornerRadius=_;}},
+ donutRatio: {get: function(){return donutRatio;}, set: function(_){donutRatio=_;}},
+ labelsOutside: {get: function(){return labelsOutside;}, set: function(_){labelsOutside=_;}},
+ labelSunbeamLayout: {get: function(){return labelSunbeamLayout;}, set: function(_){labelSunbeamLayout=_;}},
+ donut: {get: function(){return donut;}, set: function(_){donut=_;}},
+ growOnHover: {get: function(){return growOnHover;}, set: function(_){growOnHover=_;}},
+
+ // depreciated after 1.7.1
+ pieLabelsOutside: {get: function(){return labelsOutside;}, set: function(_){
+ labelsOutside=_;
+ nv.deprecated('pieLabelsOutside', 'use labelsOutside instead');
+ }},
+ // depreciated after 1.7.1
+ donutLabelsOutside: {get: function(){return labelsOutside;}, set: function(_){
+ labelsOutside=_;
+ nv.deprecated('donutLabelsOutside', 'use labelsOutside instead');
+ }},
+ // deprecated after 1.7.1
+ labelFormat: {get: function(){ return valueFormat;}, set: function(_) {
+ valueFormat=_;
+ nv.deprecated('labelFormat','use valueFormat instead');
+ }},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = typeof _.top != 'undefined' ? _.top : margin.top;
+ margin.right = typeof _.right != 'undefined' ? _.right : margin.right;
+ margin.bottom = typeof _.bottom != 'undefined' ? _.bottom : margin.bottom;
+ margin.left = typeof _.left != 'undefined' ? _.left : margin.left;
+ }},
+ y: {get: function(){return getY;}, set: function(_){
+ getY=d3.functor(_);
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color=nv.utils.getColor(_);
+ }},
+ labelType: {get: function(){return labelType;}, set: function(_){
+ labelType= _ || 'key';
+ }}
+ });
+
+ nv.utils.initOptions(chart);
+ return chart;
+ };
+ nv.models.pieChart = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var pie = nv.models.pie();
+ var legend = nv.models.legend();
+ var tooltip = nv.models.tooltip();
+
+ var margin = {top: 30, right: 20, bottom: 20, left: 20}
+ , width = null
+ , height = null
+ , showLegend = true
+ , legendPosition = "top"
+ , color = nv.utils.defaultColor()
+ , state = nv.utils.state()
+ , defaultState = null
+ , noData = null
+ , duration = 250
+ , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'stateChange', 'changeState','renderEnd')
+ ;
+
+ tooltip
+ .headerEnabled(false)
+ .duration(0)
+ .valueFormatter(function(d, i) {
+ return pie.valueFormat()(d, i);
+ });
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ var renderWatch = nv.utils.renderWatch(dispatch);
+
+ var stateGetter = function(data) {
+ return function(){
+ return {
+ active: data.map(function(d) { return !d.disabled })
+ };
+ }
+ };
+
+ var stateSetter = function(data) {
+ return function(state) {
+ if (state.active !== undefined) {
+ data.forEach(function (series, i) {
+ series.disabled = !state.active[i];
+ });
+ }
+ }
+ };
+
+ //============================================================
+ // Chart function
+ //------------------------------------------------------------
+
+ function chart(selection) {
+ renderWatch.reset();
+ renderWatch.models(pie);
+
+ selection.each(function(data) {
+ var container = d3.select(this);
+ nv.utils.initSVG(container);
+
+ var that = this;
+ var availableWidth = nv.utils.availableWidth(width, container, margin),
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+
+ chart.update = function() { container.transition().call(chart); };
+ chart.container = this;
+
+ state.setter(stateSetter(data), chart.update)
+ .getter(stateGetter(data))
+ .update();
+
+ //set state.disabled
+ state.disabled = data.map(function(d) { return !!d.disabled });
+
+ if (!defaultState) {
+ var key;
+ defaultState = {};
+ for (key in state) {
+ if (state[key] instanceof Array)
+ defaultState[key] = state[key].slice(0);
+ else
+ defaultState[key] = state[key];
+ }
+ }
+
+ // Display No Data message if there's nothing to show.
+ if (!data || !data.length) {
+ nv.utils.noData(chart, container);
+ return chart;
+ } else {
+ container.selectAll('.nv-noData').remove();
+ }
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-pieChart').data([data]);
+ var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-pieChart').append('g');
+ var g = wrap.select('g');
+
+ gEnter.append('g').attr('class', 'nv-pieWrap');
+ gEnter.append('g').attr('class', 'nv-legendWrap');
+
+ // Legend
+ if (showLegend) {
+ if (legendPosition === "top") {
+ legend.width( availableWidth ).key(pie.x());
+
+ wrap.select('.nv-legendWrap')
+ .datum(data)
+ .call(legend);
+
+ if ( margin.top != legend.height()) {
+ margin.top = legend.height();
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+ }
+
+ wrap.select('.nv-legendWrap')
+ .attr('transform', 'translate(0,' + (-margin.top) +')');
+ } else if (legendPosition === "right") {
+ var legendWidth = nv.models.legend().width();
+ if (availableWidth / 2 < legendWidth) {
+ legendWidth = (availableWidth / 2)
+ }
+ legend.height(availableHeight).key(pie.x());
+ legend.width(legendWidth);
+ availableWidth -= legend.width();
+
+ wrap.select('.nv-legendWrap')
+ .datum(data)
+ .call(legend)
+ .attr('transform', 'translate(' + (availableWidth) +',0)');
+ }
+ }
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ // Main Chart Component(s)
+ pie.width(availableWidth).height(availableHeight);
+ var pieWrap = g.select('.nv-pieWrap').datum([data]);
+ d3.transition(pieWrap).call(pie);
+
+ //============================================================
+ // Event Handling/Dispatching (in chart's scope)
+ //------------------------------------------------------------
+
+ legend.dispatch.on('stateChange', function(newState) {
+ for (var key in newState) {
+ state[key] = newState[key];
+ }
+ dispatch.stateChange(state);
+ chart.update();
+ });
+
+ // Update chart from a state object passed to event handler
+ dispatch.on('changeState', function(e) {
+ if (typeof e.disabled !== 'undefined') {
+ data.forEach(function(series,i) {
+ series.disabled = e.disabled[i];
+ });
+ state.disabled = e.disabled;
+ }
+ chart.update();
+ });
+ });
+
+ renderWatch.renderEnd('pieChart immediate');
+ return chart;
+ }
+
+ //============================================================
+ // Event Handling/Dispatching (out of chart's scope)
+ //------------------------------------------------------------
+
+ pie.dispatch.on('elementMouseover.tooltip', function(evt) {
+ evt['series'] = {
+ key: chart.x()(evt.data),
+ value: chart.y()(evt.data),
+ color: evt.color
+ };
+ tooltip.data(evt).hidden(false);
+ });
+
+ pie.dispatch.on('elementMouseout.tooltip', function(evt) {
+ tooltip.hidden(true);
+ });
+
+ pie.dispatch.on('elementMousemove.tooltip', function(evt) {
+ tooltip.position({top: d3.event.pageY, left: d3.event.pageX})();
+ });
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ // expose chart's sub-components
+ chart.legend = legend;
+ chart.dispatch = dispatch;
+ chart.pie = pie;
+ chart.tooltip = tooltip;
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ // use Object get/set functionality to map between vars and chart functions
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ noData: {get: function(){return noData;}, set: function(_){noData=_;}},
+ showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}},
+ legendPosition: {get: function(){return legendPosition;}, set: function(_){legendPosition=_;}},
+ defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}},
+
+ // deprecated options
+ tooltips: {get: function(){return tooltip.enabled();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead');
+ tooltip.enabled(!!_);
+ }},
+ tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead');
+ tooltip.contentGenerator(_);
+ }},
+
+ // options that require extra logic in the setter
+ color: {get: function(){return color;}, set: function(_){
+ color = _;
+ legend.color(color);
+ pie.color(color);
+ }},
+ duration: {get: function(){return duration;}, set: function(_){
+ duration = _;
+ renderWatch.reset(duration);
+ }},
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }}
+ });
+ nv.utils.inheritOptions(chart, pie);
+ nv.utils.initOptions(chart);
+ return chart;
+ };
+
+ nv.models.scatter = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var margin = {top: 0, right: 0, bottom: 0, left: 0}
+ , width = null
+ , height = null
+ , color = nv.utils.defaultColor() // chooses color
+ , id = Math.floor(Math.random() * 100000) //Create semi-unique ID incase user doesn't select one
+ , container = null
+ , x = d3.scale.linear()
+ , y = d3.scale.linear()
+ , z = d3.scale.linear() //linear because d3.svg.shape.size is treated as area
+ , getX = function(d) { return d.x } // accessor to get the x value
+ , getY = function(d) { return d.y } // accessor to get the y value
+ , getSize = function(d) { return d.size || 1} // accessor to get the point size
+ , getShape = function(d) { return d.shape || 'circle' } // accessor to get point shape
+ , forceX = [] // List of numbers to Force into the X scale (ie. 0, or a max / min, etc.)
+ , forceY = [] // List of numbers to Force into the Y scale
+ , forceSize = [] // List of numbers to Force into the Size scale
+ , interactive = true // If true, plots a voronoi overlay for advanced point intersection
+ , pointActive = function(d) { return !d.notActive } // any points that return false will be filtered out
+ , padData = false // If true, adds half a data points width to front and back, for lining up a line chart with a bar chart
+ , padDataOuter = .1 //outerPadding to imitate ordinal scale outer padding
+ , clipEdge = false // if true, masks points within x and y scale
+ , clipVoronoi = true // if true, masks each point with a circle... can turn off to slightly increase performance
+ , showVoronoi = false // display the voronoi areas
+ , clipRadius = function() { return 25 } // function to get the radius for voronoi point clips
+ , xDomain = null // Override x domain (skips the calculation from data)
+ , yDomain = null // Override y domain
+ , xRange = null // Override x range
+ , yRange = null // Override y range
+ , sizeDomain = null // Override point size domain
+ , sizeRange = null
+ , singlePoint = false
+ , dispatch = d3.dispatch('elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'renderEnd')
+ , useVoronoi = true
+ , duration = 250
+ ;
+
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ var x0, y0, z0 // used to store previous scales
+ , timeoutID
+ , needsUpdate = false // Flag for when the points are visually updating, but the interactive layer is behind, to disable tooltips
+ , renderWatch = nv.utils.renderWatch(dispatch, duration)
+ , _sizeRange_def = [16, 256]
+ ;
+
+ function chart(selection) {
+ renderWatch.reset();
+ selection.each(function(data) {
+ container = d3.select(this);
+ var availableWidth = nv.utils.availableWidth(width, container, margin),
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+
+ nv.utils.initSVG(container);
+
+ //add series index to each data point for reference
+ data.forEach(function(series, i) {
+ series.values.forEach(function(point) {
+ point.series = i;
+ });
+ });
+
+ // Setup Scales
+ // remap and flatten the data for use in calculating the scales' domains
+ var seriesData = (xDomain && yDomain && sizeDomain) ? [] : // if we know xDomain and yDomain and sizeDomain, no need to calculate.... if Size is constant remember to set sizeDomain to speed up performance
+ d3.merge(
+ data.map(function(d) {
+ return d.values.map(function(d,i) {
+ return { x: getX(d,i), y: getY(d,i), size: getSize(d,i) }
+ })
+ })
+ );
+
+ x .domain(xDomain || d3.extent(seriesData.map(function(d) { return d.x; }).concat(forceX)))
+
+ if (padData && data[0])
+ x.range(xRange || [(availableWidth * padDataOuter + availableWidth) / (2 *data[0].values.length), availableWidth - availableWidth * (1 + padDataOuter) / (2 * data[0].values.length) ]);
+ //x.range([availableWidth * .5 / data[0].values.length, availableWidth * (data[0].values.length - .5) / data[0].values.length ]);
+ else
+ x.range(xRange || [0, availableWidth]);
+
+ y .domain(yDomain || d3.extent(seriesData.map(function(d) { return d.y }).concat(forceY)))
+ .range(yRange || [availableHeight, 0]);
+
+ z .domain(sizeDomain || d3.extent(seriesData.map(function(d) { return d.size }).concat(forceSize)))
+ .range(sizeRange || _sizeRange_def);
+
+ // If scale's domain don't have a range, slightly adjust to make one... so a chart can show a single data point
+ singlePoint = x.domain()[0] === x.domain()[1] || y.domain()[0] === y.domain()[1];
+
+ if (x.domain()[0] === x.domain()[1])
+ x.domain()[0] ?
+ x.domain([x.domain()[0] - x.domain()[0] * 0.01, x.domain()[1] + x.domain()[1] * 0.01])
+ : x.domain([-1,1]);
+
+ if (y.domain()[0] === y.domain()[1])
+ y.domain()[0] ?
+ y.domain([y.domain()[0] - y.domain()[0] * 0.01, y.domain()[1] + y.domain()[1] * 0.01])
+ : y.domain([-1,1]);
+
+ if ( isNaN(x.domain()[0])) {
+ x.domain([-1,1]);
+ }
+
+ if ( isNaN(y.domain()[0])) {
+ y.domain([-1,1]);
+ }
+
+ x0 = x0 || x;
+ y0 = y0 || y;
+ z0 = z0 || z;
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-scatter').data([data]);
+ var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-scatter nv-chart-' + id);
+ var defsEnter = wrapEnter.append('defs');
+ var gEnter = wrapEnter.append('g');
+ var g = wrap.select('g');
+
+ wrap.classed('nv-single-point', singlePoint);
+ gEnter.append('g').attr('class', 'nv-groups');
+ gEnter.append('g').attr('class', 'nv-point-paths');
+ wrapEnter.append('g').attr('class', 'nv-point-clips');
+
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ defsEnter.append('clipPath')
+ .attr('id', 'nv-edge-clip-' + id)
+ .append('rect');
+
+ wrap.select('#nv-edge-clip-' + id + ' rect')
+ .attr('width', availableWidth)
+ .attr('height', (availableHeight > 0) ? availableHeight : 0);
+
+ g.attr('clip-path', clipEdge ? 'url(#nv-edge-clip-' + id + ')' : '');
+
+ function updateInteractiveLayer() {
+ // Always clear needs-update flag regardless of whether or not
+ // we will actually do anything (avoids needless invocations).
+ needsUpdate = false;
+
+ if (!interactive) return false;
+
+ // inject series and point index for reference into voronoi
+ if (useVoronoi === true) {
+ var vertices = d3.merge(data.map(function(group, groupIndex) {
+ return group.values
+ .map(function(point, pointIndex) {
+ // *Adding noise to make duplicates very unlikely
+ // *Injecting series and point index for reference
+ /* *Adding a 'jitter' to the points, because there's an issue in d3.geom.voronoi.
+ */
+ var pX = getX(point,pointIndex);
+ var pY = getY(point,pointIndex);
+
+ return [x(pX)+ Math.random() * 1e-4,
+ y(pY)+ Math.random() * 1e-4,
+ groupIndex,
+ pointIndex, point]; //temp hack to add noise until I think of a better way so there are no duplicates
+ })
+ .filter(function(pointArray, pointIndex) {
+ return pointActive(pointArray[4], pointIndex); // Issue #237.. move filter to after map, so pointIndex is correct!
+ })
+ })
+ );
+
+ if (vertices.length == 0) return false; // No active points, we're done
+ if (vertices.length < 3) {
+ // Issue #283 - Adding 2 dummy points to the voronoi b/c voronoi requires min 3 points to work
+ vertices.push([x.range()[0] - 20, y.range()[0] - 20, null, null]);
+ vertices.push([x.range()[1] + 20, y.range()[1] + 20, null, null]);
+ vertices.push([x.range()[0] - 20, y.range()[0] + 20, null, null]);
+ vertices.push([x.range()[1] + 20, y.range()[1] - 20, null, null]);
+ }
+
+ // keep voronoi sections from going more than 10 outside of graph
+ // to avoid overlap with other things like legend etc
+ var bounds = d3.geom.polygon([
+ [-10,-10],
+ [-10,height + 10],
+ [width + 10,height + 10],
+ [width + 10,-10]
+ ]);
+
+ var voronoi = d3.geom.voronoi(vertices).map(function(d, i) {
+ return {
+ 'data': bounds.clip(d),
+ 'series': vertices[i][2],
+ 'point': vertices[i][3]
+ }
+ });
+
+ // nuke all voronoi paths on reload and recreate them
+ wrap.select('.nv-point-paths').selectAll('path').remove();
+ var pointPaths = wrap.select('.nv-point-paths').selectAll('path').data(voronoi);
+ var vPointPaths = pointPaths
+ .enter().append("svg:path")
+ .attr("d", function(d) {
+ if (!d || !d.data || d.data.length === 0)
+ return 'M 0 0';
+ else
+ return "M" + d.data.join(",") + "Z";
+ })
+ .attr("id", function(d,i) {
+ return "nv-path-"+i; })
+ .attr("clip-path", function(d,i) { return "url(#nv-clip-"+id+"-"+i+")"; })
+ ;
+
+ // good for debugging point hover issues
+ if (showVoronoi) {
+ vPointPaths.style("fill", d3.rgb(230, 230, 230))
+ .style('fill-opacity', 0.4)
+ .style('stroke-opacity', 1)
+ .style("stroke", d3.rgb(200,200,200));
+ }
+
+ if (clipVoronoi) {
+ // voronoi sections are already set to clip,
+ // just create the circles with the IDs they expect
+ wrap.select('.nv-point-clips').selectAll('clipPath').remove();
+ wrap.select('.nv-point-clips').selectAll("clipPath")
+ .data(vertices)
+ .enter().append("svg:clipPath")
+ .attr("id", function(d, i) { return "nv-clip-"+id+"-"+i;})
+ .append("svg:circle")
+ .attr('cx', function(d) { return d[0]; })
+ .attr('cy', function(d) { return d[1]; })
+ .attr('r', clipRadius);
+ }
+
+ var mouseEventCallback = function(d, mDispatch) {
+ if (needsUpdate) return 0;
+ var series = data[d.series];
+ if (series === undefined) return;
+ var point = series.values[d.point];
+ point['color'] = color(series, d.series);
+
+ // standardize attributes for tooltip.
+ point['x'] = getX(point);
+ point['y'] = getY(point);
+
+ // can't just get box of event node since it's actually a voronoi polygon
+ var box = container.node().getBoundingClientRect();
+ var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
+ var scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
+
+ var pos = {
+ left: x(getX(point, d.point)) + box.left + scrollLeft + margin.left + 10,
+ top: y(getY(point, d.point)) + box.top + scrollTop + margin.top + 10
+ };
+
+ mDispatch({
+ point: point,
+ series: series,
+ pos: pos,
+ seriesIndex: d.series,
+ pointIndex: d.point
+ });
+ };
+
+ pointPaths
+ .on('click', function(d) {
+ mouseEventCallback(d, dispatch.elementClick);
+ })
+ .on('dblclick', function(d) {
+ mouseEventCallback(d, dispatch.elementDblClick);
+ })
+ .on('mouseover', function(d) {
+ mouseEventCallback(d, dispatch.elementMouseover);
+ })
+ .on('mouseout', function(d, i) {
+ mouseEventCallback(d, dispatch.elementMouseout);
+ });
+
+ } else {
+ // add event handlers to points instead voronoi paths
+ wrap.select('.nv-groups').selectAll('.nv-group')
+ .selectAll('.nv-point')
+ //.data(dataWithPoints)
+ //.style('pointer-events', 'auto') // recativate events, disabled by css
+ .on('click', function(d,i) {
+ //nv.log('test', d, i);
+ if (needsUpdate || !data[d.series]) return 0; //check if this is a dummy point
+ var series = data[d.series],
+ point = series.values[i];
+
+ dispatch.elementClick({
+ point: point,
+ series: series,
+ pos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top],
+ seriesIndex: d.series,
+ pointIndex: i
+ });
+ })
+ .on('dblclick', function(d,i) {
+ if (needsUpdate || !data[d.series]) return 0; //check if this is a dummy point
+ var series = data[d.series],
+ point = series.values[i];
+
+ dispatch.elementDblClick({
+ point: point,
+ series: series,
+ pos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top],
+ seriesIndex: d.series,
+ pointIndex: i
+ });
+ })
+ .on('mouseover', function(d,i) {
+ if (needsUpdate || !data[d.series]) return 0; //check if this is a dummy point
+ var series = data[d.series],
+ point = series.values[i];
+
+ dispatch.elementMouseover({
+ point: point,
+ series: series,
+ pos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top],
+ seriesIndex: d.series,
+ pointIndex: i,
+ color: color(d, i)
+ });
+ })
+ .on('mouseout', function(d,i) {
+ if (needsUpdate || !data[d.series]) return 0; //check if this is a dummy point
+ var series = data[d.series],
+ point = series.values[i];
+
+ dispatch.elementMouseout({
+ point: point,
+ series: series,
+ seriesIndex: d.series,
+ pointIndex: i,
+ color: color(d, i)
+ });
+ });
+ }
+ }
+
+ needsUpdate = true;
+ var groups = wrap.select('.nv-groups').selectAll('.nv-group')
+ .data(function(d) { return d }, function(d) { return d.key });
+ groups.enter().append('g')
+ .style('stroke-opacity', 1e-6)
+ .style('fill-opacity', 1e-6);
+ groups.exit()
+ .remove();
+ groups
+ .attr('class', function(d,i) { return 'nv-group nv-series-' + i })
+ .classed('hover', function(d) { return d.hover });
+ groups.watchTransition(renderWatch, 'scatter: groups')
+ .style('fill', function(d,i) { return color(d, i) })
+ .style('stroke', function(d,i) { return color(d, i) })
+ .style('stroke-opacity', 1)
+ .style('fill-opacity', .5);
+
+ // create the points, maintaining their IDs from the original data set
+ var points = groups.selectAll('path.nv-point')
+ .data(function(d) {
+ return d.values.map(
+ function (point, pointIndex) {
+ return [point, pointIndex]
+ }).filter(
+ function(pointArray, pointIndex) {
+ return pointActive(pointArray[0], pointIndex)
+ })
+ });
+ points.enter().append('path')
+ .style('fill', function (d) { return d.color })
+ .style('stroke', function (d) { return d.color })
+ .attr('transform', function(d) {
+ return 'translate(' + x0(getX(d[0],d[1])) + ',' + y0(getY(d[0],d[1])) + ')'
+ })
+ .attr('d',
+ nv.utils.symbol()
+ .type(function(d) { return getShape(d[0]); })
+ .size(function(d) { return z(getSize(d[0],d[1])) })
+ );
+ points.exit().remove();
+ groups.exit().selectAll('path.nv-point')
+ .watchTransition(renderWatch, 'scatter exit')
+ .attr('transform', function(d) {
+ return 'translate(' + x(getX(d[0],d[1])) + ',' + y(getY(d[0],d[1])) + ')'
+ })
+ .remove();
+ points.each(function(d) {
+ d3.select(this)
+ .classed('nv-point', true)
+ .classed('nv-point-' + d[1], true)
+ .classed('nv-noninteractive', !interactive)
+ .classed('hover',false)
+ ;
+ });
+ points
+ .watchTransition(renderWatch, 'scatter points')
+ .attr('transform', function(d) {
+ //nv.log(d, getX(d[0],d[1]), x(getX(d[0],d[1])));
+ return 'translate(' + x(getX(d[0],d[1])) + ',' + y(getY(d[0],d[1])) + ')'
+ })
+ .attr('d',
+ nv.utils.symbol()
+ .type(function(d) { return getShape(d[0]); })
+ .size(function(d) { return z(getSize(d[0],d[1])) })
+ );
+
+ // Delay updating the invisible interactive layer for smoother animation
+ clearTimeout(timeoutID); // stop repeat calls to updateInteractiveLayer
+ timeoutID = setTimeout(updateInteractiveLayer, 300);
+ //updateInteractiveLayer();
+
+ //store old scales for use in transitions on update
+ x0 = x.copy();
+ y0 = y.copy();
+ z0 = z.copy();
+
+ });
+ renderWatch.renderEnd('scatter immediate');
+ return chart;
+ }
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ chart.dispatch = dispatch;
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ // utility function calls provided by this chart
+ chart._calls = new function() {
+ this.clearHighlights = function () {
+ nv.dom.write(function() {
+ container.selectAll(".nv-point.hover").classed("hover", false);
+ });
+ return null;
+ };
+ this.highlightPoint = function (seriesIndex, pointIndex, isHoverOver) {
+ nv.dom.write(function() {
+ container.select(" .nv-series-" + seriesIndex + " .nv-point-" + pointIndex)
+ .classed("hover", isHoverOver);
+ });
+ };
+ };
+
+ // trigger calls from events too
+ dispatch.on('elementMouseover.point', function(d) {
+ if (interactive) chart._calls.highlightPoint(d.seriesIndex,d.pointIndex,true);
+ });
+
+ dispatch.on('elementMouseout.point', function(d) {
+ if (interactive) chart._calls.highlightPoint(d.seriesIndex,d.pointIndex,false);
+ });
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ xScale: {get: function(){return x;}, set: function(_){x=_;}},
+ yScale: {get: function(){return y;}, set: function(_){y=_;}},
+ pointScale: {get: function(){return z;}, set: function(_){z=_;}},
+ xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}},
+ yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}},
+ pointDomain: {get: function(){return sizeDomain;}, set: function(_){sizeDomain=_;}},
+ xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}},
+ yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}},
+ pointRange: {get: function(){return sizeRange;}, set: function(_){sizeRange=_;}},
+ forceX: {get: function(){return forceX;}, set: function(_){forceX=_;}},
+ forceY: {get: function(){return forceY;}, set: function(_){forceY=_;}},
+ forcePoint: {get: function(){return forceSize;}, set: function(_){forceSize=_;}},
+ interactive: {get: function(){return interactive;}, set: function(_){interactive=_;}},
+ pointActive: {get: function(){return pointActive;}, set: function(_){pointActive=_;}},
+ padDataOuter: {get: function(){return padDataOuter;}, set: function(_){padDataOuter=_;}},
+ padData: {get: function(){return padData;}, set: function(_){padData=_;}},
+ clipEdge: {get: function(){return clipEdge;}, set: function(_){clipEdge=_;}},
+ clipVoronoi: {get: function(){return clipVoronoi;}, set: function(_){clipVoronoi=_;}},
+ clipRadius: {get: function(){return clipRadius;}, set: function(_){clipRadius=_;}},
+ showVoronoi: {get: function(){return showVoronoi;}, set: function(_){showVoronoi=_;}},
+ id: {get: function(){return id;}, set: function(_){id=_;}},
+
+
+ // simple functor options
+ x: {get: function(){return getX;}, set: function(_){getX = d3.functor(_);}},
+ y: {get: function(){return getY;}, set: function(_){getY = d3.functor(_);}},
+ pointSize: {get: function(){return getSize;}, set: function(_){getSize = d3.functor(_);}},
+ pointShape: {get: function(){return getShape;}, set: function(_){getShape = d3.functor(_);}},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ duration: {get: function(){return duration;}, set: function(_){
+ duration = _;
+ renderWatch.reset(duration);
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ }},
+ useVoronoi: {get: function(){return useVoronoi;}, set: function(_){
+ useVoronoi = _;
+ if (useVoronoi === false) {
+ clipVoronoi = false;
+ }
+ }}
+ });
+
+ nv.utils.initOptions(chart);
+ return chart;
+ };
+
+ nv.models.scatterChart = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var scatter = nv.models.scatter()
+ , xAxis = nv.models.axis()
+ , yAxis = nv.models.axis()
+ , legend = nv.models.legend()
+ , distX = nv.models.distribution()
+ , distY = nv.models.distribution()
+ , tooltip = nv.models.tooltip()
+ ;
+
+ var margin = {top: 30, right: 20, bottom: 50, left: 75}
+ , width = null
+ , height = null
+ , container = null
+ , color = nv.utils.defaultColor()
+ , x = scatter.xScale()
+ , y = scatter.yScale()
+ , showDistX = false
+ , showDistY = false
+ , showLegend = true
+ , showXAxis = true
+ , showYAxis = true
+ , rightAlignYAxis = false
+ , state = nv.utils.state()
+ , defaultState = null
+ , dispatch = d3.dispatch('stateChange', 'changeState', 'renderEnd')
+ , noData = null
+ , duration = 250
+ ;
+
+ scatter.xScale(x).yScale(y);
+ xAxis.orient('bottom').tickPadding(10);
+ yAxis
+ .orient((rightAlignYAxis) ? 'right' : 'left')
+ .tickPadding(10)
+ ;
+ distX.axis('x');
+ distY.axis('y');
+ tooltip
+ .headerFormatter(function(d, i) {
+ return xAxis.tickFormat()(d, i);
+ })
+ .valueFormatter(function(d, i) {
+ return yAxis.tickFormat()(d, i);
+ });
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ var x0, y0
+ , renderWatch = nv.utils.renderWatch(dispatch, duration);
+
+ var stateGetter = function(data) {
+ return function(){
+ return {
+ active: data.map(function(d) { return !d.disabled })
+ };
+ }
+ };
+
+ var stateSetter = function(data) {
+ return function(state) {
+ if (state.active !== undefined)
+ data.forEach(function(series,i) {
+ series.disabled = !state.active[i];
+ });
+ }
+ };
+
+ function chart(selection) {
+ renderWatch.reset();
+ renderWatch.models(scatter);
+ if (showXAxis) renderWatch.models(xAxis);
+ if (showYAxis) renderWatch.models(yAxis);
+ if (showDistX) renderWatch.models(distX);
+ if (showDistY) renderWatch.models(distY);
+
+ selection.each(function(data) {
+ var that = this;
+
+ container = d3.select(this);
+ nv.utils.initSVG(container);
+
+ var availableWidth = nv.utils.availableWidth(width, container, margin),
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+
+ chart.update = function() {
+ if (duration === 0)
+ container.call(chart);
+ else
+ container.transition().duration(duration).call(chart);
+ };
+ chart.container = this;
+
+ state
+ .setter(stateSetter(data), chart.update)
+ .getter(stateGetter(data))
+ .update();
+
+ // DEPRECATED set state.disableddisabled
+ state.disabled = data.map(function(d) { return !!d.disabled });
+
+ if (!defaultState) {
+ var key;
+ defaultState = {};
+ for (key in state) {
+ if (state[key] instanceof Array)
+ defaultState[key] = state[key].slice(0);
+ else
+ defaultState[key] = state[key];
+ }
+ }
+
+ // Display noData message if there's nothing to show.
+ if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) {
+ nv.utils.noData(chart, container);
+ renderWatch.renderEnd('scatter immediate');
+ return chart;
+ } else {
+ container.selectAll('.nv-noData').remove();
+ }
+
+ // Setup Scales
+ x = scatter.xScale();
+ y = scatter.yScale();
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-scatterChart').data([data]);
+ var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-scatterChart nv-chart-' + scatter.id());
+ var gEnter = wrapEnter.append('g');
+ var g = wrap.select('g');
+
+ // background for pointer events
+ gEnter.append('rect').attr('class', 'nvd3 nv-background').style("pointer-events","none");
+
+ gEnter.append('g').attr('class', 'nv-x nv-axis');
+ gEnter.append('g').attr('class', 'nv-y nv-axis');
+ gEnter.append('g').attr('class', 'nv-scatterWrap');
+ gEnter.append('g').attr('class', 'nv-regressionLinesWrap');
+ gEnter.append('g').attr('class', 'nv-distWrap');
+ gEnter.append('g').attr('class', 'nv-legendWrap');
+
+ if (rightAlignYAxis) {
+ g.select(".nv-y.nv-axis")
+ .attr("transform", "translate(" + availableWidth + ",0)");
+ }
+
+ // Legend
+ if (showLegend) {
+ var legendWidth = availableWidth;
+ legend.width(legendWidth);
+
+ wrap.select('.nv-legendWrap')
+ .datum(data)
+ .call(legend);
+
+ if ( margin.top != legend.height()) {
+ margin.top = legend.height();
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+ }
+
+ wrap.select('.nv-legendWrap')
+ .attr('transform', 'translate(0' + ',' + (-margin.top) +')');
+ }
+
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ // Main Chart Component(s)
+ scatter
+ .width(availableWidth)
+ .height(availableHeight)
+ .color(data.map(function(d,i) {
+ d.color = d.color || color(d, i);
+ return d.color;
+ }).filter(function(d,i) { return !data[i].disabled }));
+
+ wrap.select('.nv-scatterWrap')
+ .datum(data.filter(function(d) { return !d.disabled }))
+ .call(scatter);
+
+
+ wrap.select('.nv-regressionLinesWrap')
+ .attr('clip-path', 'url(#nv-edge-clip-' + scatter.id() + ')');
+
+ var regWrap = wrap.select('.nv-regressionLinesWrap').selectAll('.nv-regLines')
+ .data(function (d) {
+ return d;
+ });
+
+ regWrap.enter().append('g').attr('class', 'nv-regLines');
+
+ var regLine = regWrap.selectAll('.nv-regLine')
+ .data(function (d) {
+ return [d]
+ });
+
+ regLine.enter()
+ .append('line').attr('class', 'nv-regLine')
+ .style('stroke-opacity', 0);
+
+ // don't add lines unless we have slope and intercept to use
+ regLine.filter(function(d) {
+ return d.intercept && d.slope;
+ })
+ .watchTransition(renderWatch, 'scatterPlusLineChart: regline')
+ .attr('x1', x.range()[0])
+ .attr('x2', x.range()[1])
+ .attr('y1', function (d, i) {
+ return y(x.domain()[0] * d.slope + d.intercept)
+ })
+ .attr('y2', function (d, i) {
+ return y(x.domain()[1] * d.slope + d.intercept)
+ })
+ .style('stroke', function (d, i, j) {
+ return color(d, j)
+ })
+ .style('stroke-opacity', function (d, i) {
+ return (d.disabled || typeof d.slope === 'undefined' || typeof d.intercept === 'undefined') ? 0 : 1
+ });
+
+ // Setup Axes
+ if (showXAxis) {
+ xAxis
+ .scale(x)
+ ._ticks( nv.utils.calcTicksX(availableWidth/100, data) )
+ .tickSize( -availableHeight , 0);
+
+ g.select('.nv-x.nv-axis')
+ .attr('transform', 'translate(0,' + y.range()[0] + ')')
+ .call(xAxis);
+ }
+
+ if (showYAxis) {
+ yAxis
+ .scale(y)
+ ._ticks( nv.utils.calcTicksY(availableHeight/36, data) )
+ .tickSize( -availableWidth, 0);
+
+ g.select('.nv-y.nv-axis')
+ .call(yAxis);
+ }
+
+
+ if (showDistX) {
+ distX
+ .getData(scatter.x())
+ .scale(x)
+ .width(availableWidth)
+ .color(data.map(function(d,i) {
+ return d.color || color(d, i);
+ }).filter(function(d,i) { return !data[i].disabled }));
+ gEnter.select('.nv-distWrap').append('g')
+ .attr('class', 'nv-distributionX');
+ g.select('.nv-distributionX')
+ .attr('transform', 'translate(0,' + y.range()[0] + ')')
+ .datum(data.filter(function(d) { return !d.disabled }))
+ .call(distX);
+ }
+
+ if (showDistY) {
+ distY
+ .getData(scatter.y())
+ .scale(y)
+ .width(availableHeight)
+ .color(data.map(function(d,i) {
+ return d.color || color(d, i);
+ }).filter(function(d,i) { return !data[i].disabled }));
+ gEnter.select('.nv-distWrap').append('g')
+ .attr('class', 'nv-distributionY');
+ g.select('.nv-distributionY')
+ .attr('transform', 'translate(' + (rightAlignYAxis ? availableWidth : -distY.size() ) + ',0)')
+ .datum(data.filter(function(d) { return !d.disabled }))
+ .call(distY);
+ }
+
+ //============================================================
+ // Event Handling/Dispatching (in chart's scope)
+ //------------------------------------------------------------
+
+ legend.dispatch.on('stateChange', function(newState) {
+ for (var key in newState)
+ state[key] = newState[key];
+ dispatch.stateChange(state);
+ chart.update();
+ });
+
+ // Update chart from a state object passed to event handler
+ dispatch.on('changeState', function(e) {
+ if (typeof e.disabled !== 'undefined') {
+ data.forEach(function(series,i) {
+ series.disabled = e.disabled[i];
+ });
+ state.disabled = e.disabled;
+ }
+ chart.update();
+ });
+
+ // mouseover needs availableHeight so we just keep scatter mouse events inside the chart block
+ scatter.dispatch.on('elementMouseout.tooltip', function(evt) {
+ tooltip.hidden(true);
+ container.select('.nv-chart-' + scatter.id() + ' .nv-series-' + evt.seriesIndex + ' .nv-distx-' + evt.pointIndex)
+ .attr('y1', 0);
+ container.select('.nv-chart-' + scatter.id() + ' .nv-series-' + evt.seriesIndex + ' .nv-disty-' + evt.pointIndex)
+ .attr('x2', distY.size());
+ });
+
+ scatter.dispatch.on('elementMouseover.tooltip', function(evt) {
+ container.select('.nv-series-' + evt.seriesIndex + ' .nv-distx-' + evt.pointIndex)
+ .attr('y1', evt.pos.top - availableHeight - margin.top);
+ container.select('.nv-series-' + evt.seriesIndex + ' .nv-disty-' + evt.pointIndex)
+ .attr('x2', evt.pos.left + distX.size() - margin.left);
+ tooltip.position(evt.pos).data(evt).hidden(false);
+ });
+
+ //store old scales for use in transitions on update
+ x0 = x.copy();
+ y0 = y.copy();
+
+ });
+
+ renderWatch.renderEnd('scatter with line immediate');
+ return chart;
+ }
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ // expose chart's sub-components
+ chart.dispatch = dispatch;
+ chart.scatter = scatter;
+ chart.legend = legend;
+ chart.xAxis = xAxis;
+ chart.yAxis = yAxis;
+ chart.distX = distX;
+ chart.distY = distY;
+ chart.tooltip = tooltip;
+
+ chart.options = nv.utils.optionsFunc.bind(chart);
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ container: {get: function(){return container;}, set: function(_){container=_;}},
+ showDistX: {get: function(){return showDistX;}, set: function(_){showDistX=_;}},
+ showDistY: {get: function(){return showDistY;}, set: function(_){showDistY=_;}},
+ showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}},
+ showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}},
+ showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}},
+ defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}},
+ noData: {get: function(){return noData;}, set: function(_){noData=_;}},
+ duration: {get: function(){return duration;}, set: function(_){duration=_;}},
+
+ // deprecated options
+ tooltips: {get: function(){return tooltip.enabled();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead');
+ tooltip.enabled(!!_);
+ }},
+ tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead');
+ tooltip.contentGenerator(_);
+ }},
+ tooltipXContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltipContent', 'This option is removed, put values into main tooltip.');
+ }},
+ tooltipYContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltipContent', 'This option is removed, put values into main tooltip.');
+ }},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){
+ rightAlignYAxis = _;
+ yAxis.orient( (_) ? 'right' : 'left');
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ legend.color(color);
+ distX.color(color);
+ distY.color(color);
+ }}
+ });
+
+ nv.utils.inheritOptions(chart, scatter);
+ nv.utils.initOptions(chart);
+ return chart;
+ };
+
+ nv.models.sparkline = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var margin = {top: 2, right: 0, bottom: 2, left: 0}
+ , width = 400
+ , height = 32
+ , container = null
+ , animate = true
+ , x = d3.scale.linear()
+ , y = d3.scale.linear()
+ , getX = function(d) { return d.x }
+ , getY = function(d) { return d.y }
+ , color = nv.utils.getColor(['#000'])
+ , xDomain
+ , yDomain
+ , xRange
+ , yRange
+ ;
+
+ function chart(selection) {
+ selection.each(function(data) {
+ var availableWidth = width - margin.left - margin.right,
+ availableHeight = height - margin.top - margin.bottom;
+
+ container = d3.select(this);
+ nv.utils.initSVG(container);
+
+ // Setup Scales
+ x .domain(xDomain || d3.extent(data, getX ))
+ .range(xRange || [0, availableWidth]);
+
+ y .domain(yDomain || d3.extent(data, getY ))
+ .range(yRange || [availableHeight, 0]);
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-sparkline').data([data]);
+ var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-sparkline');
+ var gEnter = wrapEnter.append('g');
+ var g = wrap.select('g');
+
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
+
+ var paths = wrap.selectAll('path')
+ .data(function(d) { return [d] });
+ paths.enter().append('path');
+ paths.exit().remove();
+ paths
+ .style('stroke', function(d,i) { return d.color || color(d, i) })
+ .attr('d', d3.svg.line()
+ .x(function(d,i) { return x(getX(d,i)) })
+ .y(function(d,i) { return y(getY(d,i)) })
+ );
+
+ // TODO: Add CURRENT data point (Need Min, Mac, Current / Most recent)
+ var points = wrap.selectAll('circle.nv-point')
+ .data(function(data) {
+ var yValues = data.map(function(d, i) { return getY(d,i); });
+ function pointIndex(index) {
+ if (index != -1) {
+ var result = data[index];
+ result.pointIndex = index;
+ return result;
+ } else {
+ return null;
+ }
+ }
+ var maxPoint = pointIndex(yValues.lastIndexOf(y.domain()[1])),
+ minPoint = pointIndex(yValues.indexOf(y.domain()[0])),
+ currentPoint = pointIndex(yValues.length - 1);
+ return [minPoint, maxPoint, currentPoint].filter(function (d) {return d != null;});
+ });
+ points.enter().append('circle');
+ points.exit().remove();
+ points
+ .attr('cx', function(d,i) { return x(getX(d,d.pointIndex)) })
+ .attr('cy', function(d,i) { return y(getY(d,d.pointIndex)) })
+ .attr('r', 2)
+ .attr('class', function(d,i) {
+ return getX(d, d.pointIndex) == x.domain()[1] ? 'nv-point nv-currentValue' :
+ getY(d, d.pointIndex) == y.domain()[0] ? 'nv-point nv-minValue' : 'nv-point nv-maxValue'
+ });
+ });
+
+ return chart;
+ }
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}},
+ yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}},
+ xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}},
+ yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}},
+ xScale: {get: function(){return x;}, set: function(_){x=_;}},
+ yScale: {get: function(){return y;}, set: function(_){y=_;}},
+ animate: {get: function(){return animate;}, set: function(_){animate=_;}},
+
+ //functor options
+ x: {get: function(){return getX;}, set: function(_){getX=d3.functor(_);}},
+ y: {get: function(){return getY;}, set: function(_){getY=d3.functor(_);}},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ }}
+ });
+
+ nv.utils.initOptions(chart);
+ return chart;
+ };
+
+ nv.models.sparklinePlus = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var sparkline = nv.models.sparkline();
+
+ var margin = {top: 15, right: 100, bottom: 10, left: 50}
+ , width = null
+ , height = null
+ , x
+ , y
+ , index = []
+ , paused = false
+ , xTickFormat = d3.format(',r')
+ , yTickFormat = d3.format(',.2f')
+ , showLastValue = true
+ , alignValue = true
+ , rightAlignValue = false
+ , noData = null
+ ;
+
+ function chart(selection) {
+ selection.each(function(data) {
+ var container = d3.select(this);
+ nv.utils.initSVG(container);
+
+ var availableWidth = nv.utils.availableWidth(width, container, margin),
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+
+ chart.update = function() { container.call(chart); };
+ chart.container = this;
+
+ // Display No Data message if there's nothing to show.
+ if (!data || !data.length) {
+ nv.utils.noData(chart, container)
+ return chart;
+ } else {
+ container.selectAll('.nv-noData').remove();
+ }
+
+ var currentValue = sparkline.y()(data[data.length-1], data.length-1);
+
+ // Setup Scales
+ x = sparkline.xScale();
+ y = sparkline.yScale();
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-sparklineplus').data([data]);
+ var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-sparklineplus');
+ var gEnter = wrapEnter.append('g');
+ var g = wrap.select('g');
+
+ gEnter.append('g').attr('class', 'nv-sparklineWrap');
+ gEnter.append('g').attr('class', 'nv-valueWrap');
+ gEnter.append('g').attr('class', 'nv-hoverArea');
+
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ // Main Chart Component(s)
+ var sparklineWrap = g.select('.nv-sparklineWrap');
+
+ sparkline.width(availableWidth).height(availableHeight);
+ sparklineWrap.call(sparkline);
+
+ if (showLastValue) {
+ var valueWrap = g.select('.nv-valueWrap');
+ var value = valueWrap.selectAll('.nv-currentValue')
+ .data([currentValue]);
+
+ value.enter().append('text').attr('class', 'nv-currentValue')
+ .attr('dx', rightAlignValue ? -8 : 8)
+ .attr('dy', '.9em')
+ .style('text-anchor', rightAlignValue ? 'end' : 'start');
+
+ value
+ .attr('x', availableWidth + (rightAlignValue ? margin.right : 0))
+ .attr('y', alignValue ? function (d) {
+ return y(d)
+ } : 0)
+ .style('fill', sparkline.color()(data[data.length - 1], data.length - 1))
+ .text(yTickFormat(currentValue));
+ }
+
+ gEnter.select('.nv-hoverArea').append('rect')
+ .on('mousemove', sparklineHover)
+ .on('click', function() { paused = !paused })
+ .on('mouseout', function() { index = []; updateValueLine(); });
+
+ g.select('.nv-hoverArea rect')
+ .attr('transform', function(d) { return 'translate(' + -margin.left + ',' + -margin.top + ')' })
+ .attr('width', availableWidth + margin.left + margin.right)
+ .attr('height', availableHeight + margin.top);
+
+ //index is currently global (within the chart), may or may not keep it that way
+ function updateValueLine() {
+ if (paused) return;
+
+ var hoverValue = g.selectAll('.nv-hoverValue').data(index);
+
+ var hoverEnter = hoverValue.enter()
+ .append('g').attr('class', 'nv-hoverValue')
+ .style('stroke-opacity', 0)
+ .style('fill-opacity', 0);
+
+ hoverValue.exit()
+ .transition().duration(250)
+ .style('stroke-opacity', 0)
+ .style('fill-opacity', 0)
+ .remove();
+
+ hoverValue
+ .attr('transform', function(d) { return 'translate(' + x(sparkline.x()(data[d],d)) + ',0)' })
+ .transition().duration(250)
+ .style('stroke-opacity', 1)
+ .style('fill-opacity', 1);
+
+ if (!index.length) return;
+
+ hoverEnter.append('line')
+ .attr('x1', 0)
+ .attr('y1', -margin.top)
+ .attr('x2', 0)
+ .attr('y2', availableHeight);
+
+ hoverEnter.append('text').attr('class', 'nv-xValue')
+ .attr('x', -6)
+ .attr('y', -margin.top)
+ .attr('text-anchor', 'end')
+ .attr('dy', '.9em');
+
+ g.select('.nv-hoverValue .nv-xValue')
+ .text(xTickFormat(sparkline.x()(data[index[0]], index[0])));
+
+ hoverEnter.append('text').attr('class', 'nv-yValue')
+ .attr('x', 6)
+ .attr('y', -margin.top)
+ .attr('text-anchor', 'start')
+ .attr('dy', '.9em');
+
+ g.select('.nv-hoverValue .nv-yValue')
+ .text(yTickFormat(sparkline.y()(data[index[0]], index[0])));
+ }
+
+ function sparklineHover() {
+ if (paused) return;
+
+ var pos = d3.mouse(this)[0] - margin.left;
+
+ function getClosestIndex(data, x) {
+ var distance = Math.abs(sparkline.x()(data[0], 0) - x);
+ var closestIndex = 0;
+ for (var i = 0; i < data.length; i++){
+ if (Math.abs(sparkline.x()(data[i], i) - x) < distance) {
+ distance = Math.abs(sparkline.x()(data[i], i) - x);
+ closestIndex = i;
+ }
+ }
+ return closestIndex;
+ }
+
+ index = [getClosestIndex(data, Math.round(x.invert(pos)))];
+ updateValueLine();
+ }
+
+ });
+
+ return chart;
+ }
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ // expose chart's sub-components
+ chart.sparkline = sparkline;
+
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ xTickFormat: {get: function(){return xTickFormat;}, set: function(_){xTickFormat=_;}},
+ yTickFormat: {get: function(){return yTickFormat;}, set: function(_){yTickFormat=_;}},
+ showLastValue: {get: function(){return showLastValue;}, set: function(_){showLastValue=_;}},
+ alignValue: {get: function(){return alignValue;}, set: function(_){alignValue=_;}},
+ rightAlignValue: {get: function(){return rightAlignValue;}, set: function(_){rightAlignValue=_;}},
+ noData: {get: function(){return noData;}, set: function(_){noData=_;}},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }}
+ });
+
+ nv.utils.inheritOptions(chart, sparkline);
+ nv.utils.initOptions(chart);
+
+ return chart;
+ };
+
+ nv.models.stackedArea = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var margin = {top: 0, right: 0, bottom: 0, left: 0}
+ , width = 960
+ , height = 500
+ , color = nv.utils.defaultColor() // a function that computes the color
+ , id = Math.floor(Math.random() * 100000) //Create semi-unique ID incase user doesn't selet one
+ , container = null
+ , getX = function(d) { return d.x } // accessor to get the x value from a data point
+ , getY = function(d) { return d.y } // accessor to get the y value from a data point
+ , style = 'stack'
+ , offset = 'zero'
+ , order = 'default'
+ , interpolate = 'linear' // controls the line interpolation
+ , clipEdge = false // if true, masks lines within x and y scale
+ , x //can be accessed via chart.xScale()
+ , y //can be accessed via chart.yScale()
+ , scatter = nv.models.scatter()
+ , duration = 250
+ , dispatch = d3.dispatch('areaClick', 'areaMouseover', 'areaMouseout','renderEnd', 'elementClick', 'elementMouseover', 'elementMouseout')
+ ;
+
+ scatter
+ .pointSize(2.2) // default size
+ .pointDomain([2.2, 2.2]) // all the same size by default
+ ;
+
+ /************************************
+ * offset:
+ * 'wiggle' (stream)
+ * 'zero' (stacked)
+ * 'expand' (normalize to 100%)
+ * 'silhouette' (simple centered)
+ *
+ * order:
+ * 'inside-out' (stream)
+ * 'default' (input order)
+ ************************************/
+
+ var renderWatch = nv.utils.renderWatch(dispatch, duration);
+
+ function chart(selection) {
+ renderWatch.reset();
+ renderWatch.models(scatter);
+ selection.each(function(data) {
+ var availableWidth = width - margin.left - margin.right,
+ availableHeight = height - margin.top - margin.bottom;
+
+ container = d3.select(this);
+ nv.utils.initSVG(container);
+
+ // Setup Scales
+ x = scatter.xScale();
+ y = scatter.yScale();
+
+ var dataRaw = data;
+ // Injecting point index into each point because d3.layout.stack().out does not give index
+ data.forEach(function(aseries, i) {
+ aseries.seriesIndex = i;
+ aseries.values = aseries.values.map(function(d, j) {
+ d.index = j;
+ d.seriesIndex = i;
+ return d;
+ });
+ });
+
+ var dataFiltered = data.filter(function(series) {
+ return !series.disabled;
+ });
+
+ data = d3.layout.stack()
+ .order(order)
+ .offset(offset)
+ .values(function(d) { return d.values }) //TODO: make values customizeable in EVERY model in this fashion
+ .x(getX)
+ .y(getY)
+ .out(function(d, y0, y) {
+ d.display = {
+ y: y,
+ y0: y0
+ };
+ })
+ (dataFiltered);
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-stackedarea').data([data]);
+ var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-stackedarea');
+ var defsEnter = wrapEnter.append('defs');
+ var gEnter = wrapEnter.append('g');
+ var g = wrap.select('g');
+
+ gEnter.append('g').attr('class', 'nv-areaWrap');
+ gEnter.append('g').attr('class', 'nv-scatterWrap');
+
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ // If the user has not specified forceY, make sure 0 is included in the domain
+ // Otherwise, use user-specified values for forceY
+ if (scatter.forceY().length == 0) {
+ scatter.forceY().push(0);
+ }
+
+ scatter
+ .width(availableWidth)
+ .height(availableHeight)
+ .x(getX)
+ .y(function(d) { return d.display.y + d.display.y0 })
+ .forceY([0])
+ .color(data.map(function(d,i) {
+ return d.color || color(d, d.seriesIndex);
+ }));
+
+ var scatterWrap = g.select('.nv-scatterWrap')
+ .datum(data);
+
+ scatterWrap.call(scatter);
+
+ defsEnter.append('clipPath')
+ .attr('id', 'nv-edge-clip-' + id)
+ .append('rect');
+
+ wrap.select('#nv-edge-clip-' + id + ' rect')
+ .attr('width', availableWidth)
+ .attr('height', availableHeight);
+
+ g.attr('clip-path', clipEdge ? 'url(#nv-edge-clip-' + id + ')' : '');
+
+ var area = d3.svg.area()
+ .x(function(d,i) { return x(getX(d,i)) })
+ .y0(function(d) {
+ return y(d.display.y0)
+ })
+ .y1(function(d) {
+ return y(d.display.y + d.display.y0)
+ })
+ .interpolate(interpolate);
+
+ var zeroArea = d3.svg.area()
+ .x(function(d,i) { return x(getX(d,i)) })
+ .y0(function(d) { return y(d.display.y0) })
+ .y1(function(d) { return y(d.display.y0) });
+
+ var path = g.select('.nv-areaWrap').selectAll('path.nv-area')
+ .data(function(d) { return d });
+
+ path.enter().append('path').attr('class', function(d,i) { return 'nv-area nv-area-' + i })
+ .attr('d', function(d,i){
+ return zeroArea(d.values, d.seriesIndex);
+ })
+ .on('mouseover', function(d,i) {
+ d3.select(this).classed('hover', true);
+ dispatch.areaMouseover({
+ point: d,
+ series: d.key,
+ pos: [d3.event.pageX, d3.event.pageY],
+ seriesIndex: d.seriesIndex
+ });
+ })
+ .on('mouseout', function(d,i) {
+ d3.select(this).classed('hover', false);
+ dispatch.areaMouseout({
+ point: d,
+ series: d.key,
+ pos: [d3.event.pageX, d3.event.pageY],
+ seriesIndex: d.seriesIndex
+ });
+ })
+ .on('click', function(d,i) {
+ d3.select(this).classed('hover', false);
+ dispatch.areaClick({
+ point: d,
+ series: d.key,
+ pos: [d3.event.pageX, d3.event.pageY],
+ seriesIndex: d.seriesIndex
+ });
+ });
+
+ path.exit().remove();
+ path.style('fill', function(d,i){
+ return d.color || color(d, d.seriesIndex)
+ })
+ .style('stroke', function(d,i){ return d.color || color(d, d.seriesIndex) });
+ path.watchTransition(renderWatch,'stackedArea path')
+ .attr('d', function(d,i) {
+ return area(d.values,i)
+ });
+
+ //============================================================
+ // Event Handling/Dispatching (in chart's scope)
+ //------------------------------------------------------------
+
+ scatter.dispatch.on('elementMouseover.area', function(e) {
+ g.select('.nv-chart-' + id + ' .nv-area-' + e.seriesIndex).classed('hover', true);
+ });
+ scatter.dispatch.on('elementMouseout.area', function(e) {
+ g.select('.nv-chart-' + id + ' .nv-area-' + e.seriesIndex).classed('hover', false);
+ });
+
+ //Special offset functions
+ chart.d3_stackedOffset_stackPercent = function(stackData) {
+ var n = stackData.length, //How many series
+ m = stackData[0].length, //how many points per series
+ i,
+ j,
+ o,
+ y0 = [];
+
+ for (j = 0; j < m; ++j) { //Looping through all points
+ for (i = 0, o = 0; i < dataRaw.length; i++) { //looping through all series
+ o += getY(dataRaw[i].values[j]); //total y value of all series at a certian point in time.
+ }
+
+ if (o) for (i = 0; i < n; i++) { //(total y value of all series at point in time i) != 0
+ stackData[i][j][1] /= o;
+ } else { //(total y value of all series at point in time i) == 0
+ for (i = 0; i < n; i++) {
+ stackData[i][j][1] = 0;
+ }
+ }
+ }
+ for (j = 0; j < m; ++j) y0[j] = 0;
+ return y0;
+ };
+
+ });
+
+ renderWatch.renderEnd('stackedArea immediate');
+ return chart;
+ }
+
+ //============================================================
+ // Global getters and setters
+ //------------------------------------------------------------
+
+ chart.dispatch = dispatch;
+ chart.scatter = scatter;
+
+ scatter.dispatch.on('elementClick', function(){ dispatch.elementClick.apply(this, arguments); });
+ scatter.dispatch.on('elementMouseover', function(){ dispatch.elementMouseover.apply(this, arguments); });
+ scatter.dispatch.on('elementMouseout', function(){ dispatch.elementMouseout.apply(this, arguments); });
+
+ chart.interpolate = function(_) {
+ if (!arguments.length) return interpolate;
+ interpolate = _;
+ return chart;
+ };
+
+ chart.duration = function(_) {
+ if (!arguments.length) return duration;
+ duration = _;
+ renderWatch.reset(duration);
+ scatter.duration(duration);
+ return chart;
+ };
+
+ chart.dispatch = dispatch;
+ chart.scatter = scatter;
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ clipEdge: {get: function(){return clipEdge;}, set: function(_){clipEdge=_;}},
+ offset: {get: function(){return offset;}, set: function(_){offset=_;}},
+ order: {get: function(){return order;}, set: function(_){order=_;}},
+ interpolate: {get: function(){return interpolate;}, set: function(_){interpolate=_;}},
+
+ // simple functor options
+ x: {get: function(){return getX;}, set: function(_){getX = d3.functor(_);}},
+ y: {get: function(){return getY;}, set: function(_){getY = d3.functor(_);}},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ }},
+ style: {get: function(){return style;}, set: function(_){
+ style = _;
+ switch (style) {
+ case 'stack':
+ chart.offset('zero');
+ chart.order('default');
+ break;
+ case 'stream':
+ chart.offset('wiggle');
+ chart.order('inside-out');
+ break;
+ case 'stream-center':
+ chart.offset('silhouette');
+ chart.order('inside-out');
+ break;
+ case 'expand':
+ chart.offset('expand');
+ chart.order('default');
+ break;
+ case 'stack_percent':
+ chart.offset(chart.d3_stackedOffset_stackPercent);
+ chart.order('default');
+ break;
+ }
+ }},
+ duration: {get: function(){return duration;}, set: function(_){
+ duration = _;
+ renderWatch.reset(duration);
+ scatter.duration(duration);
+ }}
+ });
+
+ nv.utils.inheritOptions(chart, scatter);
+ nv.utils.initOptions(chart);
+
+ return chart;
+ };
+
+ nv.models.stackedAreaChart = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var stacked = nv.models.stackedArea()
+ , xAxis = nv.models.axis()
+ , yAxis = nv.models.axis()
+ , legend = nv.models.legend()
+ , controls = nv.models.legend()
+ , interactiveLayer = nv.interactiveGuideline()
+ , tooltip = nv.models.tooltip()
+ ;
+
+ var margin = {top: 30, right: 25, bottom: 50, left: 60}
+ , width = null
+ , height = null
+ , color = nv.utils.defaultColor()
+ , showControls = true
+ , showLegend = true
+ , showXAxis = true
+ , showYAxis = true
+ , rightAlignYAxis = false
+ , useInteractiveGuideline = false
+ , x //can be accessed via chart.xScale()
+ , y //can be accessed via chart.yScale()
+ , state = nv.utils.state()
+ , defaultState = null
+ , noData = null
+ , dispatch = d3.dispatch('stateChange', 'changeState','renderEnd')
+ , controlWidth = 250
+ , controlOptions = ['Stacked','Stream','Expanded']
+ , controlLabels = {}
+ , duration = 250
+ ;
+
+ state.style = stacked.style();
+ xAxis.orient('bottom').tickPadding(7);
+ yAxis.orient((rightAlignYAxis) ? 'right' : 'left');
+
+ tooltip
+ .headerFormatter(function(d, i) {
+ return xAxis.tickFormat()(d, i);
+ })
+ .valueFormatter(function(d, i) {
+ return yAxis.tickFormat()(d, i);
+ });
+
+ interactiveLayer.tooltip
+ .headerFormatter(function(d, i) {
+ return xAxis.tickFormat()(d, i);
+ })
+ .valueFormatter(function(d, i) {
+ return yAxis.tickFormat()(d, i);
+ });
+
+ var oldYTickFormat = null,
+ oldValueFormatter = null;
+
+ controls.updateState(false);
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ var renderWatch = nv.utils.renderWatch(dispatch);
+ var style = stacked.style();
+
+ var stateGetter = function(data) {
+ return function(){
+ return {
+ active: data.map(function(d) { return !d.disabled }),
+ style: stacked.style()
+ };
+ }
+ };
+
+ var stateSetter = function(data) {
+ return function(state) {
+ if (state.style !== undefined)
+ style = state.style;
+ if (state.active !== undefined)
+ data.forEach(function(series,i) {
+ series.disabled = !state.active[i];
+ });
+ }
+ };
+
+ var percentFormatter = d3.format('%');
+
+ function chart(selection) {
+ renderWatch.reset();
+ renderWatch.models(stacked);
+ if (showXAxis) renderWatch.models(xAxis);
+ if (showYAxis) renderWatch.models(yAxis);
+
+ selection.each(function(data) {
+ var container = d3.select(this),
+ that = this;
+ nv.utils.initSVG(container);
+
+ var availableWidth = nv.utils.availableWidth(width, container, margin),
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+
+ chart.update = function() { container.transition().duration(duration).call(chart); };
+ chart.container = this;
+
+ state
+ .setter(stateSetter(data), chart.update)
+ .getter(stateGetter(data))
+ .update();
+
+ // DEPRECATED set state.disabled
+ state.disabled = data.map(function(d) { return !!d.disabled });
+
+ if (!defaultState) {
+ var key;
+ defaultState = {};
+ for (key in state) {
+ if (state[key] instanceof Array)
+ defaultState[key] = state[key].slice(0);
+ else
+ defaultState[key] = state[key];
+ }
+ }
+
+ // Display No Data message if there's nothing to show.
+ if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) {
+ nv.utils.noData(chart, container)
+ return chart;
+ } else {
+ container.selectAll('.nv-noData').remove();
+ }
+
+ // Setup Scales
+ x = stacked.xScale();
+ y = stacked.yScale();
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-stackedAreaChart').data([data]);
+ var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-stackedAreaChart').append('g');
+ var g = wrap.select('g');
+
+ gEnter.append("rect").style("opacity",0);
+ gEnter.append('g').attr('class', 'nv-x nv-axis');
+ gEnter.append('g').attr('class', 'nv-y nv-axis');
+ gEnter.append('g').attr('class', 'nv-stackedWrap');
+ gEnter.append('g').attr('class', 'nv-legendWrap');
+ gEnter.append('g').attr('class', 'nv-controlsWrap');
+ gEnter.append('g').attr('class', 'nv-interactive');
+
+ g.select("rect").attr("width",availableWidth).attr("height",availableHeight);
+
+ // Legend
+ if (showLegend) {
+ var legendWidth = (showControls) ? availableWidth - controlWidth : availableWidth;
+
+ legend.width(legendWidth);
+ g.select('.nv-legendWrap').datum(data).call(legend);
+
+ if ( margin.top != legend.height()) {
+ margin.top = legend.height();
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+ }
+
+ g.select('.nv-legendWrap')
+ .attr('transform', 'translate(' + (availableWidth-legendWidth) + ',' + (-margin.top) +')');
+ }
+
+ // Controls
+ if (showControls) {
+ var controlsData = [
+ {
+ key: controlLabels.stacked || 'Stacked',
+ metaKey: 'Stacked',
+ disabled: stacked.style() != 'stack',
+ style: 'stack'
+ },
+ {
+ key: controlLabels.stream || 'Stream',
+ metaKey: 'Stream',
+ disabled: stacked.style() != 'stream',
+ style: 'stream'
+ },
+ {
+ key: controlLabels.expanded || 'Expanded',
+ metaKey: 'Expanded',
+ disabled: stacked.style() != 'expand',
+ style: 'expand'
+ },
+ {
+ key: controlLabels.stack_percent || 'Stack %',
+ metaKey: 'Stack_Percent',
+ disabled: stacked.style() != 'stack_percent',
+ style: 'stack_percent'
+ }
+ ];
+
+ controlWidth = (controlOptions.length/3) * 260;
+ controlsData = controlsData.filter(function(d) {
+ return controlOptions.indexOf(d.metaKey) !== -1;
+ });
+
+ controls
+ .width( controlWidth )
+ .color(['#444', '#444', '#444']);
+
+ g.select('.nv-controlsWrap')
+ .datum(controlsData)
+ .call(controls);
+
+ if ( margin.top != Math.max(controls.height(), legend.height()) ) {
+ margin.top = Math.max(controls.height(), legend.height());
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+ }
+
+ g.select('.nv-controlsWrap')
+ .attr('transform', 'translate(0,' + (-margin.top) +')');
+ }
+
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ if (rightAlignYAxis) {
+ g.select(".nv-y.nv-axis")
+ .attr("transform", "translate(" + availableWidth + ",0)");
+ }
+
+ //Set up interactive layer
+ if (useInteractiveGuideline) {
+ interactiveLayer
+ .width(availableWidth)
+ .height(availableHeight)
+ .margin({left: margin.left, top: margin.top})
+ .svgContainer(container)
+ .xScale(x);
+ wrap.select(".nv-interactive").call(interactiveLayer);
+ }
+
+ stacked
+ .width(availableWidth)
+ .height(availableHeight);
+
+ var stackedWrap = g.select('.nv-stackedWrap')
+ .datum(data);
+
+ stackedWrap.transition().call(stacked);
+
+ // Setup Axes
+ if (showXAxis) {
+ xAxis.scale(x)
+ ._ticks( nv.utils.calcTicksX(availableWidth/100, data) )
+ .tickSize( -availableHeight, 0);
+
+ g.select('.nv-x.nv-axis')
+ .attr('transform', 'translate(0,' + availableHeight + ')');
+
+ g.select('.nv-x.nv-axis')
+ .transition().duration(0)
+ .call(xAxis);
+ }
+
+ if (showYAxis) {
+ var ticks;
+ if (stacked.offset() === 'wiggle') {
+ ticks = 0;
+ }
+ else {
+ ticks = nv.utils.calcTicksY(availableHeight/36, data);
+ }
+ yAxis.scale(y)
+ ._ticks(ticks)
+ .tickSize(-availableWidth, 0);
+
+ if (stacked.style() === 'expand' || stacked.style() === 'stack_percent') {
+ var currentFormat = yAxis.tickFormat();
+
+ if ( !oldYTickFormat || currentFormat !== percentFormatter )
+ oldYTickFormat = currentFormat;
+
+ //Forces the yAxis to use percentage in 'expand' mode.
+ yAxis.tickFormat(percentFormatter);
+ }
+ else {
+ if (oldYTickFormat) {
+ yAxis.tickFormat(oldYTickFormat);
+ oldYTickFormat = null;
+ }
+ }
+
+ g.select('.nv-y.nv-axis')
+ .transition().duration(0)
+ .call(yAxis);
+ }
+
+ //============================================================
+ // Event Handling/Dispatching (in chart's scope)
+ //------------------------------------------------------------
+
+ stacked.dispatch.on('areaClick.toggle', function(e) {
+ if (data.filter(function(d) { return !d.disabled }).length === 1)
+ data.forEach(function(d) {
+ d.disabled = false;
+ });
+ else
+ data.forEach(function(d,i) {
+ d.disabled = (i != e.seriesIndex);
+ });
+
+ state.disabled = data.map(function(d) { return !!d.disabled });
+ dispatch.stateChange(state);
+
+ chart.update();
+ });
+
+ legend.dispatch.on('stateChange', function(newState) {
+ for (var key in newState)
+ state[key] = newState[key];
+ dispatch.stateChange(state);
+ chart.update();
+ });
+
+ controls.dispatch.on('legendClick', function(d,i) {
+ if (!d.disabled) return;
+
+ controlsData = controlsData.map(function(s) {
+ s.disabled = true;
+ return s;
+ });
+ d.disabled = false;
+
+ stacked.style(d.style);
+
+
+ state.style = stacked.style();
+ dispatch.stateChange(state);
+
+ chart.update();
+ });
+
+ interactiveLayer.dispatch.on('elementMousemove', function(e) {
+ stacked.clearHighlights();
+ var singlePoint, pointIndex, pointXLocation, allData = [];
+ data
+ .filter(function(series, i) {
+ series.seriesIndex = i;
+ return !series.disabled;
+ })
+ .forEach(function(series,i) {
+ pointIndex = nv.interactiveBisect(series.values, e.pointXValue, chart.x());
+ var point = series.values[pointIndex];
+ var pointYValue = chart.y()(point, pointIndex);
+ if (pointYValue != null) {
+ stacked.highlightPoint(i, pointIndex, true);
+ }
+ if (typeof point === 'undefined') return;
+ if (typeof singlePoint === 'undefined') singlePoint = point;
+ if (typeof pointXLocation === 'undefined') pointXLocation = chart.xScale()(chart.x()(point,pointIndex));
+
+ //If we are in 'expand' mode, use the stacked percent value instead of raw value.
+ var tooltipValue = (stacked.style() == 'expand') ? point.display.y : chart.y()(point,pointIndex);
+ allData.push({
+ key: series.key,
+ value: tooltipValue,
+ color: color(series,series.seriesIndex),
+ stackedValue: point.display
+ });
+ });
+
+ allData.reverse();
+
+ //Highlight the tooltip entry based on which stack the mouse is closest to.
+ if (allData.length > 2) {
+ var yValue = chart.yScale().invert(e.mouseY);
+ var yDistMax = Infinity, indexToHighlight = null;
+ allData.forEach(function(series,i) {
+
+ //To handle situation where the stacked area chart is negative, we need to use absolute values
+ //when checking if the mouse Y value is within the stack area.
+ yValue = Math.abs(yValue);
+ var stackedY0 = Math.abs(series.stackedValue.y0);
+ var stackedY = Math.abs(series.stackedValue.y);
+ if ( yValue >= stackedY0 && yValue <= (stackedY + stackedY0))
+ {
+ indexToHighlight = i;
+ return;
+ }
+ });
+ if (indexToHighlight != null)
+ allData[indexToHighlight].highlight = true;
+ }
+
+ var xValue = xAxis.tickFormat()(chart.x()(singlePoint,pointIndex));
+
+ var valueFormatter = interactiveLayer.tooltip.valueFormatter();
+ // Keeps track of the tooltip valueFormatter if the chart changes to expanded view
+ if (stacked.style() === 'expand' || stacked.style() === 'stack_percent') {
+ if ( !oldValueFormatter ) {
+ oldValueFormatter = valueFormatter;
+ }
+ //Forces the tooltip to use percentage in 'expand' mode.
+ valueFormatter = d3.format(".1%");
+ }
+ else {
+ if (oldValueFormatter) {
+ valueFormatter = oldValueFormatter;
+ oldValueFormatter = null;
+ }
+ }
+
+ interactiveLayer.tooltip
+ .position({left: pointXLocation + margin.left, top: e.mouseY + margin.top})
+ .chartContainer(that.parentNode)
+ .valueFormatter(valueFormatter)
+ .data(
+ {
+ value: xValue,
+ series: allData
+ }
+ )();
+
+ interactiveLayer.renderGuideLine(pointXLocation);
+
+ });
+
+ interactiveLayer.dispatch.on("elementMouseout",function(e) {
+ stacked.clearHighlights();
+ });
+
+ // Update chart from a state object passed to event handler
+ dispatch.on('changeState', function(e) {
+
+ if (typeof e.disabled !== 'undefined' && data.length === e.disabled.length) {
+ data.forEach(function(series,i) {
+ series.disabled = e.disabled[i];
+ });
+
+ state.disabled = e.disabled;
+ }
+
+ if (typeof e.style !== 'undefined') {
+ stacked.style(e.style);
+ style = e.style;
+ }
+
+ chart.update();
+ });
+
+ });
+
+ renderWatch.renderEnd('stacked Area chart immediate');
+ return chart;
+ }
+
+ //============================================================
+ // Event Handling/Dispatching (out of chart's scope)
+ //------------------------------------------------------------
+
+ stacked.dispatch.on('elementMouseover.tooltip', function(evt) {
+ evt.point['x'] = stacked.x()(evt.point);
+ evt.point['y'] = stacked.y()(evt.point);
+ tooltip.data(evt).position(evt.pos).hidden(false);
+ });
+
+ stacked.dispatch.on('elementMouseout.tooltip', function(evt) {
+ tooltip.hidden(true)
+ });
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ // expose chart's sub-components
+ chart.dispatch = dispatch;
+ chart.stacked = stacked;
+ chart.legend = legend;
+ chart.controls = controls;
+ chart.xAxis = xAxis;
+ chart.yAxis = yAxis;
+ chart.interactiveLayer = interactiveLayer;
+ chart.tooltip = tooltip;
+
+ chart.dispatch = dispatch;
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}},
+ showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}},
+ showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}},
+ defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}},
+ noData: {get: function(){return noData;}, set: function(_){noData=_;}},
+ showControls: {get: function(){return showControls;}, set: function(_){showControls=_;}},
+ controlLabels: {get: function(){return controlLabels;}, set: function(_){controlLabels=_;}},
+ controlOptions: {get: function(){return controlOptions;}, set: function(_){controlOptions=_;}},
+
+ // deprecated options
+ tooltips: {get: function(){return tooltip.enabled();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead');
+ tooltip.enabled(!!_);
+ }},
+ tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){
+ // deprecated after 1.7.1
+ nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead');
+ tooltip.contentGenerator(_);
+ }},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }},
+ duration: {get: function(){return duration;}, set: function(_){
+ duration = _;
+ renderWatch.reset(duration);
+ stacked.duration(duration);
+ xAxis.duration(duration);
+ yAxis.duration(duration);
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color = nv.utils.getColor(_);
+ legend.color(color);
+ stacked.color(color);
+ }},
+ rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){
+ rightAlignYAxis = _;
+ yAxis.orient( rightAlignYAxis ? 'right' : 'left');
+ }},
+ useInteractiveGuideline: {get: function(){return useInteractiveGuideline;}, set: function(_){
+ useInteractiveGuideline = !!_;
+ chart.interactive(!_);
+ chart.useVoronoi(!_);
+ stacked.scatter.interactive(!_);
+ }}
+ });
+
+ nv.utils.inheritOptions(chart, stacked);
+ nv.utils.initOptions(chart);
+
+ return chart;
+ };
+// based on http://bl.ocks.org/kerryrodden/477c1bfb081b783f80ad
+ nv.models.sunburst = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var margin = {top: 0, right: 0, bottom: 0, left: 0}
+ , width = null
+ , height = null
+ , mode = "count"
+ , modes = {count: function(d) { return 1; }, size: function(d) { return d.size }}
+ , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one
+ , container = null
+ , color = nv.utils.defaultColor()
+ , duration = 500
+ , dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMousemove', 'elementMouseover', 'elementMouseout', 'renderEnd')
+ ;
+
+ var x = d3.scale.linear().range([0, 2 * Math.PI]);
+ var y = d3.scale.sqrt();
+
+ var partition = d3.layout.partition()
+ .sort(null)
+ .value(function(d) { return 1; });
+
+ var arc = d3.svg.arc()
+ .startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
+ .endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
+ .innerRadius(function(d) { return Math.max(0, y(d.y)); })
+ .outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)); });
+
+ // Keep track of the current and previous node being displayed as the root.
+ var node, prevNode;
+ // Keep track of the root node
+ var rootNode;
+
+ //============================================================
+ // chart function
+ //------------------------------------------------------------
+
+ var renderWatch = nv.utils.renderWatch(dispatch);
+
+ function chart(selection) {
+ renderWatch.reset();
+ selection.each(function(data) {
+ container = d3.select(this);
+ var availableWidth = nv.utils.availableWidth(width, container, margin);
+ var availableHeight = nv.utils.availableHeight(height, container, margin);
+ var radius = Math.min(availableWidth, availableHeight) / 2;
+ var path;
+
+ nv.utils.initSVG(container);
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('.nv-wrap.nv-sunburst').data(data);
+ var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-sunburst nv-chart-' + id);
+
+ var g = wrapEnter.selectAll('nv-sunburst');
+
+ wrap.attr('transform', 'translate(' + availableWidth / 2 + ',' + availableHeight / 2 + ')');
+
+ container.on('click', function (d, i) {
+ dispatch.chartClick({
+ data: d,
+ index: i,
+ pos: d3.event,
+ id: id
+ });
+ });
+
+ y.range([0, radius]);
+
+ node = node || data;
+ rootNode = data[0];
+ partition.value(modes[mode] || modes["count"]);
+ path = g.data(partition.nodes).enter()
+ .append("path")
+ .attr("d", arc)
+ .style("fill", function (d) {
+ return color((d.children ? d : d.parent).name);
+ })
+ .style("stroke", "#FFF")
+ .on("click", function(d) {
+ if (prevNode !== node && node !== d) prevNode = node;
+ node = d;
+ path.transition()
+ .duration(duration)
+ .attrTween("d", arcTweenZoom(d));
+ })
+ .each(stash)
+ .on("dblclick", function(d) {
+ if (prevNode.parent == d) {
+ path.transition()
+ .duration(duration)
+ .attrTween("d", arcTweenZoom(rootNode));
+ }
+ })
+ .each(stash)
+ .on('mouseover', function(d,i){
+ d3.select(this).classed('hover', true).style('opacity', 0.8);
+ dispatch.elementMouseover({
+ data: d,
+ color: d3.select(this).style("fill")
+ });
+ })
+ .on('mouseout', function(d,i){
+ d3.select(this).classed('hover', false).style('opacity', 1);
+ dispatch.elementMouseout({
+ data: d
+ });
+ })
+ .on('mousemove', function(d,i){
+ dispatch.elementMousemove({
+ data: d
+ });
+ });
+
+
+
+ // Setup for switching data: stash the old values for transition.
+ function stash(d) {
+ d.x0 = d.x;
+ d.dx0 = d.dx;
+ }
+
+ // When switching data: interpolate the arcs in data space.
+ function arcTweenData(a, i) {
+ var oi = d3.interpolate({x: a.x0, dx: a.dx0}, a);
+
+ function tween(t) {
+ var b = oi(t);
+ a.x0 = b.x;
+ a.dx0 = b.dx;
+ return arc(b);
+ }
+
+ if (i == 0) {
+ // If we are on the first arc, adjust the x domain to match the root node
+ // at the current zoom level. (We only need to do this once.)
+ var xd = d3.interpolate(x.domain(), [node.x, node.x + node.dx]);
+ return function (t) {
+ x.domain(xd(t));
+ return tween(t);
+ };
+ } else {
+ return tween;
+ }
+ }
+
+ // When zooming: interpolate the scales.
+ function arcTweenZoom(d) {
+ var xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]),
+ yd = d3.interpolate(y.domain(), [d.y, 1]),
+ yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
+ return function (d, i) {
+ return i
+ ? function (t) {
+ return arc(d);
+ }
+ : function (t) {
+ x.domain(xd(t));
+ y.domain(yd(t)).range(yr(t));
+ return arc(d);
+ };
+ };
+ }
+
+ });
+
+ renderWatch.renderEnd('sunburst immediate');
+ return chart;
+ }
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ chart.dispatch = dispatch;
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ width: {get: function(){return width;}, set: function(_){width=_;}},
+ height: {get: function(){return height;}, set: function(_){height=_;}},
+ mode: {get: function(){return mode;}, set: function(_){mode=_;}},
+ id: {get: function(){return id;}, set: function(_){id=_;}},
+ duration: {get: function(){return duration;}, set: function(_){duration=_;}},
+
+ // options that require extra logic in the setter
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top != undefined ? _.top : margin.top;
+ margin.right = _.right != undefined ? _.right : margin.right;
+ margin.bottom = _.bottom != undefined ? _.bottom : margin.bottom;
+ margin.left = _.left != undefined ? _.left : margin.left;
+ }},
+ color: {get: function(){return color;}, set: function(_){
+ color=nv.utils.getColor(_);
+ }}
+ });
+
+ nv.utils.initOptions(chart);
+ return chart;
+ };
+ nv.models.sunburstChart = function() {
+ "use strict";
+
+ //============================================================
+ // Public Variables with Default Settings
+ //------------------------------------------------------------
+
+ var sunburst = nv.models.sunburst();
+ var tooltip = nv.models.tooltip();
+
+ var margin = {top: 30, right: 20, bottom: 20, left: 20}
+ , width = null
+ , height = null
+ , color = nv.utils.defaultColor()
+ , id = Math.round(Math.random() * 100000)
+ , defaultState = null
+ , noData = null
+ , duration = 250
+ , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'stateChange', 'changeState','renderEnd')
+ ;
+
+ //============================================================
+ // Private Variables
+ //------------------------------------------------------------
+
+ var renderWatch = nv.utils.renderWatch(dispatch);
+ tooltip.headerEnabled(false).duration(0).valueFormatter(function(d, i) {
+ return d;
+ });
+
+ //============================================================
+ // Chart function
+ //------------------------------------------------------------
+
+ function chart(selection) {
+ renderWatch.reset();
+ renderWatch.models(sunburst);
+
+ selection.each(function(data) {
+ var container = d3.select(this);
+ nv.utils.initSVG(container);
+
+ var that = this;
+ var availableWidth = nv.utils.availableWidth(width, container, margin),
+ availableHeight = nv.utils.availableHeight(height, container, margin);
+
+ chart.update = function() {
+ if (duration === 0)
+ container.call(chart);
+ else
+ container.transition().duration(duration).call(chart)
+ };
+ chart.container = this;
+
+ // Display No Data message if there's nothing to show.
+ if (!data || !data.length) {
+ nv.utils.noData(chart, container);
+ return chart;
+ } else {
+ container.selectAll('.nv-noData').remove();
+ }
+
+ // Setup containers and skeleton of chart
+ var wrap = container.selectAll('g.nv-wrap.nv-sunburstChart').data(data);
+ var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-sunburstChart').append('g');
+ var g = wrap.select('g');
+
+ gEnter.append('g').attr('class', 'nv-sunburstWrap');
+
+ wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ // Main Chart Component(s)
+ sunburst.width(availableWidth).height(availableHeight);
+ var sunWrap = g.select('.nv-sunburstWrap').datum(data);
+ d3.transition(sunWrap).call(sunburst);
+
+ });
+
+ renderWatch.renderEnd('sunburstChart immediate');
+ return chart;
+ }
+
+ //============================================================
+ // Event Handling/Dispatching (out of chart's scope)
+ //------------------------------------------------------------
+
+ sunburst.dispatch.on('elementMouseover.tooltip', function(evt) {
+ evt['series'] = {
+ key: evt.data.name,
+ value: evt.data.size,
+ color: evt.color
+ };
+ tooltip.data(evt).hidden(false);
+ });
+
+ sunburst.dispatch.on('elementMouseout.tooltip', function(evt) {
+ tooltip.hidden(true);
+ });
+
+ sunburst.dispatch.on('elementMousemove.tooltip', function(evt) {
+ tooltip.position({top: d3.event.pageY, left: d3.event.pageX})();
+ });
+
+ //============================================================
+ // Expose Public Variables
+ //------------------------------------------------------------
+
+ // expose chart's sub-components
+ chart.dispatch = dispatch;
+ chart.sunburst = sunburst;
+ chart.tooltip = tooltip;
+ chart.options = nv.utils.optionsFunc.bind(chart);
+
+ // use Object get/set functionality to map between vars and chart functions
+ chart._options = Object.create({}, {
+ // simple options, just get/set the necessary values
+ noData: {get: function(){return noData;}, set: function(_){noData=_;}},
+ defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}},
+
+ // options that require extra logic in the setter
+ color: {get: function(){return color;}, set: function(_){
+ color = _;
+ sunburst.color(color);
+ }},
+ duration: {get: function(){return duration;}, set: function(_){
+ duration = _;
+ renderWatch.reset(duration);
+ sunburst.duration(duration);
+ }},
+ margin: {get: function(){return margin;}, set: function(_){
+ margin.top = _.top !== undefined ? _.top : margin.top;
+ margin.right = _.right !== undefined ? _.right : margin.right;
+ margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+ margin.left = _.left !== undefined ? _.left : margin.left;
+ }}
+ });
+ nv.utils.inheritOptions(chart, sunburst);
+ nv.utils.initOptions(chart);
+ return chart;
+ };
+
+ nv.version = "1.8.1";
+})();
\ No newline at end of file
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/analytics/public/js/rickshaw.min.js b/modules/distribution/src/repository/jaggeryapps/iotserver/units/analytics/public/js/rickshaw.min.js
new file mode 100644
index 00000000..bd576991
--- /dev/null
+++ b/modules/distribution/src/repository/jaggeryapps/iotserver/units/analytics/public/js/rickshaw.min.js
@@ -0,0 +1,3 @@
+(function(root,factory){if(typeof define==="function"&&define.amd){define(["d3"],function(d3){return root.Rickshaw=factory(d3)})}else if(typeof exports==="object"){module.exports=factory(require("d3"))}else{root.Rickshaw=factory(d3)}})(this,function(d3){var Rickshaw={namespace:function(namespace,obj){var parts=namespace.split(".");var parent=Rickshaw;for(var i=1,length=parts.length;i
0){var x=s.data[0].x;var y=s.data[0].y;if(typeof x!="number"||typeof y!="number"&&y!==null){throw"x and y properties of points should be numbers instead of "+typeof x+" and "+typeof y}}if(s.data.length>=3){if(s.data[2].xthis.window.xMax)isInRange=false;return isInRange}return true};this.onUpdate=function(callback){this.updateCallbacks.push(callback)};this.onConfigure=function(callback){this.configureCallbacks.push(callback)};this.registerRenderer=function(renderer){this._renderers=this._renderers||{};this._renderers[renderer.name]=renderer};this.configure=function(args){this.config=this.config||{};if(args.width||args.height){this.setSize(args)}Rickshaw.keys(this.defaults).forEach(function(k){this.config[k]=k in args?args[k]:k in this?this[k]:this.defaults[k]},this);Rickshaw.keys(this.config).forEach(function(k){this[k]=this.config[k]},this);if("stack"in args)args.unstack=!args.stack;var renderer=args.renderer||this.renderer&&this.renderer.name||"stack";this.setRenderer(renderer,args);this.configureCallbacks.forEach(function(callback){callback(args)})};this.setRenderer=function(r,args){if(typeof r=="function"){this.renderer=new r({graph:self});this.registerRenderer(this.renderer)}else{if(!this._renderers[r]){throw"couldn't find renderer "+r}this.renderer=this._renderers[r]}if(typeof args=="object"){this.renderer.configure(args)}};this.setSize=function(args){args=args||{};if(typeof window!==undefined){var style=window.getComputedStyle(this.element,null);var elementWidth=parseInt(style.getPropertyValue("width"),10);var elementHeight=parseInt(style.getPropertyValue("height"),10)}this.width=args.width||elementWidth||400;this.height=args.height||elementHeight||250;this.vis&&this.vis.attr("width",this.width).attr("height",this.height)};this.initialize(args)};Rickshaw.namespace("Rickshaw.Fixtures.Color");Rickshaw.Fixtures.Color=function(){this.schemes={};this.schemes.spectrum14=["#ecb796","#dc8f70","#b2a470","#92875a","#716c49","#d2ed82","#bbe468","#a1d05d","#e7cbe6","#d8aad6","#a888c2","#9dc2d3","#649eb9","#387aa3"].reverse();this.schemes.spectrum2000=["#57306f","#514c76","#646583","#738394","#6b9c7d","#84b665","#a7ca50","#bfe746","#e2f528","#fff726","#ecdd00","#d4b11d","#de8800","#de4800","#c91515","#9a0000","#7b0429","#580839","#31082b"];this.schemes.spectrum2001=["#2f243f","#3c2c55","#4a3768","#565270","#6b6b7c","#72957f","#86ad6e","#a1bc5e","#b8d954","#d3e04e","#ccad2a","#cc8412","#c1521d","#ad3821","#8a1010","#681717","#531e1e","#3d1818","#320a1b"];this.schemes.classic9=["#423d4f","#4a6860","#848f39","#a2b73c","#ddcb53","#c5a32f","#7d5836","#963b20","#7c2626","#491d37","#2f254a"].reverse();this.schemes.httpStatus={503:"#ea5029",502:"#d23f14",500:"#bf3613",410:"#efacea",409:"#e291dc",403:"#f457e8",408:"#e121d2",401:"#b92dae",405:"#f47ceb",404:"#a82a9f",400:"#b263c6",301:"#6fa024",302:"#87c32b",307:"#a0d84c",304:"#28b55c",200:"#1a4f74",206:"#27839f",201:"#52adc9",202:"#7c979f",203:"#a5b8bd",204:"#c1cdd1"};this.schemes.colorwheel=["#b5b6a9","#858772","#785f43","#96557e","#4682b4","#65b9ac","#73c03a","#cb513a"].reverse();this.schemes.cool=["#5e9d2f","#73c03a","#4682b4","#7bc3b8","#a9884e","#c1b266","#a47493","#c09fb5"];this.schemes.munin=["#00cc00","#0066b3","#ff8000","#ffcc00","#330099","#990099","#ccff00","#ff0000","#808080","#008f00","#00487d","#b35a00","#b38f00","#6b006b","#8fb300","#b30000","#bebebe","#80ff80","#80c9ff","#ffc080","#ffe680","#aa80ff","#ee00cc","#ff8080","#666600","#ffbfff","#00ffcc","#cc6699","#999900"]};Rickshaw.namespace("Rickshaw.Fixtures.RandomData");Rickshaw.Fixtures.RandomData=function(timeInterval){var addData;timeInterval=timeInterval||1;var lastRandomValue=200;var timeBase=Math.floor((new Date).getTime()/1e3);this.addData=function(data){var randomValue=Math.random()*100+15+lastRandomValue;var index=data[0].length;var counter=1;data.forEach(function(series){var randomVariance=Math.random()*20;var v=randomValue/25+counter++ +(Math.cos(index*counter*11/960)+2)*15+(Math.cos(index/7)+2)*7+(Math.cos(index/17)+2)*1;series.push({x:index*timeInterval+timeBase,y:v+randomVariance})});lastRandomValue=randomValue*.85};this.removeData=function(data){data.forEach(function(series){series.shift()});timeBase+=timeInterval}};Rickshaw.namespace("Rickshaw.Fixtures.Time");Rickshaw.Fixtures.Time=function(){var self=this;this.months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];this.units=[{name:"decade",seconds:86400*365.25*10,formatter:function(d){return parseInt(d.getUTCFullYear()/10,10)*10}},{name:"year",seconds:86400*365.25,formatter:function(d){return d.getUTCFullYear()}},{name:"month",seconds:86400*30.5,formatter:function(d){return self.months[d.getUTCMonth()]}},{name:"week",seconds:86400*7,formatter:function(d){return self.formatDate(d)}},{name:"day",seconds:86400,formatter:function(d){return d.getUTCDate()}},{name:"6 hour",seconds:3600*6,formatter:function(d){return self.formatTime(d)}},{name:"hour",seconds:3600,formatter:function(d){return self.formatTime(d)}},{name:"15 minute",seconds:60*15,formatter:function(d){return self.formatTime(d)}},{name:"minute",seconds:60,formatter:function(d){return d.getUTCMinutes()}},{name:"15 second",seconds:15,formatter:function(d){return d.getUTCSeconds()+"s"}},{name:"second",seconds:1,formatter:function(d){return d.getUTCSeconds()+"s"}},{name:"decisecond",seconds:1/10,formatter:function(d){return d.getUTCMilliseconds()+"ms"}},{name:"centisecond",seconds:1/100,formatter:function(d){return d.getUTCMilliseconds()+"ms"}}];this.unit=function(unitName){return this.units.filter(function(unit){return unitName==unit.name}).shift()};this.formatDate=function(d){return d3.time.format("%b %e")(d)};this.formatTime=function(d){return d.toUTCString().match(/(\d+:\d+):/)[1]};this.ceil=function(time,unit){var date,floor,year;if(unit.name=="month"){date=new Date(time*1e3);floor=Date.UTC(date.getUTCFullYear(),date.getUTCMonth())/1e3;if(floor==time)return time;year=date.getUTCFullYear();var month=date.getUTCMonth();if(month==11){month=0;year=year+1}else{month+=1}return Date.UTC(year,month)/1e3}if(unit.name=="year"){date=new Date(time*1e3);floor=Date.UTC(date.getUTCFullYear(),0)/1e3;if(floor==time)return time;year=date.getUTCFullYear()+1;return Date.UTC(year,0)/1e3}return Math.ceil(time/unit.seconds)*unit.seconds}};Rickshaw.namespace("Rickshaw.Fixtures.Time.Local");Rickshaw.Fixtures.Time.Local=function(){var self=this;this.months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];this.units=[{name:"decade",seconds:86400*365.25*10,formatter:function(d){return parseInt(d.getFullYear()/10,10)*10}},{name:"year",seconds:86400*365.25,formatter:function(d){return d.getFullYear()}},{name:"month",seconds:86400*30.5,formatter:function(d){return self.months[d.getMonth()]}},{name:"week",seconds:86400*7,formatter:function(d){return self.formatDate(d)}},{name:"day",seconds:86400,formatter:function(d){return d.getDate()}},{name:"6 hour",seconds:3600*6,formatter:function(d){return self.formatTime(d)}},{name:"hour",seconds:3600,formatter:function(d){return self.formatTime(d)}},{name:"15 minute",seconds:60*15,formatter:function(d){return self.formatTime(d)}},{name:"minute",seconds:60,formatter:function(d){return d.getMinutes()}},{name:"15 second",seconds:15,formatter:function(d){return d.getSeconds()+"s"}},{name:"second",seconds:1,formatter:function(d){return d.getSeconds()+"s"}},{name:"decisecond",seconds:1/10,formatter:function(d){return d.getMilliseconds()+"ms"}},{name:"centisecond",seconds:1/100,formatter:function(d){return d.getMilliseconds()+"ms"}}];this.unit=function(unitName){return this.units.filter(function(unit){return unitName==unit.name}).shift()};this.formatDate=function(d){return d3.time.format("%b %e")(d)};this.formatTime=function(d){return d.toString().match(/(\d+:\d+):/)[1]};this.ceil=function(time,unit){var date,floor,year;if(unit.name=="day"){var nearFuture=new Date((time+unit.seconds-1)*1e3);var rounded=new Date(0);rounded.setMilliseconds(0);rounded.setSeconds(0);rounded.setMinutes(0);rounded.setHours(0);rounded.setDate(nearFuture.getDate());rounded.setMonth(nearFuture.getMonth());rounded.setFullYear(nearFuture.getFullYear());return rounded.getTime()/1e3}if(unit.name=="month"){date=new Date(time*1e3);floor=new Date(date.getFullYear(),date.getMonth()).getTime()/1e3;if(floor==time)return time;year=date.getFullYear();var month=date.getMonth();if(month==11){month=0;year=year+1}else{month+=1}return new Date(year,month).getTime()/1e3}if(unit.name=="year"){date=new Date(time*1e3);floor=new Date(date.getUTCFullYear(),0).getTime()/1e3;if(floor==time)return time;year=date.getFullYear()+1;return new Date(year,0).getTime()/1e3}return Math.ceil(time/unit.seconds)*unit.seconds}};Rickshaw.namespace("Rickshaw.Fixtures.Number");Rickshaw.Fixtures.Number.formatKMBT=function(y){var abs_y=Math.abs(y);if(abs_y>=1e12){return y/1e12+"T"}else if(abs_y>=1e9){return y/1e9+"B"}else if(abs_y>=1e6){return y/1e6+"M"}else if(abs_y>=1e3){return y/1e3+"K"}else if(abs_y<1&&y>0){return y.toFixed(2)}else if(abs_y===0){return""}else{return y}};Rickshaw.Fixtures.Number.formatBase1024KMGTP=function(y){var abs_y=Math.abs(y);if(abs_y>=0x4000000000000){return y/0x4000000000000+"P"}else if(abs_y>=1099511627776){return y/1099511627776+"T"}else if(abs_y>=1073741824){return y/1073741824+"G"}else if(abs_y>=1048576){return y/1048576+"M"}else if(abs_y>=1024){return y/1024+"K"}else if(abs_y<1&&y>0){return y.toFixed(2)}else if(abs_y===0){return""}else{return y}};Rickshaw.namespace("Rickshaw.Color.Palette");Rickshaw.Color.Palette=function(args){var color=new Rickshaw.Fixtures.Color;args=args||{};this.schemes={};this.scheme=color.schemes[args.scheme]||args.scheme||color.schemes.colorwheel;this.runningIndex=0;this.generatorIndex=0;if(args.interpolatedStopCount){var schemeCount=this.scheme.length-1;var i,j,scheme=[];for(i=0;iself.graph.x.range()[1]){if(annotation.element){annotation.line.classList.add("offscreen");annotation.element.style.display="none"}annotation.boxes.forEach(function(box){if(box.rangeElement)box.rangeElement.classList.add("offscreen")});return}if(!annotation.element){var element=annotation.element=document.createElement("div");element.classList.add("annotation");this.elements.timeline.appendChild(element);element.addEventListener("click",function(e){element.classList.toggle("active");annotation.line.classList.toggle("active");annotation.boxes.forEach(function(box){if(box.rangeElement)box.rangeElement.classList.toggle("active")})},false)}annotation.element.style.left=left+"px";annotation.element.style.display="block";annotation.boxes.forEach(function(box){var element=box.element;if(!element){element=box.element=document.createElement("div");element.classList.add("content");element.innerHTML=box.content;annotation.element.appendChild(element);annotation.line=document.createElement("div");annotation.line.classList.add("annotation_line");self.graph.element.appendChild(annotation.line);if(box.end){box.rangeElement=document.createElement("div");box.rangeElement.classList.add("annotation_range");self.graph.element.appendChild(box.rangeElement)}}if(box.end){var annotationRangeStart=left;var annotationRangeEnd=Math.min(self.graph.x(box.end),self.graph.x.range()[1]);if(annotationRangeStart>annotationRangeEnd){annotationRangeEnd=left;annotationRangeStart=Math.max(self.graph.x(box.end),self.graph.x.range()[0])}var annotationRangeWidth=annotationRangeEnd-annotationRangeStart;box.rangeElement.style.left=annotationRangeStart+"px";box.rangeElement.style.width=annotationRangeWidth+"px";box.rangeElement.classList.remove("offscreen")}annotation.line.classList.remove("offscreen");annotation.line.style.left=left+"px"})},this)};this.graph.onUpdate(function(){self.update()})};Rickshaw.namespace("Rickshaw.Graph.Axis.Time");Rickshaw.Graph.Axis.Time=function(args){var self=this;this.graph=args.graph;this.elements=[];this.ticksTreatment=args.ticksTreatment||"plain";this.fixedTimeUnit=args.timeUnit;var time=args.timeFixture||new Rickshaw.Fixtures.Time;this.appropriateTimeUnit=function(){var unit;var units=time.units;var domain=this.graph.x.domain();var rangeSeconds=domain[1]-domain[0];units.forEach(function(u){if(Math.floor(rangeSeconds/u.seconds)>=2){unit=unit||u}});return unit||time.units[time.units.length-1]};this.tickOffsets=function(){var domain=this.graph.x.domain();var unit=this.fixedTimeUnit||this.appropriateTimeUnit();var count=Math.ceil((domain[1]-domain[0])/unit.seconds);var runningTick=domain[0];var offsets=[];for(var i=0;iself.graph.x.range()[1])return;var element=document.createElement("div");element.style.left=self.graph.x(o.value)+"px";element.classList.add("x_tick");element.classList.add(self.ticksTreatment);var title=document.createElement("div");title.classList.add("title");title.innerHTML=o.unit.formatter(new Date(o.value*1e3));element.appendChild(title);self.graph.element.appendChild(element);self.elements.push(element)})};this.graph.onUpdate(function(){self.render()})};Rickshaw.namespace("Rickshaw.Graph.Axis.X");Rickshaw.Graph.Axis.X=function(args){var self=this;var berthRate=.1;this.initialize=function(args){this.graph=args.graph;this.orientation=args.orientation||"top";this.pixelsPerTick=args.pixelsPerTick||75;if(args.ticks)this.staticTicks=args.ticks;if(args.tickValues)this.tickValues=args.tickValues;this.tickSize=args.tickSize||4;this.ticksTreatment=args.ticksTreatment||"plain";if(args.element){this.element=args.element;this._discoverSize(args.element,args);this.vis=d3.select(args.element).append("svg:svg").attr("height",this.height).attr("width",this.width).attr("class","rickshaw_graph x_axis_d3");this.element=this.vis[0][0];this.element.style.position="relative";this.setSize({width:args.width,height:args.height})}else{this.vis=this.graph.vis}this.graph.onUpdate(function(){self.render()})};this.setSize=function(args){args=args||{};if(!this.element)return;this._discoverSize(this.element.parentNode,args);this.vis.attr("height",this.height).attr("width",this.width*(1+berthRate));var berth=Math.floor(this.width*berthRate/2);this.element.style.left=-1*berth+"px"};this.render=function(){if(this._renderWidth!==undefined&&this.graph.width!==this._renderWidth)this.setSize({auto:true});var axis=d3.svg.axis().scale(this.graph.x).orient(this.orientation);axis.tickFormat(args.tickFormat||function(x){return x});if(this.tickValues)axis.tickValues(this.tickValues);this.ticks=this.staticTicks||Math.floor(this.graph.width/this.pixelsPerTick);var berth=Math.floor(this.width*berthRate/2)||0;var transform;if(this.orientation=="top"){var yOffset=this.height||this.graph.height;transform="translate("+berth+","+yOffset+")"}else{transform="translate("+berth+", 0)"}if(this.element){this.vis.selectAll("*").remove()}this.vis.append("svg:g").attr("class",["x_ticks_d3",this.ticksTreatment].join(" ")).attr("transform",transform).call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));var gridSize=(this.orientation=="bottom"?1:-1)*this.graph.height;this.graph.vis.append("svg:g").attr("class","x_grid_d3").call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize)).selectAll("text").each(function(){this.parentNode.setAttribute("data-x-value",this.textContent)});this._renderHeight=this.graph.height};this._discoverSize=function(element,args){if(typeof window!=="undefined"){var style=window.getComputedStyle(element,null);var elementHeight=parseInt(style.getPropertyValue("height"),10);if(!args.auto){var elementWidth=parseInt(style.getPropertyValue("width"),10)}}this.width=(args.width||elementWidth||this.graph.width)*(1+berthRate);this.height=args.height||elementHeight||40};this.initialize(args)};Rickshaw.namespace("Rickshaw.Graph.Axis.Y");Rickshaw.Graph.Axis.Y=Rickshaw.Class.create({initialize:function(args){this.graph=args.graph;this.orientation=args.orientation||"right";this.pixelsPerTick=args.pixelsPerTick||75;if(args.ticks)this.staticTicks=args.ticks;if(args.tickValues)this.tickValues=args.tickValues;this.tickSize=args.tickSize||4;this.ticksTreatment=args.ticksTreatment||"plain";this.tickFormat=args.tickFormat||function(y){return y};this.berthRate=.1;if(args.element){this.element=args.element;this.vis=d3.select(args.element).append("svg:svg").attr("class","rickshaw_graph y_axis");this.element=this.vis[0][0];this.element.style.position="relative";this.setSize({width:args.width,height:args.height})}else{this.vis=this.graph.vis}var self=this;this.graph.onUpdate(function(){self.render()})},setSize:function(args){args=args||{};if(!this.element)return;if(typeof window!=="undefined"){var style=window.getComputedStyle(this.element.parentNode,null);var elementWidth=parseInt(style.getPropertyValue("width"),10);if(!args.auto){var elementHeight=parseInt(style.getPropertyValue("height"),10)}}this.width=args.width||elementWidth||this.graph.width*this.berthRate;this.height=args.height||elementHeight||this.graph.height;this.vis.attr("width",this.width).attr("height",this.height*(1+this.berthRate));var berth=this.height*this.berthRate;if(this.orientation=="left"){this.element.style.top=-1*berth+"px"}},render:function(){if(this._renderHeight!==undefined&&this.graph.height!==this._renderHeight)this.setSize({auto:true});this.ticks=this.staticTicks||Math.floor(this.graph.height/this.pixelsPerTick);var axis=this._drawAxis(this.graph.y);this._drawGrid(axis);this._renderHeight=this.graph.height},_drawAxis:function(scale){var axis=d3.svg.axis().scale(scale).orient(this.orientation);axis.tickFormat(this.tickFormat);if(this.tickValues)axis.tickValues(this.tickValues);if(this.orientation=="left"){var berth=this.height*this.berthRate;var transform="translate("+this.width+", "+berth+")"}if(this.element){this.vis.selectAll("*").remove()}this.vis.append("svg:g").attr("class",["y_ticks",this.ticksTreatment].join(" ")).attr("transform",transform).call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));return axis},_drawGrid:function(axis){var gridSize=(this.orientation=="right"?1:-1)*this.graph.width;this.graph.vis.append("svg:g").attr("class","y_grid").call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize)).selectAll("text").each(function(){this.parentNode.setAttribute("data-y-value",this.textContent)
+})}});Rickshaw.namespace("Rickshaw.Graph.Axis.Y.Scaled");Rickshaw.Graph.Axis.Y.Scaled=Rickshaw.Class.create(Rickshaw.Graph.Axis.Y,{initialize:function($super,args){if(typeof args.scale==="undefined"){throw new Error("Scaled requires scale")}this.scale=args.scale;if(typeof args.grid==="undefined"){this.grid=true}else{this.grid=args.grid}$super(args)},_drawAxis:function($super,scale){var domain=this.scale.domain();var renderDomain=this.graph.renderer.domain().y;var extents=[Math.min.apply(Math,domain),Math.max.apply(Math,domain)];var extentMap=d3.scale.linear().domain([0,1]).range(extents);var adjExtents=[extentMap(renderDomain[0]),extentMap(renderDomain[1])];var adjustment=d3.scale.linear().domain(extents).range(adjExtents);var adjustedScale=this.scale.copy().domain(domain.map(adjustment)).range(scale.range());return $super(adjustedScale)},_drawGrid:function($super,axis){if(this.grid){$super(axis)}}});Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Highlight");Rickshaw.Graph.Behavior.Series.Highlight=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;var colorSafe={};var activeLine=null;var disabledColor=args.disabledColor||function(seriesColor){return d3.interpolateRgb(seriesColor,d3.rgb("#d8d8d8"))(.8).toString()};this.addHighlightEvents=function(l){l.element.addEventListener("mouseover",function(e){if(activeLine)return;else activeLine=l;self.legend.lines.forEach(function(line){if(l===line){if(self.graph.renderer.unstack&&(line.series.renderer?line.series.renderer.unstack:true)){var seriesIndex=self.graph.series.indexOf(line.series);line.originalIndex=seriesIndex;var series=self.graph.series.splice(seriesIndex,1)[0];self.graph.series.push(series)}return}colorSafe[line.series.name]=colorSafe[line.series.name]||line.series.color;line.series.color=disabledColor(line.series.color)});self.graph.update()},false);l.element.addEventListener("mouseout",function(e){if(!activeLine)return;else activeLine=null;self.legend.lines.forEach(function(line){if(l===line&&line.hasOwnProperty("originalIndex")){var series=self.graph.series.pop();self.graph.series.splice(line.originalIndex,0,series);delete line.originalIndex}if(colorSafe[line.series.name]){line.series.color=colorSafe[line.series.name]}});self.graph.update()},false)};if(this.legend){this.legend.lines.forEach(function(l){self.addHighlightEvents(l)})}};Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Order");Rickshaw.Graph.Behavior.Series.Order=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;if(typeof window.jQuery=="undefined"){throw"couldn't find jQuery at window.jQuery"}if(typeof window.jQuery.ui=="undefined"){throw"couldn't find jQuery UI at window.jQuery.ui"}jQuery(function(){jQuery(self.legend.list).sortable({containment:"parent",tolerance:"pointer",update:function(event,ui){var series=[];jQuery(self.legend.list).find("li").each(function(index,item){if(!item.series)return;series.push(item.series)});for(var i=self.graph.series.length-1;i>=0;i--){self.graph.series[i]=series.shift()}self.graph.update()}});jQuery(self.legend.list).disableSelection()});this.graph.onUpdate(function(){var h=window.getComputedStyle(self.legend.element).height;self.legend.element.style.height=h})};Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Toggle");Rickshaw.Graph.Behavior.Series.Toggle=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;this.addAnchor=function(line){var anchor=document.createElement("a");anchor.innerHTML="✔";anchor.classList.add("action");line.element.insertBefore(anchor,line.element.firstChild);anchor.onclick=function(e){if(line.series.disabled){line.series.enable();line.element.classList.remove("disabled")}else{if(this.graph.series.filter(function(s){return!s.disabled}).length<=1)return;line.series.disable();line.element.classList.add("disabled")}self.graph.update()}.bind(this);var label=line.element.getElementsByTagName("span")[0];label.onclick=function(e){var disableAllOtherLines=line.series.disabled;if(!disableAllOtherLines){for(var i=0;idomainX){dataIndex=Math.abs(domainX-data[i].x)0){alignables.forEach(function(el){el.classList.remove("left");el.classList.add("right")});var rightAlignError=this._calcLayoutError(alignables);if(rightAlignError>leftAlignError){alignables.forEach(function(el){el.classList.remove("right");el.classList.add("left")})}}if(typeof this.onRender=="function"){this.onRender(args)}},_calcLayoutError:function(alignables){var parentRect=this.element.parentNode.getBoundingClientRect();var error=0;var alignRight=alignables.forEach(function(el){var rect=el.getBoundingClientRect();if(!rect.width){return}if(rect.right>parentRect.right){error+=rect.right-parentRect.right}if(rect.left=self.previewWidth){frameAfterDrag[0]-=frameAfterDrag[1]-self.previewWidth;frameAfterDrag[1]=self.previewWidth}}self.graphs.forEach(function(graph){var domainScale=d3.scale.linear().interpolate(d3.interpolateNumber).domain([0,self.previewWidth]).range(graph.dataDomain());var windowAfterDrag=[domainScale(frameAfterDrag[0]),domainScale(frameAfterDrag[1])];self.slideCallbacks.forEach(function(callback){callback(graph,windowAfterDrag[0],windowAfterDrag[1])});if(frameAfterDrag[0]===0){windowAfterDrag[0]=undefined}if(frameAfterDrag[1]===self.previewWidth){windowAfterDrag[1]=undefined}graph.window.xMin=windowAfterDrag[0];graph.window.xMax=windowAfterDrag[1];graph.update()})}function onMousedown(){drag.target=d3.event.target;drag.start=self._getClientXFromEvent(d3.event,drag);self.frameBeforeDrag=self.currentFrame.slice();d3.event.preventDefault?d3.event.preventDefault():d3.event.returnValue=false;d3.select(document).on("mousemove.rickshaw_range_slider_preview",onMousemove);d3.select(document).on("mouseup.rickshaw_range_slider_preview",onMouseup);d3.select(document).on("touchmove.rickshaw_range_slider_preview",onMousemove);d3.select(document).on("touchend.rickshaw_range_slider_preview",onMouseup);d3.select(document).on("touchcancel.rickshaw_range_slider_preview",onMouseup)}function onMousedownLeftHandle(datum,index){drag.left=true;onMousedown()}function onMousedownRightHandle(datum,index){drag.right=true;onMousedown()}function onMousedownMiddleHandle(datum,index){drag.left=true;drag.right=true;drag.rigid=true;onMousedown()}function onMouseup(datum,index){d3.select(document).on("mousemove.rickshaw_range_slider_preview",null);d3.select(document).on("mouseup.rickshaw_range_slider_preview",null);d3.select(document).on("touchmove.rickshaw_range_slider_preview",null);d3.select(document).on("touchend.rickshaw_range_slider_preview",null);d3.select(document).on("touchcancel.rickshaw_range_slider_preview",null);delete self.frameBeforeDrag;drag.left=false;drag.right=false;drag.rigid=false}element.select("rect.left_handle").on("mousedown",onMousedownLeftHandle);element.select("rect.right_handle").on("mousedown",onMousedownRightHandle);element.select("rect.middle_handle").on("mousedown",onMousedownMiddleHandle);element.select("rect.left_handle").on("touchstart",onMousedownLeftHandle);element.select("rect.right_handle").on("touchstart",onMousedownRightHandle);element.select("rect.middle_handle").on("touchstart",onMousedownMiddleHandle)},_getClientXFromEvent:function(event,drag){switch(event.type){case"touchstart":case"touchmove":var touchList=event.changedTouches;var touch=null;for(var touchIndex=0;touchIndexyMax)yMax=y});if(!series.length)return;if(series[0].xxMax)xMax=series[series.length-1].x});xMin-=(xMax-xMin)*this.padding.left;xMax+=(xMax-xMin)*this.padding.right;yMin=this.graph.min==="auto"?yMin:this.graph.min||0;yMax=this.graph.max===undefined?yMax:this.graph.max;if(this.graph.min==="auto"||yMin<0){yMin-=(yMax-yMin)*this.padding.bottom}if(this.graph.max===undefined){yMax+=(yMax-yMin)*this.padding.top}return{x:[xMin,xMax],y:[yMin,yMax]}},render:function(args){args=args||{};var graph=this.graph;var series=args.series||graph.series;var vis=args.vis||graph.vis;vis.selectAll("*").remove();var data=series.filter(function(s){return!s.disabled}).map(function(s){return s.stack});var pathNodes=vis.selectAll("path.path").data(data).enter().append("svg:path").classed("path",true).attr("d",this.seriesPathFactory());if(this.stroke){var strokeNodes=vis.selectAll("path.stroke").data(data).enter().append("svg:path").classed("stroke",true).attr("d",this.seriesStrokeFactory())}var i=0;series.forEach(function(series){if(series.disabled)return;series.path=pathNodes[0][i];if(this.stroke)series.stroke=strokeNodes[0][i];this._styleSeries(series);i++},this)},_styleSeries:function(series){var fill=this.fill?series.color:"none";var stroke=this.stroke?series.color:"none";series.path.setAttribute("fill",fill);series.path.setAttribute("stroke",stroke);series.path.setAttribute("stroke-width",this.strokeWidth);if(series.className){d3.select(series.path).classed(series.className,true)}if(series.className&&this.stroke){d3.select(series.stroke).classed(series.className,true)}},configure:function(args){args=args||{};Rickshaw.keys(this.defaults()).forEach(function(key){if(!args.hasOwnProperty(key)){this[key]=this[key]||this.graph[key]||this.defaults()[key];return}if(typeof this.defaults()[key]=="object"){Rickshaw.keys(this.defaults()[key]).forEach(function(k){this[key][k]=args[key][k]!==undefined?args[key][k]:this[key][k]!==undefined?this[key][k]:this.defaults()[key][k]},this)}else{this[key]=args[key]!==undefined?args[key]:this[key]!==undefined?this[key]:this.graph[key]!==undefined?this.graph[key]:this.defaults()[key]}},this)},setStrokeWidth:function(strokeWidth){if(strokeWidth!==undefined){this.strokeWidth=strokeWidth}},setTension:function(tension){if(tension!==undefined){this.tension=tension}}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Line");Rickshaw.Graph.Renderer.Line=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"line",defaults:function($super){return Rickshaw.extend($super(),{unstack:true,fill:false,stroke:true})},seriesPathFactory:function(){var graph=this.graph;var factory=d3.svg.line().x(function(d){return graph.x(d.x)}).y(function(d){return graph.y(d.y)}).interpolate(this.graph.interpolation).tension(this.tension);factory.defined&&factory.defined(function(d){return d.y!==null});return factory}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Stack");Rickshaw.Graph.Renderer.Stack=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"stack",defaults:function($super){return Rickshaw.extend($super(),{fill:true,stroke:false,unstack:false})},seriesPathFactory:function(){var graph=this.graph;var factory=d3.svg.area().x(function(d){return graph.x(d.x)}).y0(function(d){return graph.y(d.y0)}).y1(function(d){return graph.y(d.y+d.y0)}).interpolate(this.graph.interpolation).tension(this.tension);factory.defined&&factory.defined(function(d){return d.y!==null});return factory}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Bar");Rickshaw.Graph.Renderer.Bar=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"bar",defaults:function($super){var defaults=Rickshaw.extend($super(),{gapSize:.05,unstack:false});delete defaults.tension;return defaults},initialize:function($super,args){args=args||{};this.gapSize=args.gapSize||this.gapSize;$super(args)},domain:function($super){var domain=$super();var frequentInterval=this._frequentInterval(this.graph.stackedData.slice(-1).shift());domain.x[1]+=Number(frequentInterval.magnitude);return domain},barWidth:function(series){var frequentInterval=this._frequentInterval(series.stack);var barWidth=this.graph.x.magnitude(frequentInterval.magnitude)*(1-this.gapSize);return barWidth},render:function(args){args=args||{};var graph=this.graph;var series=args.series||graph.series;var vis=args.vis||graph.vis;vis.selectAll("*").remove();var barWidth=this.barWidth(series.active()[0]);var barXOffset=0;var activeSeriesCount=series.filter(function(s){return!s.disabled}).length;var seriesBarWidth=this.unstack?barWidth/activeSeriesCount:barWidth;var transform=function(d){var matrix=[1,0,0,d.y<0?-1:1,0,d.y<0?graph.y.magnitude(Math.abs(d.y))*2:0];return"matrix("+matrix.join(",")+")"};series.forEach(function(series){if(series.disabled)return;var barWidth=this.barWidth(series);var nodes=vis.selectAll("path").data(series.stack.filter(function(d){return d.y!==null})).enter().append("svg:rect").attr("x",function(d){return graph.x(d.x)+barXOffset}).attr("y",function(d){return graph.y(d.y0+Math.abs(d.y))*(d.y<0?-1:1)}).attr("width",seriesBarWidth).attr("height",function(d){return graph.y.magnitude(Math.abs(d.y))}).attr("transform",transform);Array.prototype.forEach.call(nodes[0],function(n){n.setAttribute("fill",series.color)});if(this.unstack)barXOffset+=seriesBarWidth},this)},_frequentInterval:function(data){var intervalCounts={};for(var i=0;i0){this[0].data.forEach(function(plot){item.data.push({x:plot.x,y:0})})}else if(item.data.length===0){item.data.push({x:this.timeBase-(this.timeInterval||0),y:0})}this.push(item);if(this.legend){this.legend.addLine(this.itemByName(item.name))}},addData:function(data,x){var index=this.getIndex();Rickshaw.keys(data).forEach(function(name){if(!this.itemByName(name)){this.addItem({name:name})}},this);this.forEach(function(item){item.data.push({x:x||(index*this.timeInterval||1)+this.timeBase,y:data[item.name]||0})},this)},getIndex:function(){return this[0]&&this[0].data&&this[0].data.length?this[0].data.length:0},itemByName:function(name){for(var i=0;i1;i--){this.currentSize+=1;this.currentIndex+=1;this.forEach(function(item){item.data.unshift({x:((i-1)*this.timeInterval||1)+this.timeBase,y:0,i:i})},this)}}},addData:function($super,data,x){$super(data,x);this.currentSize+=1;this.currentIndex+=1;if(this.maxDataPoints!==undefined){while(this.currentSize>this.maxDataPoints){this.dropData()}}},dropData:function(){this.forEach(function(item){item.data.splice(0,1)});this.currentSize-=1},getIndex:function(){return this.currentIndex}});return Rickshaw});
\ No newline at end of file
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/analytics/public/js/stream_layers.js b/modules/distribution/src/repository/jaggeryapps/iotserver/units/analytics/public/js/stream_layers.js
new file mode 100644
index 00000000..43d7cfe0
--- /dev/null
+++ b/modules/distribution/src/repository/jaggeryapps/iotserver/units/analytics/public/js/stream_layers.js
@@ -0,0 +1,35 @@
+
+/* Inspired by Lee Byron's test data generator. */
+function stream_layers(n, m, o) {
+ if (arguments.length < 3) o = 0;
+ function bump(a) {
+ var x = 1 / (.1 + Math.random()),
+ y = 2 * Math.random() - .5,
+ z = 10 / (.1 + Math.random());
+ for (var i = 0; i < m; i++) {
+ var w = (i / m - y) * z;
+ a[i] += x * Math.exp(-w * w);
+ }
+ }
+ return d3.range(n).map(function() {
+ var a = [], i;
+ for (i = 0; i < m; i++) a[i] = o + o * Math.random();
+ for (i = 0; i < 5; i++) bump(a);
+ return a.map(stream_index);
+ });
+}
+
+/* Another layer generator using gamma distributions. */
+function stream_waves(n, m) {
+ return d3.range(n).map(function(i) {
+ return d3.range(m).map(function(j) {
+ var x = 20 * j / m - i / 3;
+ return 2 * x * Math.exp(-.5 * x);
+ }).map(stream_index);
+ });
+}
+
+function stream_index(d, i) {
+ return {x: i, y: Math.max(0, d)};
+}
+
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/appbar/appbar.hbs b/modules/distribution/src/repository/jaggeryapps/iotserver/units/appbar/appbar.hbs
new file mode 100644
index 00000000..e82f3989
--- /dev/null
+++ b/modules/distribution/src/repository/jaggeryapps/iotserver/units/appbar/appbar.hbs
@@ -0,0 +1,57 @@
+{{#zone "main"}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{/zone}}
\ No newline at end of file
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/appbar/appbar.js b/modules/distribution/src/repository/jaggeryapps/iotserver/units/appbar/appbar.js
new file mode 100644
index 00000000..cb0d769b
--- /dev/null
+++ b/modules/distribution/src/repository/jaggeryapps/iotserver/units/appbar/appbar.js
@@ -0,0 +1,78 @@
+function onRequest(context) {
+ var constants = require("/modules/constants.js");
+ var user = session.get(constants.USER_SESSION_KEY);
+
+ var links = {
+ "users": [],
+ "policies": [],
+ "profiles": [],
+ "device-mgt": [],
+ "store": [],
+ "dashboard": [],
+ "statistics" : []
+ };
+ var dashboardLink = {
+ title: "Go back to Dashboard",
+ icon: "fw-left-arrow",
+ url: "/iotserver"
+ };
+
+ var deviceMgtLink = {
+ title: "Go back to Device Management",
+ icon: "fw-left-arrow",
+ url: "/iotserver/devices"
+ };
+
+ var storeLink = {
+ title: "Go back to Store",
+ icon: "fw-left-arrow",
+ url: "/iotserver"
+ };
+
+
+ links.users.push(dashboardLink);
+ links.policies.push(dashboardLink);
+ links.profiles.push(dashboardLink);
+ links.store.push(dashboardLink);
+ links.store.push(storeLink);
+ links.statistics.push(deviceMgtLink);
+ links['device-mgt'].push(dashboardLink);
+
+ if (user) {
+ var userModule = require("/modules/user.js").userModule;
+ var permissions = userModule.getUIPermissions();
+ context.permissions = permissions;
+
+ //if (permissions.ADD_USER) {
+ // links.users.push({
+ // title: "Add User",
+ // icon: "fw-add-user",
+ // url: "/iotserver/users/add-user"
+ // });
+ //}
+ if (permissions.ADD_POLICY) {
+ links.policies.push({
+ title: "Add Policy",
+ icon: "fw-policy",
+ url: "/iotserver/policies/add-policy"
+ });
+ }
+ //if (permissions.ADD_USER) {
+ // links.profiles.push({
+ // title: "Add Profile",
+ // icon: "fw-settings",
+ // url: "/iotserver/profiles/add-profile"
+ // });
+ //}
+ if (permissions.ADD_DEVICE) {
+ links["device-mgt"].push({
+ title: "Add Device",
+ icon: "fw-add",
+ url: "/iotserver/devices/add-device"
+ });
+ }
+ }// end-if-user
+
+ context.currentActions = links[context.link];
+ return context;
+}
\ No newline at end of file
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/appbar/appbar.json b/modules/distribution/src/repository/jaggeryapps/iotserver/units/appbar/appbar.json
new file mode 100644
index 00000000..6240cff5
--- /dev/null
+++ b/modules/distribution/src/repository/jaggeryapps/iotserver/units/appbar/appbar.json
@@ -0,0 +1,3 @@
+{
+ "predicate": "false"
+}
\ No newline at end of file
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/appbar/appbar_bck.hbs b/modules/distribution/src/repository/jaggeryapps/iotserver/units/appbar/appbar_bck.hbs
new file mode 100644
index 00000000..c246a57a
--- /dev/null
+++ b/modules/distribution/src/repository/jaggeryapps/iotserver/units/appbar/appbar_bck.hbs
@@ -0,0 +1,950 @@
+{{#zone "main"}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Register
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Register
+
+
+
+
+
+
+
+
+
+
+
+
+ Confirm Password
+
+
+
+
+
+
+
+ I Agree by Clicking register, you agree to the Terms and Conditions set out by this site
+
+
+
+
+ Register
+ Cancel
+
+
+
+
+
+
+
+{{/zone}}
\ No newline at end of file
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/dashboard/dashboard.hbs b/modules/distribution/src/repository/jaggeryapps/iotserver/units/dashboard/dashboard.hbs
new file mode 100644
index 00000000..c2f9870e
--- /dev/null
+++ b/modules/distribution/src/repository/jaggeryapps/iotserver/units/dashboard/dashboard.hbs
@@ -0,0 +1,87 @@
+{{#zone "main"}}
+
+
+
+{{/zone}}
+{{#zone "bottomJs"}}
+
+{{/zone}}
\ No newline at end of file
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/dashboard/dashboard.js b/modules/distribution/src/repository/jaggeryapps/iotserver/units/dashboard/dashboard.js
new file mode 100644
index 00000000..688371b4
--- /dev/null
+++ b/modules/distribution/src/repository/jaggeryapps/iotserver/units/dashboard/dashboard.js
@@ -0,0 +1,3 @@
+function onRequest(context){
+ return context;
+}
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/dashboard/dashboard.json b/modules/distribution/src/repository/jaggeryapps/iotserver/units/dashboard/dashboard.json
new file mode 100644
index 00000000..0e0dcd23
--- /dev/null
+++ b/modules/distribution/src/repository/jaggeryapps/iotserver/units/dashboard/dashboard.json
@@ -0,0 +1,3 @@
+{
+
+}
\ No newline at end of file
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/dashboard/public/js/dashboard.js b/modules/distribution/src/repository/jaggeryapps/iotserver/units/dashboard/public/js/dashboard.js
new file mode 100644
index 00000000..eaae317d
--- /dev/null
+++ b/modules/distribution/src/repository/jaggeryapps/iotserver/units/dashboard/public/js/dashboard.js
@@ -0,0 +1,20 @@
+var updateStats = function (serviceURL, id) {
+ invokerUtil.get(
+ serviceURL,
+ function (data) {
+ $(id).html(data);
+ }, function (message) {
+ console.log(message);
+ }
+ );
+};
+
+$(document).ready(function(){
+ //updateStats("/mdm-admin/devices/count", "#device-count");
+ //updateStats("/mdm-admin/policies/count", "#policy-count");
+ //updateStats("/mdm-admin/users/count/" + "carbon.super", "#user-count");
+ //TODO: get this value from devicecloud webservice
+ $("#device-count").html("0");
+ $("#policy-count").html("0");
+ $("#user-count").html("0");
+});
\ No newline at end of file
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/device-detail.hbs b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/device-detail.hbs
new file mode 100644
index 00000000..2228685c
--- /dev/null
+++ b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/device-detail.hbs
@@ -0,0 +1,195 @@
+{{#zone "main"}}
+
+
+
+ Device {{device.name}}
+
+ ({{device.viewModel.vendor}} {{device.viewModel.model}})
+
+
+
+
+
+
+
+
+
+ {{unit "operation-bar" deviceType=device.type}}
+
+
+
+
+
+
+
+
+
+
+
+
Device: {{device.viewModel.vendor}} {{device.properties.model}}
+
Model: {{device.viewModel.model}}
+
IMEI: {{device.viewModel.imei}}
+ {{#if device.viewModel.udid}}
UDID: {{device.viewModel.udid}}
{{/if}}
+ {{#if device.viewModel.phoneNumber}}
Phone Number: {{device.viewModel.phoneNumber}}
{{/if}}
+
+
+
+
+
+
+
+
+
+
+ {{#if device.viewModel.BatteryLevel}}
+
+
+
BATTERY
+
+
+
{{device.viewModel.BatteryLevel}}%
+
+
+
+ {{/if}}
+
+ {{#if device.viewModel.DeviceCapacity}}
+
+
+
STORAGE
+
+
+
{{device.viewModel.DeviceCapacityPercentage}}%{{device.viewModel.DeviceCapacityUsed}} GB Free
+
+
+
+ {{/if}}
+ {{#if device.viewModel.internal_memory.FreeCapacity}}
+
+
+
LOCAL STORAGE
+
+
+
{{device.viewModel.internal_memory.DeviceCapacityPercentage}}%{{device.viewModel.internal_memory.FreeCapacity}} GB Free
+
+
+
+ {{/if}}
+ {{#if device.viewModel.external_memory.FreeCapacity}}
+
+
+
EXTERNAL STORAGE
+
+
+
{{device.viewModel.external_memory.DeviceCapacityPercentage}}%{{device.viewModel.external_memory.FreeCapacity}} GB Free
+
+
+
+ {{/if}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Not available yet
+
+
+
+
+
+
+
+
+
+
+ Not available yet
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{/zone}}
+{{#zone "bottomJs"}}
+
+
+{{/zone}}
\ No newline at end of file
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/device-detail.js b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/device-detail.js
new file mode 100644
index 00000000..24169f72
--- /dev/null
+++ b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/device-detail.js
@@ -0,0 +1,59 @@
+function onRequest(context) {
+ var uri = request.getRequestURI();
+ var uriMatcher = new URIMatcher(String(uri));
+ var isMatched = uriMatcher.match("/{context}/device/{deviceType}/{+deviceId}");
+ if (isMatched) {
+ var matchedElements = uriMatcher.elements();
+ var deviceType = matchedElements.deviceType;
+ var deviceId = matchedElements.deviceId;
+ context.deviceType = deviceType;
+ context.deviceId = deviceId;
+ var deviceModule = require("/modules/device.js").deviceModule;
+ var device = deviceModule.viewDevice(deviceType, deviceId);
+ if (device){
+ var viewModel = {};
+ var deviceInfo = device.properties.DEVICE_INFO;
+ log.info(deviceInfo);
+ if (deviceInfo != undefined && String(deviceInfo.toString()).length > 0){
+ deviceInfo = JSON.parse(deviceInfo);
+ if (device.type == "ios"){
+ viewModel.imei = device.properties.IMEI;
+ viewModel.phoneNumber = deviceInfo.PhoneNumber;
+ viewModel.udid = deviceInfo.UDID;
+ viewModel.BatteryLevel = Math.round(deviceInfo.BatteryLevel * 100);
+ viewModel.DeviceCapacity = Math.round(deviceInfo.DeviceCapacity * 100) / 100;
+ viewModel.AvailableDeviceCapacity = Math.round(deviceInfo.AvailableDeviceCapacity * 100) / 100;
+ viewModel.DeviceCapacityUsed = Math.round((viewModel.DeviceCapacity
+ - viewModel.AvailableDeviceCapacity) * 100) / 100;
+ viewModel.DeviceCapacityPercentage = Math.round(viewModel.DeviceCapacityUsed
+ / viewModel.DeviceCapacity * 10000) /100;
+ }else if(device.type == "android"){
+
+ viewModel.imei = device.properties.IMEI;
+ viewModel.model = device.properties.DEVICE_MODEL;
+ viewModel.vendor = device.properties.VENDOR;
+ viewModel.internal_memory = {};
+ viewModel.external_memory = {};
+ viewModel.location = {
+ latitude: device.properties.LATITUDE,
+ longitude: device.properties.LONGITUDE
+ };
+ viewModel.BatteryLevel = deviceInfo.BATTERY_LEVEL;
+ viewModel.internal_memory.FreeCapacity = Math.round((deviceInfo.INTERNAL_TOTAL_MEMORY -
+ deviceInfo.INTERNAL_AVAILABLE_MEMORY) * 100) / 100;
+ viewModel.internal_memory.DeviceCapacityPercentage = Math.round(deviceInfo.INTERNAL_AVAILABLE_MEMORY
+ / deviceInfo.INTERNAL_TOTAL_MEMORY * 10000) / 100;
+ viewModel.external_memory.FreeCapacity = Math.round((deviceInfo.EXTERNAL_TOTAL_MEMORY -
+ deviceInfo.EXTERNAL_AVAILABLE_MEMORY) * 100) / 100;
+ viewModel.external_memory.DeviceCapacityPercentage = Math.round(deviceInfo.EXTERNAL_AVAILABLE_MEMORY
+ /deviceInfo.EXTERNAL_TOTAL_MEMORY * 10000) /100;
+ }
+ device.viewModel = viewModel;
+ }
+ }
+ context.device = device;
+ } else {
+ response.sendError(404);
+ }
+ return context;
+}
\ No newline at end of file
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/device-detail.json b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/device-detail.json
new file mode 100644
index 00000000..3dbff381
--- /dev/null
+++ b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/device-detail.json
@@ -0,0 +1,3 @@
+{
+ "predicate": "false"
+}
\ No newline at end of file
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/img/device_icons/android.png b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/img/device_icons/android.png
new file mode 100644
index 00000000..7fee78a6
Binary files /dev/null and b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/img/device_icons/android.png differ
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/img/device_icons/arduino.png b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/img/device_icons/arduino.png
new file mode 100644
index 00000000..31cb9fa3
Binary files /dev/null and b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/img/device_icons/arduino.png differ
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/img/device_icons/digital_display.png b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/img/device_icons/digital_display.png
new file mode 100644
index 00000000..fb1f624c
Binary files /dev/null and b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/img/device_icons/digital_display.png differ
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/img/device_icons/firealarm.png b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/img/device_icons/firealarm.png
new file mode 100644
index 00000000..aaf16925
Binary files /dev/null and b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/img/device_icons/firealarm.png differ
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/img/device_icons/ios.png b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/img/device_icons/ios.png
new file mode 100644
index 00000000..4b09796f
Binary files /dev/null and b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/img/device_icons/ios.png differ
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/img/device_icons/sensebot.png b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/img/device_icons/sensebot.png
new file mode 100644
index 00000000..04e5752a
Binary files /dev/null and b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/img/device_icons/sensebot.png differ
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/img/graph.png b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/img/graph.png
new file mode 100644
index 00000000..dd819ef4
Binary files /dev/null and b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/img/graph.png differ
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/js/device-detail.js b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/js/device-detail.js
new file mode 100644
index 00000000..04a2bedd
--- /dev/null
+++ b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-detail/public/js/device-detail.js
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
+ *
+ * WSO2 Inc. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+(function () {
+ var deviceId = $(".device-id");
+ var deviceIdentifier = deviceId.data("deviceid");
+ var deviceType = deviceId.data("type");
+ var payload = [deviceIdentifier];
+ if (deviceType == "ios") {
+ var serviceUrl = "/ios/operation/deviceinfo";
+ } else if (deviceType == "android") {
+ var serviceUrl = "/mdm-android-agent/operation/device-info";
+ }
+ invokerUtil.post(serviceUrl, payload,
+ function(message){
+ console.log(message);
+ }, function (message) {
+ console.log(message);
+ });
+ $(document).ready(function(){
+ loadOperationBar(deviceType);
+ loadMap();
+ });
+ function loadMap(){
+ var map;
+ function initialize() {
+ var mapOptions = {
+ zoom: 18
+ };
+ var lat = 6.9098591;
+ var long = 79.8523753;
+ map = new google.maps.Map(document.getElementById('device-location'),
+ mapOptions);
+
+ var pos = new google.maps.LatLng(lat,
+ long);
+ var marker = new google.maps.Marker({
+ position: pos,
+ map: map
+ });
+
+ map.setCenter(pos);
+ }
+ google.maps.event.addDomListener(window, 'load', initialize);
+ }
+}());
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/device-listing.hbs b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/device-listing.hbs
new file mode 100644
index 00000000..8dddd607
--- /dev/null
+++ b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/device-listing.hbs
@@ -0,0 +1,168 @@
+{{#zone "main"}}
+
+
+
+
+
+
+
+
+
+
+
Do you really want to remove this device from your Devices List?
+
+
+
+
+
+
+
+
+
+
+
Device was successfully removed.
+
+
+
+
+
+
+
+
+
+
+
Please enter new name for the device?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Device was successfully updated.
+
+
+
+
+
+
+
+
+
+
+
Exception at backend. Try Later.
+
+
+
+
+
+
+
+
+
+
+
Action not permitted.
+
+
+
+
+
+
+
+
+
+
+
Device does not exist.
+
+
+
+
+
+
+
+
+
+
+
Device does not exist.
+
+
+
+
+
+{{/zone}}
+{{#zone "bottomJs"}}
+
+
+{{/zone}}
\ No newline at end of file
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/device-listing.js b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/device-listing.js
new file mode 100644
index 00000000..3a5fc9b6
--- /dev/null
+++ b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/device-listing.js
@@ -0,0 +1,14 @@
+function onRequest(context){
+ var userModule = require("/modules/user.js").userModule;
+ var permissions = [];
+ if(userModule.isAuthorized("/permission/device-mgt/admin/devices/list")){
+ permissions.push("LIST_DEVICES");
+ }else if(userModule.isAuthorized("/permission/device-mgt/user/devices/list")){
+ permissions.push("LIST_OWN_DEVICES");
+ }else if(userModule.isAuthorized("/permission/device-mgt/admin/policies/list")){
+ permissions.push("LIST_POLICIES");
+ }
+ permissions.push("LIST_OWN_DEVICES");
+ context.permissions = stringify(permissions);
+ return context;
+}
\ No newline at end of file
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/device-listing.json b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/device-listing.json
new file mode 100644
index 00000000..3dbff381
--- /dev/null
+++ b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/device-listing.json
@@ -0,0 +1,3 @@
+{
+ "predicate": "false"
+}
\ No newline at end of file
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/images/android.png b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/images/android.png
new file mode 100644
index 00000000..7fee78a6
Binary files /dev/null and b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/images/android.png differ
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/images/arduino.png b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/images/arduino.png
new file mode 100644
index 00000000..31cb9fa3
Binary files /dev/null and b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/images/arduino.png differ
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/images/digital_display.png b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/images/digital_display.png
new file mode 100644
index 00000000..fb1f624c
Binary files /dev/null and b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/images/digital_display.png differ
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/images/firealarm.png b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/images/firealarm.png
new file mode 100644
index 00000000..aaf16925
Binary files /dev/null and b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/images/firealarm.png differ
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/images/ios.png b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/images/ios.png
new file mode 100644
index 00000000..4b09796f
Binary files /dev/null and b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/images/ios.png differ
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/images/sensebot.png b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/images/sensebot.png
new file mode 100644
index 00000000..04e5752a
Binary files /dev/null and b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/images/sensebot.png differ
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/js/device-listing.js b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/js/device-listing.js
new file mode 100644
index 00000000..5e4565d5
--- /dev/null
+++ b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/js/device-listing.js
@@ -0,0 +1,346 @@
+/*
+ * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
+ *
+ * WSO2 Inc. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+(function () {
+ var cache = {};
+ var permissionSet = {};
+ var validateAndReturn = function (value) {
+ return (value == undefined || value == null) ? "Unspecified" : value;
+ };
+ Handlebars.registerHelper("deviceMap", function (device) {
+ device.owner = validateAndReturn(device.owner);
+ device.ownership = validateAndReturn(device.ownership);
+ var arr = device.properties;
+ if (arr) {
+ device.properties = arr.reduce(function (total, current) {
+ total[current.name] = validateAndReturn(current.value);
+ return total;
+ }, {});
+ }
+ });
+
+ //This method is used to setup permission for device listing
+ $.setPermission = function (permission) {
+ permissionSet[permission] = true;
+ };
+
+ $.hasPermission = function (permission) {
+ return permissionSet[permission];
+ };
+})();
+
+/*
+ * Setting-up global variables.
+ */
+var deviceCheckbox = "#ast-container .ctrl-wr-asset .itm-select input[type='checkbox']";
+var assetContainer = "#ast-container";
+
+/*
+ * DOM ready functions.
+ */
+$(document).ready(function () {
+ /* Adding selected class for selected devices */
+ $(deviceCheckbox).each(function () {
+ addDeviceSelectedClass(this);
+ });
+
+ var i;
+ var permissionList = $("#permission").data("permission");
+ for (i = 0; i < permissionList.length; i++) {
+ $.setPermission(permissionList[i]);
+ }
+
+ /* for device list sorting drop down */
+ $(".ctrl-filter-type-switcher").popover({
+ html: true,
+ content: function () {
+ return $("#content-filter-types").html();
+ }
+ });
+});
+
+/*
+ * On Select All Device button click function.
+ *
+ * @param button: Select All Device button
+ */
+function selectAllDevices(button) {
+ if (!$(button).data('select')) {
+ $(deviceCheckbox).each(function (index) {
+ $(this).prop('checked', true);
+ addDeviceSelectedClass(this);
+ });
+ $(button).data('select', true);
+ $(button).html('Deselect All Devices');
+ } else {
+ $(deviceCheckbox).each(function (index) {
+ $(this).prop('checked', false);
+ addDeviceSelectedClass(this);
+ });
+ $(button).data('select', false);
+ $(button).html('Select All Devices');
+ }
+}
+
+/*
+ * On listing layout toggle buttons click function.
+ *
+ * @param view: Selected view type
+ * @param selection: Selection button
+ */
+function changeDeviceView(view, selection) {
+ $(".view-toggle").each(function () {
+ $(this).removeClass("selected");
+ });
+ $(selection).addClass("selected");
+ if (view == "list") {
+ $(assetContainer).addClass("list-view");
+ } else {
+ $(assetContainer).removeClass("list-view");
+ }
+}
+
+/*
+ * Add selected style class to the parent element function.
+ *
+ * @param checkbox: Selected checkbox
+ */
+function addDeviceSelectedClass(checkbox) {
+ if ($(checkbox).is(":checked")) {
+ $(checkbox).closest(".ctrl-wr-asset").addClass("selected device-select");
+ } else {
+ $(checkbox).closest(".ctrl-wr-asset").removeClass("selected device-select");
+ }
+}
+function loadDevices(searchType, searchParam) {
+ var deviceListing = $("#device-listing");
+ var deviceListingSrc = deviceListing.attr("src");
+ var imageResource = deviceListing.data("image-resource");
+ $.template("device-listing", deviceListingSrc, function (template) {
+ var serviceURL;
+ if ($.hasPermission("LIST_DEVICES")) {
+ serviceURL = "/iotserver/api/devices/all";
+ } else if ($.hasPermission("LIST_OWN_DEVICES")) {
+ //Get authenticated users devices
+ serviceURL = "/iotserver/api/devices/all";
+ } else {
+ $("#ast-container").html("Permission denied");
+ return;
+ }
+ if (searchParam) {
+ if (searchType == "users") {
+ serviceURL = serviceURL + "?user=" + searchParam;
+ } else if (searchType == "user-roles") {
+ serviceURL = serviceURL + "?role=" + searchParam;
+ } else {
+ serviceURL = serviceURL + "?type=" + searchParam;
+ }
+ }
+ var successCallback = function (data) {
+ data = JSON.parse(data);
+ var viewModel = {};
+ viewModel.devices = data.data;
+ viewModel.imageLocation = imageResource;
+ if(!data.data || data.data.length <= 0){
+ $("#ast-container").html($("#no-devices-div-content").html());
+ } else {
+ var content = template(viewModel);
+ $("#ast-container").html(content);
+ /*
+ * On device checkbox select add parent selected style class
+ */
+ $(deviceCheckbox).click(function () {
+ addDeviceSelectedClass(this);
+ });
+ attachEvents();
+ }
+ };
+ invokerUtil.get(serviceURL,
+ successCallback, function (message) {
+ console.log(message);
+ });
+ });
+}
+$(document).ready(function () {
+ loadDevices();
+});
+
+
+/**
+ * Sorting function of users
+ * listed on User Management page in WSO2 MDM Console.
+ */
+$(function () {
+ var sortableElem = '.wr-sortable';
+ $(sortableElem).sortable({
+ beforeStop: function () {
+ var sortedIDs = $(this).sortable('toArray');
+ console.log(sortedIDs);
+ }
+ });
+ $(sortableElem).disableSelection();
+});
+
+var modalPopup = ".wr-modalpopup";
+var modalPopupContainer = modalPopup + " .modalpopup-container";
+var modalPopupContent = modalPopup + " .modalpopup-content";
+var body = "body";
+
+/*
+ * set popup maximum height function.
+ */
+function setPopupMaxHeight() {
+ $(modalPopupContent).css('max-height', ($(body).height() - ($(body).height() / 100 * 30)));
+ $(modalPopupContainer).css('margin-top', (-($(modalPopupContainer).height() / 2)));
+}
+
+/*
+ * show popup function.
+ */
+function showPopup() {
+ $(modalPopup).show();
+ setPopupMaxHeight();
+}
+
+/*
+ * hide popup function.
+ */
+function hidePopup() {
+ $(modalPopupContent).html('');
+ $(modalPopup).hide();
+}
+
+
+/**
+ * Following functions should be triggered after AJAX request is made.
+ */
+function attachEvents() {
+ /**
+ * Following click function would execute
+ * when a user clicks on "Remove" link
+ * on Device Management page in WSO2 MDM Console.
+ */
+ $("a.remove-device-link").click(function () {
+ var deviceId = $(this).data("deviceid");
+ var deviceType = $(this).data("devicetype");
+ var removeDeviceAPI = "/iotserver/api/device/" + deviceType + "/" + deviceId + "/remove";
+
+ $(modalPopupContent).html($('#remove-device-modal-content').html());
+ showPopup();
+
+ $("a#remove-device-yes-link").click(function () {
+ invokerUtil.get(
+ removeDeviceAPI,
+ function (data) {
+ if (data == 200) {
+ $("#" + username).addClass("hide");
+ $(modalPopupContent).html($('#remove-device-200-content').html());
+ $("a#remove-device-200-link").click(function () {
+ hidePopup();
+ });
+ } else if (data == 400) {
+ $(modalPopupContent).html($('#remove-device-400-content').html());
+ $("a#remove-device-400-link").click(function () {
+ hidePopup();
+ });
+ } else if (data == 403) {
+ $(modalPopupContent).html($('#remove-device-403-content').html());
+ $("a#remove-device-403-link").click(function () {
+ hidePopup();
+ });
+ } else if (data == 409) {
+ $(modalPopupContent).html($('#remove-device-409-content').html());
+ $("a#remove-device-409-link").click(function () {
+ hidePopup();
+ });
+ }
+ },
+ function () {
+ $(modalPopupContent).html($('#remove-device-unexpected-error-content').html());
+ $("a#remove-device-unexpected-error-link").click(function () {
+ hidePopup();
+ });
+ }
+ );
+ });
+
+ $("a#remove-device-cancel-link").click(function () {
+ hidePopup();
+ });
+
+ });
+
+ /**
+ * Following click function would execute
+ * when a user clicks on "Edit" link
+ * on Device Management page in WSO2 MDM Console.
+ */
+ $("a.edit-device-link").click(function () {
+ var deviceId = $(this).data("deviceid");
+ var deviceType = $(this).data("devicetype");
+ var deviceName = $(this).data("devicename");
+ var editDeviceAPI = "/iotserver/api/device/" + deviceType + "/" + deviceId + "/update";
+
+ $(modalPopupContent).html($('#edit-device-modal-content').html());
+ $('#edit-device-name').val(deviceName);
+ showPopup();
+
+ $("a#edit-device-yes-link").click(function () {
+ device={};
+ device.name = $('#edit-device-name').val();
+ invokerUtil.post(
+ editDeviceAPI,
+ device,
+ function (data) {
+ if (data == 200) {
+ $("#" + username).addClass("hide");
+ $(modalPopupContent).html($('#edit-device-200-content').html());
+ $("a#edit-device-200-link").click(function () {
+ hidePopup();
+ });
+ } else if (data == 400) {
+ $(modalPopupContent).html($('#edit-device-400-content').html());
+ $("a#edit-device-400-link").click(function () {
+ hidePopup();
+ });
+ } else if (data == 403) {
+ $(modalPopupContent).html($('#edit-device-403-content').html());
+ $("a#edit-device-403-link").click(function () {
+ hidePopup();
+ });
+ } else if (data == 409) {
+ $(modalPopupContent).html($('#edit-device-409-content').html());
+ $("a#edit-device-409-link").click(function () {
+ hidePopup();
+ });
+ }
+ },
+ function () {
+ $(modalPopupContent).html($('#edit-device-unexpected-error-content').html());
+ $("a#edit-device-unexpected-error-link").click(function () {
+ hidePopup();
+ });
+ }
+ );
+ });
+
+ $("a#edit-device-cancel-link").click(function () {
+ hidePopup();
+ });
+ });
+}
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/templates/device-listing.hbs b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/templates/device-listing.hbs
new file mode 100644
index 00000000..e7d40e4c
--- /dev/null
+++ b/modules/distribution/src/repository/jaggeryapps/iotserver/units/device-listing/public/templates/device-listing.hbs
@@ -0,0 +1,54 @@
+{{#each devices}}
+ {{deviceMap this}}
+
+
+{{/each}}
+
+
\ No newline at end of file
diff --git a/modules/distribution/src/repository/jaggeryapps/iotserver/units/devices/android/android.hbs b/modules/distribution/src/repository/jaggeryapps/iotserver/units/devices/android/android.hbs
new file mode 100644
index 00000000..aa0018ca
--- /dev/null
+++ b/modules/distribution/src/repository/jaggeryapps/iotserver/units/devices/android/android.hbs
@@ -0,0 +1,89 @@
+{{#zone "main"}}
+