From: Ant Zucaro Date: Wed, 11 Sep 2013 01:38:30 +0000 (-0400) Subject: Add the full version of nvd3. X-Git-Url: https://git.rm.cloudns.org/?a=commitdiff_plain;h=8bbc0e04b873fa928a0d3cd72eed8e41cc061629;p=xonotic%2Fxonstat.git Add the full version of nvd3. --- diff --git a/xonstat/static/js/nv.d3.js b/xonstat/static/js/nv.d3.js new file mode 100644 index 0000000..5b19a49 --- /dev/null +++ b/xonstat/static/js/nv.d3.js @@ -0,0 +1,14128 @@ +(function(){ + +var nv = window.nv || {}; + + +nv.version = '1.1.10b'; +nv.dev = true //set false when in production + +window.nv = nv; + +nv.tooltip = {}; // For the tooltip system +nv.utils = {}; // Utility subsystem +nv.models = {}; //stores all the possible models/components +nv.charts = {}; //stores all the ready to use charts +nv.graphs = []; //stores all the graphs currently on the page +nv.logs = {}; //stores some statistics and potential error messages + +nv.dispatch = d3.dispatch('render_start', 'render_end'); + +// ************************************************************************* +// Development render timers - disabled if dev = false + +if (nv.dev) { + nv.dispatch.on('render_start', function(e) { + nv.logs.startTime = +new Date(); + }); + + nv.dispatch.on('render_end', function(e) { + nv.logs.endTime = +new Date(); + nv.logs.totalTime = nv.logs.endTime - nv.logs.startTime; + nv.log('total', nv.logs.totalTime); // used for development, to keep track of graph generation times + }); +} + +// ******************************************** +// Public Core NV functions + +// Logs all arguments, and returns the last so you can test things in place +// Note: in IE8 console.log is an object not a function, and if modernizr is used +// then calling Function.prototype.bind with with anything other than a function +// causes a TypeError to be thrown. +nv.log = function() { + if (nv.dev && console.log && console.log.apply) + console.log.apply(console, arguments) + else if (nv.dev && typeof console.log == "function" && Function.prototype.bind) { + var log = Function.prototype.bind.call(console.log, console); + log.apply(console, arguments); + } + return arguments[arguments.length - 1]; +}; + + +nv.render = function render(step) { + step = step || 1; // number of graphs to generate in each timeout loop + + nv.render.active = true; + nv.dispatch.render_start(); + + setTimeout(function() { + var chart, graph; + + for (var i = 0; i < step && (graph = nv.render.queue[i]); i++) { + chart = graph.generate(); + if (typeof graph.callback == typeof(Function)) graph.callback(chart); + nv.graphs.push(chart); + } + + nv.render.queue.splice(0, i); + + if (nv.render.queue.length) setTimeout(arguments.callee, 0); + else { nv.render.active = false; nv.dispatch.render_end(); } + }, 0); +}; + +nv.render.active = false; +nv.render.queue = []; + +nv.addGraph = function(obj) { + if (typeof arguments[0] === typeof(Function)) + obj = {generate: arguments[0], callback: arguments[1]}; + + nv.render.queue.push(obj); + + if (!nv.render.active) nv.render(); +}; + +nv.identity = function(d) { return d; }; + +nv.strip = function(s) { return s.replace(/(\s|&)/g,''); }; + +function daysInMonth(month,year) { + return (new Date(year, month+1, 0)).getDate(); +} + +function d3_time_range(floor, step, number) { + return function(t0, t1, dt) { + var time = floor(t0), times = []; + if (time < t0) step(time); + if (dt > 1) { + while (time < t1) { + var date = new Date(+time); + if ((number(date) % dt === 0)) times.push(date); + step(time); + } + } else { + while (time < t1) { times.push(new Date(+time)); step(time); } + } + return times; + }; +} + +d3.time.monthEnd = function(date) { + return new Date(date.getFullYear(), date.getMonth(), 0); +}; + +d3.time.monthEnds = d3_time_range(d3.time.monthEnd, function(date) { + date.setUTCDate(date.getUTCDate() + 1); + date.setDate(daysInMonth(date.getMonth() + 1, date.getFullYear())); + }, function(date) { + return date.getMonth(); + } +); + +/* Utility class to handle creation of an interactive layer. +This places a rectangle on top of the chart. When you mouse move over it, it sends a dispatch +containing the X-coordinate. It can also render a vertical line where the mouse is located. + +dispatch.elementMousemove is the important event to latch onto. It is fired whenever the mouse moves over +the rectangle. The dispatch is given one object which contains the mouseX/Y location. +It also has 'pointXValue', which is the conversion of mouseX to the x-axis scale. +*/ +nv.interactiveGuideline = function() { + "use strict"; + var tooltip = nv.models.tooltip(); + //Public settings + var width = null + , height = null + //Please pass in the bounding chart's top and left margins + //This is important for calculating the correct mouseX/Y positions. + , margin = {left: 0, top: 0} + , xScale = d3.scale.linear() + , yScale = d3.scale.linear() + , dispatch = d3.dispatch('elementMousemove', 'elementMouseout') + , showGuideLine = true + , svgContainer = null + //Must pass in the bounding chart's container. + //The mousemove event is attached to this container. + ; + + //Private variables + var isMSIE = navigator.userAgent.indexOf("MSIE") !== -1 //Check user-agent for Microsoft Internet Explorer. + ; + + + function layer(selection) { + selection.each(function(data) { + var container = d3.select(this); + + var availableWidth = (width || 960), availableHeight = (height || 400); + + var wrap = container.selectAll("g.nv-wrap.nv-interactiveLineLayer").data([data]); + var wrapEnter = wrap.enter() + .append("g").attr("class", " nv-wrap nv-interactiveLineLayer"); + + + wrapEnter.append("g").attr("class","nv-interactiveGuideLine"); + + if (!svgContainer) { + return; + } + + function mouseHandler() { + var d3mouse = d3.mouse(this); + var mouseX = d3mouse[0]; + var mouseY = d3mouse[1]; + var subtractMargin = true; + var mouseOutAnyReason = false; + if (isMSIE) { + /* + D3.js (or maybe SVG.getScreenCTM) has a nasty bug in Internet Explorer 10. + d3.mouse() returns incorrect X,Y mouse coordinates when mouse moving + over a rect in IE 10. + However, d3.event.offsetX/Y also returns the mouse coordinates + relative to the triggering . So we use offsetX/Y on IE. + */ + mouseX = d3.event.offsetX; + mouseY = d3.event.offsetY; + + /* + On IE, if you attach a mouse event listener to the container, + it will actually trigger it for all the child elements (like , , etc). + When this happens on IE, the offsetX/Y is set to where ever the child element + is located. + As a result, we do NOT need to subtract margins to figure out the mouse X/Y + position under this scenario. Removing the line below *will* cause + the interactive layer to not work right on IE. + */ + if(d3.event.target.tagName !== "svg") + subtractMargin = false; + + if (d3.event.target.className.baseVal.match("nv-legend")) + mouseOutAnyReason = true; + + } + + if(subtractMargin) { + mouseX -= margin.left; + mouseY -= margin.top; + } + + /* If mouseX/Y is outside of the chart's bounds, + trigger a mouseOut event. + */ + if (mouseX < 0 || mouseY < 0 + || mouseX > availableWidth || mouseY > availableHeight + || (d3.event.relatedTarget && d3.event.relatedTarget.ownerSVGElement === undefined) + || mouseOutAnyReason + ) + { + if (isMSIE) { + if (d3.event.relatedTarget + && d3.event.relatedTarget.ownerSVGElement === undefined + && d3.event.relatedTarget.className.match(tooltip.nvPointerEventsClass)) { + return; + } + } + dispatch.elementMouseout({ + mouseX: mouseX, + mouseY: mouseY + }); + layer.renderGuideLine(null); //hide the guideline + return; + } + + var pointXValue = xScale.invert(mouseX); + dispatch.elementMousemove({ + mouseX: mouseX, + mouseY: mouseY, + pointXValue: pointXValue + }); + } + + svgContainer + .on("mousemove",mouseHandler, true) + .on("mouseout",mouseHandler,true) + ; + + //Draws a vertical guideline at the given X postion. + layer.renderGuideLine = function(x) { + if (!showGuideLine) return; + var line = wrap.select(".nv-interactiveGuideLine") + .selectAll("line") + .data((x != null) ? [nv.utils.NaNtoZero(x)] : [], String); + + line.enter() + .append("line") + .attr("class", "nv-guideline") + .attr("x1", function(d) { return d;}) + .attr("x2", function(d) { return d;}) + .attr("y1", availableHeight) + .attr("y2",0) + ; + line.exit().remove(); + + } + }); + } + + layer.dispatch = dispatch; + layer.tooltip = tooltip; + + layer.margin = function(_) { + if (!arguments.length) return margin; + margin.top = typeof _.top != 'undefined' ? _.top : margin.top; + margin.left = typeof _.left != 'undefined' ? _.left : margin.left; + return layer; + }; + + layer.width = function(_) { + if (!arguments.length) return width; + width = _; + return layer; + }; + + layer.height = function(_) { + if (!arguments.length) return height; + height = _; + return layer; + }; + + layer.xScale = function(_) { + if (!arguments.length) return xScale; + xScale = _; + return layer; + }; + + layer.showGuideLine = function(_) { + if (!arguments.length) return showGuideLine; + showGuideLine = _; + return layer; + }; + + layer.svgContainer = function(_) { + if (!arguments.length) return svgContainer; + svgContainer = _; + return layer; + }; + + + return layer; +}; + +/* Utility class that uses d3.bisect to find the index in a given array, where a search value can be inserted. +This is different from normal bisectLeft; this function finds the nearest index to insert the search value. + +For instance, lets say your array is [1,2,3,5,10,30], and you search for 28. +Normal d3.bisectLeft will return 4, because 28 is inserted after the number 10. But interactiveBisect will return 5 +because 28 is closer to 30 than 10. + +Unit tests can be found in: interactiveBisectTest.html + +Has the following known issues: + * Will not work if the data points move backwards (ie, 10,9,8,7, etc) or if the data points are in random order. + * Won't work if there are duplicate x coordinate values. +*/ +nv.interactiveBisect = function (values, searchVal, xAccessor) { + "use strict"; + if (! values instanceof Array) return null; + if (typeof xAccessor !== 'function') xAccessor = function(d,i) { return d.x;} + + var bisect = d3.bisector(xAccessor).left; + var index = d3.max([0, bisect(values,searchVal) - 1]); + var currentValue = xAccessor(values[index], index); + if (typeof currentValue === 'undefined') currentValue = index; + + if (currentValue === searchVal) return index; //found exact match + + var nextIndex = d3.min([index+1, values.length - 1]); + var nextValue = xAccessor(values[nextIndex], nextIndex); + if (typeof nextValue === 'undefined') nextValue = nextIndex; + + if (Math.abs(nextValue - searchVal) >= Math.abs(currentValue - searchVal)) + return index; + else + return nextIndex +};/* Tooltip rendering model for nvd3 charts. +window.nv.models.tooltip is the updated,new way to render tooltips. + +window.nv.tooltip.show is the old tooltip code. +window.nv.tooltip.* also has various helper methods. +*/ +(function() { + "use strict"; + window.nv.tooltip = {}; + + /* Model which can be instantiated to handle tooltip rendering. + Example usage: + var tip = nv.models.tooltip().gravity('w').distance(23) + .data(myDataObject); + + tip(); //just invoke the returned function to render tooltip. + */ + window.nv.models.tooltip = function() { + var content = null //HTML contents of the tooltip. If null, the content is generated via the data variable. + , data = null /* Tooltip data. If data is given in the proper format, a consistent tooltip is generated. + Format of data: + { + key: "Date", + value: "August 2009", + series: [ + { + key: "Series 1", + value: "Value 1", + color: "#000" + }, + { + key: "Series 2", + value: "Value 2", + color: "#00f" + } + ] + + } + + */ + , gravity = 'w' //Can be 'n','s','e','w'. Determines how tooltip is positioned. + , distance = 50 //Distance to offset tooltip from the mouse location. + , snapDistance = 25 //Tolerance allowed before tooltip is moved from its current position (creates 'snapping' effect) + , fixedTop = null //If not null, this fixes the top position of the tooltip. + , classes = null //Attaches additional CSS classes to the tooltip DIV that is created. + , chartContainer = null //Parent DIV, of the SVG Container that holds the chart. + , position = {left: null, top: null} //Relative position of the tooltip inside chartContainer. + , enabled = true //True -> tooltips are rendered. False -> don't render tooltips. + //Generates a unique id when you create a new tooltip() object + , id = "nvtooltip-" + Math.floor(Math.random() * 100000) + ; + + //CSS class to specify whether element should not have mouse events. + var nvPointerEventsClass = "nv-pointer-events-none"; + + //Format function for the tooltip values column + var valueFormatter = function(d,i) { + return d; + }; + + //Format function for the tooltip header value. + var headerFormatter = function(d) { + return d; + }; + + //By default, the tooltip model renders a beautiful table inside a DIV. + //You can override this function if a custom tooltip is desired. + var contentGenerator = function(d) { + if (content != null) return content; + + if (d == null) return ''; + + var html = ""; + if (d.series instanceof Array) { + d.series.forEach(function(item, i) { + html += ""; + html += ""; + html += ""; + html += ""; + }); + } + html += "
" + headerFormatter(d.value) + "
" + item.key + ":" + valueFormatter(item.value,i) + "
"; + return html; + }; + + var dataSeriesExists = function(d) { + if (d && d.series && d.series.length > 0) return true; + + return false; + }; + + //In situations where the chart is in a 'viewBox', re-position the tooltip based on how far chart is zoomed. + function convertViewBoxRatio() { + if (chartContainer) { + var svg = d3.select(chartContainer); + if (svg.node().tagName !== "svg") { + svg = svg.select("svg"); + } + var viewBox = (svg.node()) ? svg.attr('viewBox') : null; + if (viewBox) { + viewBox = viewBox.split(' '); + var ratio = parseInt(svg.style('width')) / viewBox[2]; + + position.left = position.left * ratio; + position.top = position.top * ratio; + } + } + } + + //Creates new tooltip container, or uses existing one on DOM. + function getTooltipContainer(newContent) { + var body; + if (chartContainer) + body = d3.select(chartContainer); + else + body = d3.select("body"); + + var container = body.select(".nvtooltip"); + if (container.node() === null) { + //Create new tooltip div if it doesn't exist on DOM. + container = body.append("div") + .attr("class", "nvtooltip " + (classes? classes: "xy-tooltip")) + .attr("id",id) + ; + } + + + container.node().innerHTML = newContent; + container.style("top",0).style("left",0).style("opacity",0); + container.selectAll("div, table, td, tr").classed(nvPointerEventsClass,true) + container.classed(nvPointerEventsClass,true); + return container.node(); + } + + + + //Draw the tooltip onto the DOM. + function nvtooltip() { + if (!enabled) return; + if (!dataSeriesExists(data)) return; + + convertViewBoxRatio(); + + var left = position.left; + var top = (fixedTop != null) ? fixedTop : position.top; + var container = getTooltipContainer(contentGenerator(data)); + + if (chartContainer) { + var svgComp = chartContainer.getElementsByTagName("svg")[0]; + var boundRect = (svgComp) ? svgComp.getBoundingClientRect() : chartContainer.getBoundingClientRect(); + var svgOffset = {left:0,top:0}; + if (svgComp) { + var svgBound = svgComp.getBoundingClientRect(); + var chartBound = chartContainer.getBoundingClientRect(); + svgOffset.top = Math.abs(svgBound.top - chartBound.top); + svgOffset.left = Math.abs(svgBound.left - chartBound.left); + } + //If the parent container is an overflow
with scrollbars, subtract the scroll offsets. + //You need to also add any offset between the element and its containing
+ //Finally, add any offset of the containing
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; + } + + nv.tooltip.calcTooltipPosition([left,top], gravity, distance, container); + return nvtooltip; + }; + + nvtooltip.nvPointerEventsClass = nvPointerEventsClass; + + nvtooltip.content = function(_) { + if (!arguments.length) return content; + content = _; + return nvtooltip; + }; + + nvtooltip.contentGenerator = function(_) { + if (!arguments.length) return contentGenerator; + if (typeof _ === 'function') { + contentGenerator = _; + } + return nvtooltip; + }; + + nvtooltip.data = function(_) { + if (!arguments.length) return data; + data = _; + return nvtooltip; + }; + + nvtooltip.gravity = function(_) { + if (!arguments.length) return gravity; + gravity = _; + return nvtooltip; + }; + + nvtooltip.distance = function(_) { + if (!arguments.length) return distance; + distance = _; + return nvtooltip; + }; + + nvtooltip.snapDistance = function(_) { + if (!arguments.length) return snapDistance; + snapDistance = _; + return nvtooltip; + }; + + nvtooltip.classes = function(_) { + if (!arguments.length) return classes; + classes = _; + return nvtooltip; + }; + + nvtooltip.chartContainer = function(_) { + if (!arguments.length) return chartContainer; + chartContainer = _; + return nvtooltip; + }; + + nvtooltip.position = function(_) { + if (!arguments.length) return position; + position.left = (typeof _.left !== 'undefined') ? _.left : position.left; + position.top = (typeof _.top !== 'undefined') ? _.top : position.top; + return nvtooltip; + }; + + nvtooltip.fixedTop = function(_) { + if (!arguments.length) return fixedTop; + fixedTop = _; + return nvtooltip; + }; + + nvtooltip.enabled = function(_) { + if (!arguments.length) return enabled; + enabled = _; + return nvtooltip; + }; + + nvtooltip.valueFormatter = function(_) { + if (!arguments.length) return valueFormatter; + if (typeof _ === 'function') { + valueFormatter = _; + } + return nvtooltip; + }; + + nvtooltip.headerFormatter = function(_) { + if (!arguments.length) return headerFormatter; + if (typeof _ === 'function') { + headerFormatter = _; + } + return nvtooltip; + }; + + //id() is a read-only function. You can't use it to set the id. + nvtooltip.id = function() { + return id; + }; + + + return nvtooltip; + }; + + + //Original tooltip.show function. Kept for backward compatibility. + // pos = [left,top] + nv.tooltip.show = function(pos, content, gravity, dist, parentContainer, classes) { + + //Create new tooltip div if it doesn't exist on DOM. + var container = document.createElement('div'); + container.className = 'nvtooltip ' + (classes ? classes : 'xy-tooltip'); + + var body = parentContainer; + if ( !parentContainer || parentContainer.tagName.match(/g|svg/i)) { + //If the parent element is an SVG element, place tooltip in the element. + body = document.getElementsByTagName('body')[0]; + } + + container.style.left = 0; + container.style.top = 0; + container.style.opacity = 0; + container.innerHTML = content; + body.appendChild(container); + + //If the parent container is an overflow
with scrollbars, subtract the scroll offsets. + if (parentContainer) { + pos[0] = pos[0] - parentContainer.scrollLeft; + pos[1] = pos[1] - parentContainer.scrollTop; + } + nv.tooltip.calcTooltipPosition(pos, gravity, dist, container); + }; + + //Looks up the ancestry of a DOM element, and returns the first NON-svg node. + nv.tooltip.findFirstNonSVGParent = function(Elem) { + while(Elem.tagName.match(/^g|svg$/i) !== null) { + Elem = Elem.parentNode; + } + return Elem; + }; + + //Finds the total offsetTop of a given DOM element. + //Looks up the entire ancestry of an element, up to the first relatively positioned element. + nv.tooltip.findTotalOffsetTop = function ( Elem, initialTop ) { + var offsetTop = initialTop; + + do { + if( !isNaN( Elem.offsetTop ) ) { + offsetTop += (Elem.offsetTop); + } + } while( Elem = Elem.offsetParent ); + return offsetTop; + }; + + //Finds the total offsetLeft of a given DOM element. + //Looks up the entire ancestry of an element, up to the first relatively positioned element. + nv.tooltip.findTotalOffsetLeft = function ( Elem, initialLeft) { + var offsetLeft = initialLeft; + + do { + if( !isNaN( Elem.offsetLeft ) ) { + offsetLeft += (Elem.offsetLeft); + } + } while( Elem = Elem.offsetParent ); + return offsetLeft; + }; + + //Global utility function to render a tooltip on the DOM. + //pos = [left,top] coordinates of where to place the tooltip, relative to the SVG chart container. + //gravity = how to orient the tooltip + //dist = how far away from the mouse to place tooltip + //container = tooltip DIV + nv.tooltip.calcTooltipPosition = function(pos, gravity, dist, container) { + + var height = parseInt(container.offsetHeight), + width = parseInt(container.offsetWidth), + windowWidth = nv.utils.windowSize().width, + windowHeight = nv.utils.windowSize().height, + scrollTop = window.pageYOffset, + scrollLeft = window.pageXOffset, + left, top; + + windowHeight = window.innerWidth >= document.body.scrollWidth ? windowHeight : windowHeight - 16; + windowWidth = window.innerHeight >= document.body.scrollHeight ? windowWidth : windowWidth - 16; + + gravity = gravity || 's'; + dist = dist || 20; + + var tooltipTop = function ( Elem ) { + return nv.tooltip.findTotalOffsetTop(Elem, top); + }; + + var tooltipLeft = function ( Elem ) { + return nv.tooltip.findTotalOffsetLeft(Elem,left); + }; + + switch (gravity) { + case 'e': + left = pos[0] - width - dist; + top = pos[1] - (height / 2); + var tLeft = tooltipLeft(container); + var tTop = tooltipTop(container); + if (tLeft < scrollLeft) left = pos[0] + dist > scrollLeft ? pos[0] + dist : scrollLeft - tLeft + left; + if (tTop < scrollTop) top = scrollTop - tTop + top; + if (tTop + height > scrollTop + windowHeight) top = scrollTop + windowHeight - tTop + top - height; + break; + case 'w': + left = pos[0] + dist; + top = pos[1] - (height / 2); + var tLeft = tooltipLeft(container); + var tTop = tooltipTop(container); + if (tLeft + width > windowWidth) left = pos[0] - width - dist; + if (tTop < scrollTop) top = scrollTop + 5; + if (tTop + height > scrollTop + windowHeight) top = scrollTop + windowHeight - tTop + top - height; + break; + case 'n': + left = pos[0] - (width / 2) - 5; + top = pos[1] + dist; + var tLeft = tooltipLeft(container); + var tTop = tooltipTop(container); + if (tLeft < scrollLeft) left = scrollLeft + 5; + if (tLeft + width > windowWidth) left = left - width/2 + 5; + if (tTop + height > scrollTop + windowHeight) top = scrollTop + windowHeight - tTop + top - height; + break; + case 's': + left = pos[0] - (width / 2); + top = pos[1] - height - dist; + var tLeft = tooltipLeft(container); + var tTop = tooltipTop(container); + if (tLeft < scrollLeft) left = scrollLeft + 5; + if (tLeft + width > windowWidth) left = left - width/2 + 5; + if (scrollTop > tTop) top = scrollTop; + break; + case 'none': + left = pos[0]; + top = pos[1] - dist; + var tLeft = tooltipLeft(container); + var tTop = tooltipTop(container); + break; + } + + + container.style.left = left+'px'; + container.style.top = top+'px'; + container.style.opacity = 1; + container.style.position = 'absolute'; + + return container; + }; + + //Global utility function to remove tooltips from the DOM. + nv.tooltip.cleanup = function() { + + // Find the tooltips, mark them for removal by this class (so others cleanups won't find it) + var tooltips = document.getElementsByClassName('nvtooltip'); + var purging = []; + while(tooltips.length) { + purging.push(tooltips[0]); + tooltips[0].style.transitionDelay = '0 !important'; + tooltips[0].style.opacity = 0; + tooltips[0].className = 'nvtooltip-pending-removal'; + } + + setTimeout(function() { + + while (purging.length) { + var removeMe = purging.pop(); + removeMe.parentNode.removeChild(removeMe); + } + }, 500); + }; + +})(); + +nv.utils.windowSize = function() { + // Sane defaults + var size = {width: 640, height: 480}; + + // Earlier IE uses Doc.body + if (document.body && document.body.offsetWidth) { + size.width = document.body.offsetWidth; + size.height = document.body.offsetHeight; + } + + // 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; + } + + // Most recent browsers use + if (window.innerWidth && window.innerHeight) { + size.width = window.innerWidth; + size.height = window.innerHeight; + } + return (size); +}; + + + +// Easy way to bind multiple functions to window.onresize +// TODO: give a way to remove a function after its bound, other than removing all of them +nv.utils.windowResize = function(fun){ + if (fun === undefined) return; + var oldresize = window.onresize; + + window.onresize = function(e) { + if (typeof oldresize == 'function') oldresize(e); + fun(e); + } +} + +// Backwards compatible way to implement more d3-like coloring of graphs. +// If passed an array, wrap it in a function which implements the old default +// behavior +nv.utils.getColor = function(color) { + if (!arguments.length) return nv.utils.defaultColor(); //if you pass in nothing, get default colors back + + if( Object.prototype.toString.call( color ) === '[object Array]' ) + return function(d, i) { return d.color || color[i % color.length]; }; + else + return color; + //can't really help it if someone passes rubbish as color +} + +// Default color chooser uses the index of an object as before. +nv.utils.defaultColor = function() { + var colors = d3.scale.category20().range(); + return function(d, i) { return d.color || colors[i % colors.length] }; +} + + +// 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) { + getKey = getKey || function(series) { return series.key }; // use default series.key if getKey is undefined + defaultColors = defaultColors || d3.scale.category20().range(); //default color function + + var defIndex = defaultColors.length; //current default color (going in reverse) + + return function(series, index) { + var key = getKey(series); + + if (!defIndex) defIndex = defaultColors.length; //used all the default colors, start over + + if (typeof dictionary[key] !== "undefined") + return (typeof dictionary[key] === "function") ? dictionary[key]() : dictionary[key]; + else + return defaultColors[--defIndex]; // no match in dictionary, use default color + } +} + + + +// 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) { + d3.selectAll(links).on("click", function() { + history.pushState(this.href, this.textContent, this.href); + load(this.href); + d3.event.preventDefault(); + }); + + function load(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.select(window).on("popstate", function() { + if (d3.event.state) load(d3.event.state); + }); +} + +/* For situations where 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 (svgTextElem instanceof d3.selection) { + var fontSize = parseInt(svgTextElem.style("font-size").replace("px","")); + 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) return 0; + + return n; +}; + +/* +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; +};nv.models.axis = function() { + "use strict"; + //============================================================ + // Public Variables with Default Settings + //------------------------------------------------------------ + + var axis = d3.svg.axis() + ; + + var margin = {top: 0, right: 0, bottom: 0, left: 0} + , width = 75 //only used for tickLabel currently + , height = 60 //only used for tickLabel currently + , scale = d3.scale.linear() + , axisLabelText = null + , showMaxMin = true //TODO: showMaxMin should be disabled on all ordinal scaled axes + , highlightZero = true + , rotateLabels = 0 + , rotateYLabel = true + , staggerLabels = false + , isOrdinal = false + , ticks = null + ; + + axis + .scale(scale) + .orient('bottom') + .tickFormat(function(d) { return d }) + ; + + //============================================================ + + + //============================================================ + // Private Variables + //------------------------------------------------------------ + + var scale0; + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + var container = d3.select(this); + + + //------------------------------------------------------------ + // 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.transition().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(); + switch (axis.orient()) { + case 'top': + axisLabel.enter().append('text').attr('class', 'nv-axislabel'); + var w = (scale.range().length==2) ? scale.range()[1] : (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) { + var axisMaxMin = wrap.selectAll('g.nv-axisMaxMin') + .data(scale.domain()); + axisMaxMin.enter().append('g').attr('class', 'nv-axisMaxMin').append('text'); + axisMaxMin.exit().remove(); + axisMaxMin + .attr('transform', function(d,i) { + return 'translate(' + scale(d) + ',0)' + }) + .select('text') + .attr('dy', '0em') + .attr('y', -axis.tickPadding()) + .attr('text-anchor', 'middle') + .text(function(d,i) { + var v = fmt(d); + return ('' + v).match('NaN') ? '' : v; + }); + axisMaxMin.transition() + .attr('transform', function(d,i) { + return 'translate(' + scale.range()[i] + ',0)' + }); + } + break; + case 'bottom': + var xLabelMargin = 36; + var maxTextWidth = 30; + var xTicks = g.selectAll('g').select("text"); + if (rotateLabels%360) { + //Calculate the longest xTick width + xTicks.each(function(d,i){ + var width = this.getBBox().width; + if(width > maxTextWidth) maxTextWidth = width; + }); + //Convert to radians before calculating sin. Add 30 to margin for healthy padding. + var sin = Math.abs(Math.sin(rotateLabels*Math.PI/180)); + var xLabelMargin = (sin ? sin*maxTextWidth : maxTextWidth)+30; + //Rotate all xTicks + xTicks + .attr('transform', function(d,i,j) { return 'rotate(' + rotateLabels + ' 0,0)' }) + .style('text-anchor', rotateLabels%360 > 0 ? 'start' : 'end'); + } + axisLabel.enter().append('text').attr('class', 'nv-axislabel'); + var w = (scale.range().length==2) ? scale.range()[1] : (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) { + var 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', 'nv-axisMaxMin').append('text'); + axisMaxMin.exit().remove(); + axisMaxMin + .attr('transform', function(d,i) { + return 'translate(' + (scale(d) + (isOrdinal ? scale.rangeBand() / 2 : 0)) + ',0)' + }) + .select('text') + .attr('dy', '.71em') + .attr('y', axis.tickPadding()) + .attr('transform', function(d,i,j) { return 'rotate(' + rotateLabels + ' 0,0)' }) + .style('text-anchor', rotateLabels ? (rotateLabels%360 > 0 ? 'start' : 'end') : 'middle') + .text(function(d,i) { + var v = fmt(d); + return ('' + v).match('NaN') ? '' : v; + }); + axisMaxMin.transition() + .attr('transform', function(d,i) { + //return 'translate(' + scale.range()[i] + ',0)' + //return 'translate(' + scale(d) + ',0)' + return 'translate(' + (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 ? (scale.range()[0] / 2) : axis.tickPadding()); + if (showMaxMin) { + var axisMaxMin = wrap.selectAll('g.nv-axisMaxMin') + .data(scale.domain()); + axisMaxMin.enter().append('g').attr('class', 'nv-axisMaxMin').append('text') + .style('opacity', 0); + axisMaxMin.exit().remove(); + axisMaxMin + .attr('transform', function(d,i) { + return 'translate(0,' + 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.transition() + .attr('transform', function(d,i) { + return 'translate(0,' + 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.getBBox().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) + 12) : -10) //TODO: consider calculating this based on largest tick width... OR at least expose this on chart + .attr('x', rotateYLabel ? (-scale.range()[0] / 2) : -axis.tickPadding()); + if (showMaxMin) { + var axisMaxMin = wrap.selectAll('g.nv-axisMaxMin') + .data(scale.domain()); + axisMaxMin.enter().append('g').attr('class', 'nv-axisMaxMin').append('text') + .style('opacity', 0); + axisMaxMin.exit().remove(); + axisMaxMin + .attr('transform', function(d,i) { + return 'translate(0,' + 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.transition() + .attr('transform', function(d,i) { + return 'translate(0,' + 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.getBBox().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.getBBox().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) + } + }); + g.selectAll('g') // the g's wrapping each tick + .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 line ... Maybe should not be an option and should just be in CSS? + if (highlightZero) + g.selectAll('.tick') + .filter(function(d) { return !parseFloat(Math.round(d.__data__*100000)/1000000) && (d.__data__ !== undefined) }) //this is because sometimes the 0 tick is a very small fraction, TODO: think of cleaner technique + .classed('zero', true); + + //store old scales for use in transitions on update + scale0 = scale.copy(); + + }); + + return chart; + } + + + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + + // expose chart's sub-components + chart.axis = axis; + + d3.rebind(chart, axis, 'orient', 'tickValues', 'tickSubdivide', 'tickSize', 'tickPadding', 'tickFormat'); + d3.rebind(chart, scale, 'domain', 'range', 'rangeBand', 'rangeBands'); //these are also accessible by chart.scale(), but added common ones directly for ease of use + + chart.options = nv.utils.optionsFunc.bind(chart); + + 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.ticks = function(_) { + if (!arguments.length) return ticks; + ticks = _; + return chart; + }; + + chart.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.axisLabel = function(_) { + if (!arguments.length) return axisLabelText; + axisLabelText = _; + return chart; + } + + chart.showMaxMin = function(_) { + if (!arguments.length) return showMaxMin; + showMaxMin = _; + return chart; + } + + chart.highlightZero = function(_) { + if (!arguments.length) return highlightZero; + highlightZero = _; + return chart; + } + + chart.scale = function(_) { + if (!arguments.length) return scale; + scale = _; + axis.scale(scale); + isOrdinal = typeof scale.rangeBands === 'function'; + d3.rebind(chart, scale, 'domain', 'range', 'rangeBand', 'rangeBands'); + return chart; + } + + chart.rotateYLabel = function(_) { + if(!arguments.length) return rotateYLabel; + rotateYLabel = _; + return chart; + } + + chart.rotateLabels = function(_) { + if(!arguments.length) return rotateLabels; + rotateLabels = _; + return chart; + } + + chart.staggerLabels = function(_) { + if (!arguments.length) return staggerLabels; + staggerLabels = _; + return 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 } + , 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 + , tickFormat = null + , color = nv.utils.getColor(['#1f77b4']) + , dispatch = d3.dispatch('elementMouseover', 'elementMouseout') + ; + + //============================================================ + + + 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); + + 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'); + gEnter.append('path').attr('class', 'nv-markerTriangle'); + + 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) + /* + .attr('x', rangeMin < 0 ? + rangeMax > 0 ? + x1(rangeMin) + : x1(rangeMax) + : x1(0)) + */ + + g.select('rect.nv-rangeAvg') + .attr('height', availableHeight) + .attr('width', w1(rangeAvg)) + .attr('x', xp1(rangeAvg)) + .datum(rangeAvg) + /* + .attr('width', rangeMax <= 0 ? + x1(rangeMax) - x1(rangeAvg) + : x1(rangeAvg) - x1(rangeMin)) + .attr('x', rangeMax <= 0 ? + x1(rangeAvg) + : x1(rangeMin)) + */ + + 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) + /* + .attr('width', rangeMax <= 0 ? + x1(rangeAvg) - x1(rangeMin) + : x1(rangeMax) - x1(rangeAvg)) + .attr('x', rangeMax <= 0 ? + x1(rangeMin) + : x1(rangeAvg)) + */ + + 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', + pos: [x1(measurez[0]), availableHeight/2] + }) + }) + .on('mouseout', function() { + dispatch.elementMouseout({ + value: measurez[0], + label: measureLabelz[0] || 'Current' + }) + }) + + var h3 = availableHeight / 6; + if (markerz[0]) { + g.selectAll('path.nv-markerTriangle') + .attr('transform', function(d) { return 'translate(' + x1(markerz[0]) + ',' + (availableHeight / 2) + ')' }) + .attr('d', 'M0,' + h3 + 'L' + h3 + ',' + (-h3) + ' ' + (-h3) + ',' + (-h3) + 'Z') + .on('mouseover', function() { + dispatch.elementMouseover({ + value: markerz[0], + label: markerLabelz[0] || 'Previous', + pos: [x1(markerz[0]), availableHeight/2] + }) + }) + .on('mouseout', function() { + dispatch.elementMouseout({ + value: markerz[0], + label: markerLabelz[0] || 'Previous' + }) + }); + } else { + g.selectAll('path.nv-markerTriangle').remove(); + } + + + 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, + pos: [x1(d), availableHeight/2] + }) + }) + .on('mouseout', function(d,i) { + var label = rangeLabelz[i] || (!i ? "Maximum" : i == 1 ? "Mean" : "Minimum"); + + dispatch.elementMouseout({ + value: d, + label: label + }) + }) + +/* // THIS IS THE PREVIOUS BULLET IMPLEMENTATION, WILL REMOVE SHORTLY + // Update the range rects. + var range = g.selectAll('rect.nv-range') + .data(rangez); + + range.enter().append('rect') + .attr('class', function(d, i) { return 'nv-range nv-s' + i; }) + .attr('width', w0) + .attr('height', availableHeight) + .attr('x', reverse ? x0 : 0) + .on('mouseover', function(d,i) { + dispatch.elementMouseover({ + value: d, + label: (i <= 0) ? 'Maximum' : (i > 1) ? 'Minimum' : 'Mean', //TODO: make these labels a variable + pos: [x1(d), availableHeight/2] + }) + }) + .on('mouseout', function(d,i) { + dispatch.elementMouseout({ + value: d, + label: (i <= 0) ? 'Minimum' : (i >=1) ? 'Maximum' : 'Mean' //TODO: make these labels a variable + }) + }) + + d3.transition(range) + .attr('x', reverse ? x1 : 0) + .attr('width', w1) + .attr('height', availableHeight); + + + // Update the measure rects. + var measure = g.selectAll('rect.nv-measure') + .data(measurez); + + measure.enter().append('rect') + .attr('class', function(d, i) { return 'nv-measure nv-s' + i; }) + .style('fill', function(d,i) { return color(d,i ) }) + .attr('width', w0) + .attr('height', availableHeight / 3) + .attr('x', reverse ? x0 : 0) + .attr('y', availableHeight / 3) + .on('mouseover', function(d) { + dispatch.elementMouseover({ + value: d, + label: 'Current', //TODO: make these labels a variable + pos: [x1(d), availableHeight/2] + }) + }) + .on('mouseout', function(d) { + dispatch.elementMouseout({ + value: d, + label: 'Current' //TODO: make these labels a variable + }) + }) + + d3.transition(measure) + .attr('width', w1) + .attr('height', availableHeight / 3) + .attr('x', reverse ? x1 : 0) + .attr('y', availableHeight / 3); + + + + // Update the marker lines. + var marker = g.selectAll('path.nv-markerTriangle') + .data(markerz); + + var h3 = availableHeight / 6; + marker.enter().append('path') + .attr('class', 'nv-markerTriangle') + .attr('transform', function(d) { return 'translate(' + x0(d) + ',' + (availableHeight / 2) + ')' }) + .attr('d', 'M0,' + h3 + 'L' + h3 + ',' + (-h3) + ' ' + (-h3) + ',' + (-h3) + 'Z') + .on('mouseover', function(d,i) { + dispatch.elementMouseover({ + value: d, + label: 'Previous', + pos: [x1(d), availableHeight/2] + }) + }) + .on('mouseout', function(d,i) { + dispatch.elementMouseout({ + value: d, + label: 'Previous' + }) + }); + + d3.transition(marker) + .attr('transform', function(d) { return 'translate(' + (x1(d) - x1(0)) + ',' + (availableHeight / 2) + ')' }); + + marker.exit().remove(); +*/ + + }); + + // d3.timer.flush(); // Not needed? + + return chart; + } + + + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + + chart.dispatch = dispatch; + + chart.options = nv.utils.optionsFunc.bind(chart); + + // left, right, top, bottom + chart.orient = function(_) { + if (!arguments.length) return orient; + orient = _; + reverse = orient == 'right' || orient == 'bottom'; + return chart; + }; + + // ranges (bad, satisfactory, good) + chart.ranges = function(_) { + if (!arguments.length) return ranges; + ranges = _; + return chart; + }; + + // markers (previous, goal) + chart.markers = function(_) { + if (!arguments.length) return markers; + markers = _; + return chart; + }; + + // measures (actual, forecast) + chart.measures = function(_) { + if (!arguments.length) return measures; + measures = _; + return chart; + }; + + chart.forceX = function(_) { + if (!arguments.length) return forceX; + forceX = _; + return chart; + }; + + chart.width = function(_) { + if (!arguments.length) return width; + width = _; + return chart; + }; + + chart.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + 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.tickFormat = function(_) { + if (!arguments.length) return tickFormat; + tickFormat = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + return 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 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 } + , measures = function(d) { return d.measures } + , width = null + , height = 55 + , tickFormat = null + , tooltips = true + , tooltip = function(key, x, y, e, graph) { + return '

' + x + '

' + + '

' + y + '

' + } + , noData = 'No Data Available.' + , dispatch = d3.dispatch('tooltipShow', 'tooltipHide') + ; + + //============================================================ + + + //============================================================ + // Private Variables + //------------------------------------------------------------ + + var showTooltip = function(e, offsetElement) { + var left = e.pos[0] + ( offsetElement.offsetLeft || 0 ) + margin.left, + top = e.pos[1] + ( offsetElement.offsetTop || 0) + margin.top, + content = tooltip(e.key, e.label, e.value, e, chart); + + nv.tooltip.show([left, top], content, e.value < 0 ? 'e' : 'w', null, offsetElement); + }; + + //============================================================ + + + function chart(selection) { + selection.each(function(d, i) { + var container = d3.select(this); + + var availableWidth = (width || parseInt(container.style('width')) || 960) + - margin.left - margin.right, + 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)) { + 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', 18 + margin.top + availableHeight / 2) + .text(function(d) { return d }); + + 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; + + /* + // Derive width-scales from the x-scales. + var w0 = bulletWidth(x0), + w1 = bulletWidth(x1); + + function bulletWidth(x) { + var x0 = x(0); + return function(d) { + return Math.abs(x(d) - x(0)); + }; + } + + function bulletTranslate(x) { + return function(d) { + return 'translate(' + x(d) + ',0)'; + }; + } + */ + + 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( 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(); + + + //============================================================ + // Event Handling/Dispatching (in chart's scope) + //------------------------------------------------------------ + + dispatch.on('tooltipShow', function(e) { + e.key = d.title; + if (tooltips) showTooltip(e, that.parentNode); + }); + + //============================================================ + + }); + + d3.timer.flush(); + + return chart; + } + + + //============================================================ + // Event Handling/Dispatching (out of chart's scope) + //------------------------------------------------------------ + + bullet.dispatch.on('elementMouseover.tooltip', function(e) { + dispatch.tooltipShow(e); + }); + + bullet.dispatch.on('elementMouseout.tooltip', function(e) { + dispatch.tooltipHide(e); + }); + + dispatch.on('tooltipHide', function() { + if (tooltips) nv.tooltip.cleanup(); + }); + + //============================================================ + + + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + + chart.dispatch = dispatch; + chart.bullet = bullet; + + d3.rebind(chart, bullet, 'color'); + + chart.options = nv.utils.optionsFunc.bind(chart); + + // left, right, top, bottom + chart.orient = function(x) { + if (!arguments.length) return orient; + orient = x; + reverse = orient == 'right' || orient == 'bottom'; + return chart; + }; + + // ranges (bad, satisfactory, good) + chart.ranges = function(x) { + if (!arguments.length) return ranges; + ranges = x; + return chart; + }; + + // markers (previous, goal) + chart.markers = function(x) { + if (!arguments.length) return markers; + markers = x; + return chart; + }; + + // measures (actual, forecast) + chart.measures = function(x) { + if (!arguments.length) return measures; + measures = x; + return chart; + }; + + chart.width = function(x) { + if (!arguments.length) return width; + width = x; + return chart; + }; + + chart.height = function(x) { + if (!arguments.length) return height; + height = x; + return chart; + }; + + 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.tickFormat = function(x) { + if (!arguments.length) return tickFormat; + tickFormat = x; + return chart; + }; + + chart.tooltips = function(_) { + if (!arguments.length) return tooltips; + tooltips = _; + return chart; + }; + + chart.tooltipContent = function(_) { + if (!arguments.length) return tooltip; + tooltip = _; + return chart; + }; + + chart.noData = function(_) { + if (!arguments.length) return noData; + noData = _; + return 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() + ; + + 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 + , tooltips = true + , showControls = true + , useInteractiveGuideline = false + , rescaleY = true + , tooltip = function(key, x, y, e, graph) { + return '

' + key + '

' + + '

' + y + ' at ' + x + '

' + } + , x //can be accessed via chart.xScale() + , y //can be accessed via chart.yScale() + , id = lines.id() + , state = { index: 0, rescaleY: rescaleY } + , defaultState = null + , noData = 'No Data Available.' + , average = function(d) { return d.average } + , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'stateChange', 'changeState') + , transitionDuration = 250 + ; + + xAxis + .orient('bottom') + .tickPadding(7) + ; + yAxis + .orient((rightAlignYAxis) ? 'right' : 'left') + ; + + //============================================================ + controls.updateState(false); + + //============================================================ + // Private Variables + //------------------------------------------------------------ + + var dx = d3.scale.linear() + , index = {i: 0, x: 0} + ; + + var showTooltip = function(e, offsetElement) { + var left = e.pos[0] + ( offsetElement.offsetLeft || 0 ), + top = e.pos[1] + ( offsetElement.offsetTop || 0), + x = xAxis.tickFormat()(lines.x()(e.point, e.pointIndex)), + y = yAxis.tickFormat()(lines.y()(e.point, e.pointIndex)), + content = tooltip(e.series.key, x, y, e, chart); + + nv.tooltip.show([left, top], content, null, null, offsetElement); + }; + +/* + //Moved to see if we can get better behavior to fix issue #315 + 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) { + d.x += d3.event.dx; + d.i = Math.round(dx.invert(d.x)); + + d3.select(this).attr('transform', 'translate(' + dx(d.i) + ',0)'); + chart.update(); + } + + function dragEnd(d,i) { + d3.select(chart.container) + .style('cursor', 'auto'); + chart.update(); + } +*/ + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + var container = d3.select(this).classed('nv-chart-' + id, true), + that = this; + + 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() { 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]; + } + } + + 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) { + 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 = 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 = (height || parseInt(container.style('height')) || 400) + - margin.top - margin.bottom; + } + + 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']); + 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.'); + } + + //------------------------------------------------------------ + // Main Chart Component(s) + + //------------------------------------------------------------ + //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 })); + + //d3.transition(linesWrap).call(lines); + linesWrap.call(lines); + + /*Handle average lines [AN-612] ----------------------------*/ + + //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) + //Suggest how many ticks based on the chart width and D3 should listen (70 is the optimal number for MM/DD/YY dates) + .ticks( Math.min(data[0].values.length,availableWidth/70) ) + .tickSize(-availableHeight, 0); + + g.select('.nv-x.nv-axis') + .attr('transform', 'translate(0,' + y.range()[0] + ')'); + d3.transition(g.select('.nv-x.nv-axis')) + .call(xAxis); + } + + + if (showYAxis) { + yAxis + .scale(y) + .ticks( availableHeight / 36 ) + .tickSize( -availableWidth, 0); + + d3.transition(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.transitionDuration(); + chart.transitionDuration(0); + chart.update(); + chart.transitionDuration(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) { + state.disabled = newState.disabled; + 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) + }); + }); + + var xValue = xAxis.tickFormat()(chart.x()(singlePoint,pointIndex)); + interactiveLayer.tooltip + .position({left: pointXLocation + margin.left, top: e.mouseY + margin.top}) + .chartContainer(that.parentNode) + .enabled(tooltips) + .valueFormatter(function(d,i) { + return yAxis.tickFormat()(d); + }) + .data( + { + value: xValue, + series: allData + } + )(); + + interactiveLayer.renderGuideLine(pointXLocation); + + }); + + interactiveLayer.dispatch.on("elementMouseout",function(e) { + dispatch.tooltipHide(); + lines.clearHighlights(); + }); + + dispatch.on('tooltipShow', function(e) { + if (tooltips) showTooltip(e, that.parentNode); + }); + + + // 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(); + }); + + //============================================================ + + }); + + return chart; + } + + + //============================================================ + // Event Handling/Dispatching (out of chart's scope) + //------------------------------------------------------------ + + lines.dispatch.on('elementMouseover.tooltip', function(e) { + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top]; + dispatch.tooltipShow(e); + }); + + lines.dispatch.on('elementMouseout.tooltip', function(e) { + dispatch.tooltipHide(e); + }); + + dispatch.on('tooltipHide', function() { + if (tooltips) nv.tooltip.cleanup(); + }); + + //============================================================ + + + //============================================================ + // 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; + + d3.rebind(chart, lines, 'defined', 'isArea', 'x', 'y', 'xScale','yScale', 'size', 'xDomain', 'yDomain', 'xRange', 'yRange', 'forceX', 'forceY', 'interactive', 'clipEdge', 'clipVoronoi','useVoronoi', 'id'); + + chart.options = nv.utils.optionsFunc.bind(chart); + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + legend.color(color); + return chart; + }; + + chart.rescaleY = function(_) { + if (!arguments.length) return rescaleY; + rescaleY = _ + return rescaleY; + }; + + chart.showControls = function(_) { + if (!arguments.length) return showControls; + showControls = _; + return chart; + }; + + chart.useInteractiveGuideline = function(_) { + if(!arguments.length) return useInteractiveGuideline; + useInteractiveGuideline = _; + if (_ === true) { + chart.interactive(false); + chart.useVoronoi(false); + } + return chart; + }; + + chart.showLegend = function(_) { + if (!arguments.length) return showLegend; + showLegend = _; + return chart; + }; + + chart.showXAxis = function(_) { + if (!arguments.length) return showXAxis; + showXAxis = _; + return chart; + }; + + chart.showYAxis = function(_) { + if (!arguments.length) return showYAxis; + showYAxis = _; + return chart; + }; + + chart.rightAlignYAxis = function(_) { + if(!arguments.length) return rightAlignYAxis; + rightAlignYAxis = _; + yAxis.orient( (_) ? 'right' : 'left'); + return chart; + }; + + chart.tooltips = function(_) { + if (!arguments.length) return tooltips; + tooltips = _; + return chart; + }; + + chart.tooltipContent = function(_) { + if (!arguments.length) return tooltip; + tooltip = _; + return chart; + }; + + chart.state = function(_) { + if (!arguments.length) return state; + state = _; + return chart; + }; + + chart.defaultState = function(_) { + if (!arguments.length) return defaultState; + defaultState = _; + return chart; + }; + + chart.noData = function(_) { + if (!arguments.length) return noData; + noData = _; + return chart; + }; + + chart.average = function(_) { + if(!arguments.length) return average; + average = _; + return chart; + }; + + chart.transitionDuration = function(_) { + if (!arguments.length) return transitionDuration; + transitionDuration = _; + return chart; + }; + + //============================================================ + + + //============================================================ + // Functions + //------------------------------------------------------------ + + /* Normalize the data according to an index point. */ + function indexify(idx, data) { + return data.map(function(line, i) { + if (!line.values) { + return line; + } + var v = lines.y()(line.values[idx], idx); + + //TODO: implement check below, and disable series if series loses 100% or more cause divide by 0 issue + if (v < -.95) { + //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': (lines.y()(point, pointIndex) - v) / (1 + v) }; + return point; + }) + + return line; + }) + } + + //============================================================ + + + 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 + , 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') + , rectClass = 'discreteBar' + ; + + //============================================================ + + + //============================================================ + // Private Variables + //------------------------------------------------------------ + + var x0, y0; + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + var availableWidth = width - margin.left - margin.right, + availableHeight = height - margin.top - margin.bottom, + container = d3.select(this); + + + //add series index to each data point for reference + data = data.map(function(series, i) { + series.values = series.values.map(function(point) { + point.series = i; + return point; + }); + return series; + }); + + + //------------------------------------------------------------ + // 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() + .transition() + .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 + .transition() + .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({ + value: getY(d,i), + point: d, + series: data[d.series], + pos: [x(getX(d,i)) + (x.rangeBand() * (d.series + .5) / data.length), y(getY(d,i))], // TODO: Figure out why the value appears to be shifted + pointIndex: i, + seriesIndex: d.series, + e: d3.event + }); + }) + .on('mouseout', function(d,i) { + d3.select(this).classed('hover', false); + dispatch.elementMouseout({ + value: getY(d,i), + point: d, + series: data[d.series], + pointIndex: i, + seriesIndex: d.series, + e: d3.event + }); + }) + .on('click', function(d,i) { + dispatch.elementClick({ + value: getY(d,i), + point: d, + series: data[d.series], + pos: [x(getX(d,i)) + (x.rangeBand() * (d.series + .5) / data.length), y(getY(d,i))], // TODO: Figure out why the value appears to be shifted + pointIndex: i, + seriesIndex: d.series, + e: d3.event + }); + d3.event.stopPropagation(); + }) + .on('dblclick', function(d,i) { + dispatch.elementDblClick({ + value: getY(d,i), + point: d, + series: data[d.series], + pos: [x(getX(d,i)) + (x.rangeBand() * (d.series + .5) / data.length), y(getY(d,i))], // TODO: Figure out why the value appears to be shifted + pointIndex: i, + seriesIndex: d.series, + e: d3.event + }); + 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)) }) + .transition() + .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) + .transition() + .attr('width', x.rangeBand() * .9 / data.length); + bars.transition() + //.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(); + + }); + + return chart; + } + + + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + + chart.dispatch = dispatch; + + chart.options = nv.utils.optionsFunc.bind(chart); + + chart.x = function(_) { + if (!arguments.length) return getX; + getX = _; + return chart; + }; + + chart.y = function(_) { + if (!arguments.length) return getY; + getY = _; + return chart; + }; + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.xScale = function(_) { + if (!arguments.length) return x; + x = _; + return chart; + }; + + chart.yScale = function(_) { + if (!arguments.length) return y; + y = _; + return chart; + }; + + chart.xDomain = function(_) { + if (!arguments.length) return xDomain; + xDomain = _; + return chart; + }; + + chart.yDomain = function(_) { + if (!arguments.length) return yDomain; + yDomain = _; + return chart; + }; + + chart.xRange = function(_) { + if (!arguments.length) return xRange; + xRange = _; + return chart; + }; + + chart.yRange = function(_) { + if (!arguments.length) return yRange; + yRange = _; + return chart; + }; + + chart.forceY = function(_) { + if (!arguments.length) return forceY; + forceY = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + return chart; + }; + + chart.id = function(_) { + if (!arguments.length) return id; + id = _; + return chart; + }; + + chart.showValues = function(_) { + if (!arguments.length) return showValues; + showValues = _; + return chart; + }; + + chart.valueFormat= function(_) { + if (!arguments.length) return valueFormat; + valueFormat = _; + return chart; + }; + + chart.rectClass= function(_) { + if (!arguments.length) return rectClass; + rectClass = _; + return 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() + ; + + 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 + , tooltips = true + , tooltip = function(key, x, y, e, graph) { + return '

' + x + '

' + + '

' + y + '

' + } + , x + , y + , noData = "No Data Available." + , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'beforeUpdate') + , transitionDuration = 250 + ; + + xAxis + .orient('bottom') + .highlightZero(false) + .showMaxMin(false) + .tickFormat(function(d) { return d }) + ; + yAxis + .orient((rightAlignYAxis) ? 'right' : 'left') + .tickFormat(d3.format(',.1f')) + ; + + //============================================================ + + + //============================================================ + // Private Variables + //------------------------------------------------------------ + + var showTooltip = function(e, offsetElement) { + var left = e.pos[0] + ( offsetElement.offsetLeft || 0 ), + top = e.pos[1] + ( offsetElement.offsetTop || 0), + x = xAxis.tickFormat()(discretebar.x()(e.point, e.pointIndex)), + y = yAxis.tickFormat()(discretebar.y()(e.point, e.pointIndex)), + content = tooltip(e.series.key, x, y, e, chart); + + nv.tooltip.show([left, top], content, e.value < 0 ? 'n' : 's', null, offsetElement); + }; + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + var container = d3.select(this), + that = this; + + 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(transitionDuration).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) { + 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 = 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'); + 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( availableWidth / 100 ) + .tickSize(-availableHeight, 0); + + g.select('.nv-x.nv-axis') + .attr('transform', 'translate(0,' + (y.range()[0] + ((discretebar.showValues() && y.domain()[0] < 0) ? 16 : 0)) + ')'); + //d3.transition(g.select('.nv-x.nv-axis')) + g.select('.nv-x.nv-axis').transition() + .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( availableHeight / 36 ) + .tickSize( -availableWidth, 0); + + g.select('.nv-y.nv-axis').transition() + .call(yAxis); + } + + //------------------------------------------------------------ + + + //============================================================ + // Event Handling/Dispatching (in chart's scope) + //------------------------------------------------------------ + + dispatch.on('tooltipShow', function(e) { + if (tooltips) showTooltip(e, that.parentNode); + }); + + //============================================================ + + + }); + + return chart; + } + + //============================================================ + // Event Handling/Dispatching (out of chart's scope) + //------------------------------------------------------------ + + discretebar.dispatch.on('elementMouseover.tooltip', function(e) { + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top]; + dispatch.tooltipShow(e); + }); + + discretebar.dispatch.on('elementMouseout.tooltip', function(e) { + dispatch.tooltipHide(e); + }); + + dispatch.on('tooltipHide', function() { + if (tooltips) nv.tooltip.cleanup(); + }); + + //============================================================ + + + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + + // expose chart's sub-components + chart.dispatch = dispatch; + chart.discretebar = discretebar; + chart.xAxis = xAxis; + chart.yAxis = yAxis; + + d3.rebind(chart, discretebar, 'x', 'y', 'xDomain', 'yDomain', 'xRange', 'yRange', 'forceX', 'forceY', 'id', 'showValues', 'valueFormat'); + + chart.options = nv.utils.optionsFunc.bind(chart); + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + discretebar.color(color); + return chart; + }; + + chart.showXAxis = function(_) { + if (!arguments.length) return showXAxis; + showXAxis = _; + return chart; + }; + + chart.showYAxis = function(_) { + if (!arguments.length) return showYAxis; + showYAxis = _; + return chart; + }; + + chart.rightAlignYAxis = function(_) { + if(!arguments.length) return rightAlignYAxis; + rightAlignYAxis = _; + yAxis.orient( (_) ? 'right' : 'left'); + return chart; + }; + + chart.staggerLabels = function(_) { + if (!arguments.length) return staggerLabels; + staggerLabels = _; + return chart; + }; + + chart.tooltips = function(_) { + if (!arguments.length) return tooltips; + tooltips = _; + return chart; + }; + + chart.tooltipContent = function(_) { + if (!arguments.length) return tooltip; + tooltip = _; + return chart; + }; + + chart.noData = function(_) { + if (!arguments.length) return noData; + noData = _; + return chart; + }; + + chart.transitionDuration = function(_) { + if (!arguments.length) return transitionDuration; + transitionDuration = _; + return 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 + ; + + //============================================================ + + + //============================================================ + // Private Variables + //------------------------------------------------------------ + + var scale0; + + //============================================================ + + + function chart(selection) { + 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); + + + //------------------------------------------------------------ + // 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)) }) + distWrap.exit().selectAll('line.nv-dist' + axis) + .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); + 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(); + + }); + + return chart; + } + + + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + chart.options = nv.utils.optionsFunc.bind(chart); + + 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; + }; + //============================================================ + + + 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 = 960 + , height = 500 + , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one + , 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') + , interactive = true + ; + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + var availableWidth = width - margin.left - margin.right, + availableHeight = height - margin.top - margin.bottom, + container = d3.select(this); + + + //------------------------------------------------------------ + // 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(); + + + var barsEnter = bars.enter().append('rect') + //.attr('class', function(d,i,j) { return (getY(d,i) < 0 ? 'nv-bar negative' : 'nv-bar positive') + ' nv-bar-' + j + '-' + i }) + .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({ + point: d, + series: data[0], + pos: [x(getX(d,i)), y(getY(d,i))], // TODO: Figure out why the value appears to be shifted + pointIndex: i, + seriesIndex: 0, + e: d3.event + }); + + }) + .on('mouseout', function(d,i) { + if (!interactive) return; + d3.select(this).classed('hover', false); + dispatch.elementMouseout({ + point: d, + series: data[0], + pointIndex: i, + seriesIndex: 0, + e: d3.event + }); + }) + .on('click', function(d,i) { + if (!interactive) return; + dispatch.elementClick({ + //label: d[label], + value: getY(d,i), + data: d, + index: i, + pos: [x(getX(d,i)), y(getY(d,i))], + e: d3.event, + id: id + }); + d3.event.stopPropagation(); + }) + .on('dblclick', function(d,i) { + if (!interactive) return; + dispatch.elementDblClick({ + //label: d[label], + value: getY(d,i), + data: d, + index: i, + pos: [x(getX(d,i)), y(getY(d,i))], + e: d3.event, + id: id + }); + 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 }) + .transition() + .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.transition() + .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)) }); + + }); + + return chart; + } + + //Create methods to allow outside functions to highlight a specific bar. + chart.highlightPoint = function(pointIndex, isHoverOver) { + d3.select(".nv-historicalBar-" + id) + .select(".nv-bars .nv-bar-0-" + pointIndex) + .classed("hover", isHoverOver) + ; + }; + + chart.clearHighlights = function() { + d3.select(".nv-historicalBar-" + id) + .select(".nv-bars .nv-bar.hover") + .classed("hover", false) + ; + }; + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + + chart.dispatch = dispatch; + + chart.options = nv.utils.optionsFunc.bind(chart); + + chart.x = function(_) { + if (!arguments.length) return getX; + getX = _; + return chart; + }; + + chart.y = function(_) { + if (!arguments.length) return getY; + getY = _; + return chart; + }; + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.xScale = function(_) { + if (!arguments.length) return x; + x = _; + return chart; + }; + + chart.yScale = function(_) { + if (!arguments.length) return y; + y = _; + return chart; + }; + + chart.xDomain = function(_) { + if (!arguments.length) return xDomain; + xDomain = _; + return chart; + }; + + chart.yDomain = function(_) { + if (!arguments.length) return yDomain; + yDomain = _; + return chart; + }; + + chart.xRange = function(_) { + if (!arguments.length) return xRange; + xRange = _; + return chart; + }; + + chart.yRange = function(_) { + if (!arguments.length) return yRange; + yRange = _; + return chart; + }; + + chart.forceX = function(_) { + if (!arguments.length) return forceX; + forceX = _; + return chart; + }; + + chart.forceY = function(_) { + if (!arguments.length) return forceY; + forceY = _; + return chart; + }; + + chart.padData = function(_) { + if (!arguments.length) return padData; + padData = _; + return chart; + }; + + chart.clipEdge = function(_) { + if (!arguments.length) return clipEdge; + clipEdge = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + return chart; + }; + + chart.id = function(_) { + if (!arguments.length) return id; + id = _; + return chart; + }; + + chart.interactive = function(_) { + if(!arguments.length) return interactive; + interactive = false; + return chart; + }; + + //============================================================ + + + return chart; +} + +nv.models.historicalBarChart = function() { + "use strict"; + //============================================================ + // Public Variables with Default Settings + //------------------------------------------------------------ + + var bars = nv.models.historicalBar() + , xAxis = nv.models.axis() + , yAxis = nv.models.axis() + , legend = nv.models.legend() + ; + + + 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 + , tooltips = true + , tooltip = function(key, x, y, e, graph) { + return '

' + key + '

' + + '

' + y + ' at ' + x + '

' + } + , x + , y + , state = {} + , defaultState = null + , noData = 'No Data Available.' + , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'stateChange', 'changeState') + , transitionDuration = 250 + ; + + xAxis + .orient('bottom') + .tickPadding(7) + ; + yAxis + .orient( (rightAlignYAxis) ? 'right' : 'left') + ; + + //============================================================ + + + //============================================================ + // Private Variables + //------------------------------------------------------------ + + var showTooltip = function(e, offsetElement) { + + // New addition to calculate position if SVG is scaled with viewBox, may move TODO: consider implementing everywhere else + if (offsetElement) { + var svg = d3.select(offsetElement).select('svg'); + var viewBox = (svg.node()) ? svg.attr('viewBox') : null; + if (viewBox) { + viewBox = viewBox.split(' '); + var ratio = parseInt(svg.style('width')) / viewBox[2]; + e.pos[0] = e.pos[0] * ratio; + e.pos[1] = e.pos[1] * ratio; + } + } + + var left = e.pos[0] + ( offsetElement.offsetLeft || 0 ), + top = e.pos[1] + ( offsetElement.offsetTop || 0), + x = xAxis.tickFormat()(bars.x()(e.point, e.pointIndex)), + y = yAxis.tickFormat()(bars.y()(e.point, e.pointIndex)), + content = tooltip(e.series.key, x, y, e, chart); + + nv.tooltip.show([left, top], content, null, null, offsetElement); + }; + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + var container = d3.select(this), + that = this; + + 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() { 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) { + 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 = 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'); + + //------------------------------------------------------------ + + + //------------------------------------------------------------ + // Legend + + if (showLegend) { + legend.width(availableWidth); + + g.select('.nv-legendWrap') + .datum(data) + .call(legend); + + if ( margin.top != legend.height()) { + margin.top = legend.height(); + availableHeight = (height || parseInt(container.style('height')) || 400) + - margin.top - margin.bottom; + } + + 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)"); + } + + + //------------------------------------------------------------ + // Main Chart Component(s) + + 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) + .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( availableHeight / 36 ) + .tickSize( -availableWidth, 0); + + g.select('.nv-y.nv-axis') + .transition() + .call(yAxis); + } + //------------------------------------------------------------ + + + //============================================================ + // Event Handling/Dispatching (in chart's scope) + //------------------------------------------------------------ + + 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('tooltipShow', function(e) { + if (tooltips) showTooltip(e, that.parentNode); + }); + + + dispatch.on('changeState', function(e) { + + if (typeof e.disabled !== 'undefined') { + data.forEach(function(series,i) { + series.disabled = e.disabled[i]; + }); + + state.disabled = e.disabled; + } + + selection.call(chart); + }); + + //============================================================ + + }); + + return chart; + } + + + //============================================================ + // Event Handling/Dispatching (out of chart's scope) + //------------------------------------------------------------ + + bars.dispatch.on('elementMouseover.tooltip', function(e) { + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top]; + dispatch.tooltipShow(e); + }); + + bars.dispatch.on('elementMouseout.tooltip', function(e) { + dispatch.tooltipHide(e); + }); + + dispatch.on('tooltipHide', function() { + if (tooltips) nv.tooltip.cleanup(); + }); + + //============================================================ + + + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + + // expose chart's sub-components + chart.dispatch = dispatch; + chart.bars = bars; + chart.legend = legend; + chart.xAxis = xAxis; + chart.yAxis = yAxis; + + d3.rebind(chart, bars, 'defined', 'isArea', 'x', 'y', 'size', 'xScale', 'yScale', + 'xDomain', 'yDomain', 'xRange', 'yRange', 'forceX', 'forceY', 'interactive', 'clipEdge', 'clipVoronoi', 'id', 'interpolate','highlightPoint','clearHighlights', 'interactive'); + + chart.options = nv.utils.optionsFunc.bind(chart); + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + legend.color(color); + return chart; + }; + + chart.showLegend = function(_) { + if (!arguments.length) return showLegend; + showLegend = _; + return chart; + }; + + chart.showXAxis = function(_) { + if (!arguments.length) return showXAxis; + showXAxis = _; + return chart; + }; + + chart.showYAxis = function(_) { + if (!arguments.length) return showYAxis; + showYAxis = _; + return chart; + }; + + chart.rightAlignYAxis = function(_) { + if(!arguments.length) return rightAlignYAxis; + rightAlignYAxis = _; + yAxis.orient( (_) ? 'right' : 'left'); + return chart; + }; + + chart.tooltips = function(_) { + if (!arguments.length) return tooltips; + tooltips = _; + return chart; + }; + + chart.tooltipContent = function(_) { + if (!arguments.length) return tooltip; + tooltip = _; + return chart; + }; + + chart.state = function(_) { + if (!arguments.length) return state; + state = _; + return chart; + }; + + chart.defaultState = function(_) { + if (!arguments.length) return defaultState; + defaultState = _; + return chart; + }; + + chart.noData = function(_) { + if (!arguments.length) return noData; + noData = _; + return chart; + }; + + chart.transitionDuration = function(_) { + if (!arguments.length) return transitionDuration; + transitionDuration = _; + return chart; + }; + + //============================================================ + + + return chart; +} +nv.models.indentedTree = function() { + "use strict"; + //============================================================ + // Public Variables with Default Settings + //------------------------------------------------------------ + + var margin = {top: 0, right: 0, bottom: 0, left: 0} //TODO: implement, maybe as margin on the containing div + , width = 960 + , height = 500 + , color = nv.utils.defaultColor() + , id = Math.floor(Math.random() * 10000) + , header = true + , filterZero = false + , noData = "No Data Available." + , childIndent = 20 + , columns = [{key:'key', label: 'Name', type:'text'}] //TODO: consider functions like chart.addColumn, chart.removeColumn, instead of a block like this + , tableClass = null + , iconOpen = 'images/grey-plus.png' //TODO: consider removing this and replacing with a '+' or '-' unless user defines images + , iconClose = 'images/grey-minus.png' + , dispatch = d3.dispatch('elementClick', 'elementDblclick', 'elementMouseover', 'elementMouseout') + , getUrl = function(d) { return d.url } + ; + + //============================================================ + + var idx = 0; + + function chart(selection) { + selection.each(function(data) { + var depth = 1, + container = d3.select(this); + + var tree = d3.layout.tree() + .children(function(d) { return d.values }) + .size([height, childIndent]); //Not sure if this is needed now that the result is HTML + + chart.update = function() { container.transition().duration(600).call(chart) }; + + + //------------------------------------------------------------ + // Display No Data message if there's nothing to show. + if (!data[0]) data[0] = {key: noData}; + + //------------------------------------------------------------ + + + var nodes = tree.nodes(data[0]); + + // nodes.map(function(d) { + // d.id = i++; + // }) + + //------------------------------------------------------------ + // Setup containers and skeleton of chart + + var wrap = d3.select(this).selectAll('div').data([[nodes]]); + var wrapEnter = wrap.enter().append('div').attr('class', 'nvd3 nv-wrap nv-indentedtree'); + var tableEnter = wrapEnter.append('table'); + var table = wrap.select('table').attr('width', '100%').attr('class', tableClass); + + //------------------------------------------------------------ + + + if (header) { + var thead = tableEnter.append('thead'); + + var theadRow1 = thead.append('tr'); + + columns.forEach(function(column) { + theadRow1 + .append('th') + .attr('width', column.width ? column.width : '10%') + .style('text-align', column.type == 'numeric' ? 'right' : 'left') + .append('span') + .text(column.label); + }); + } + + + var tbody = table.selectAll('tbody') + .data(function(d) { return d }); + tbody.enter().append('tbody'); + + + + //compute max generations + depth = d3.max(nodes, function(node) { return node.depth }); + tree.size([height, depth * childIndent]); //TODO: see if this is necessary at all + + + // Update the nodes… + var node = tbody.selectAll('tr') + // .data(function(d) { return d; }, function(d) { return d.id || (d.id == ++i)}); + .data(function(d) { return d.filter(function(d) { return (filterZero && !d.children) ? filterZero(d) : true; } )}, function(d,i) { return d.id || (d.id || ++idx)}); + //.style('display', 'table-row'); //TODO: see if this does anything + + node.exit().remove(); + + node.select('img.nv-treeicon') + .attr('src', icon) + .classed('folded', folded); + + var nodeEnter = node.enter().append('tr'); + + + columns.forEach(function(column, index) { + + var nodeName = nodeEnter.append('td') + .style('padding-left', function(d) { return (index ? 0 : d.depth * childIndent + 12 + (icon(d) ? 0 : 16)) + 'px' }, 'important') //TODO: check why I did the ternary here + .style('text-align', column.type == 'numeric' ? 'right' : 'left'); + + + if (index == 0) { + nodeName.append('img') + .classed('nv-treeicon', true) + .classed('nv-folded', folded) + .attr('src', icon) + .style('width', '14px') + .style('height', '14px') + .style('padding', '0 1px') + .style('display', function(d) { return icon(d) ? 'inline-block' : 'none'; }) + .on('click', click); + } + + + nodeName.each(function(d) { + if (!index && getUrl(d)) + d3.select(this) + .append('a') + .attr('href',getUrl) + .attr('class', d3.functor(column.classes)) + .append('span') + else + d3.select(this) + .append('span') + + d3.select(this).select('span') + .attr('class', d3.functor(column.classes) ) + .text(function(d) { return column.format ? column.format(d) : + (d[column.key] || '-') }); + }); + + if (column.showCount) { + nodeName.append('span') + .attr('class', 'nv-childrenCount'); + + node.selectAll('span.nv-childrenCount').text(function(d) { + return ((d.values && d.values.length) || (d._values && d._values.length)) ? //If this is a parent + '(' + ((d.values && (d.values.filter(function(d) { return filterZero ? filterZero(d) : true; }).length)) //If children are in values check its children and filter + || (d._values && d._values.filter(function(d) { return filterZero ? filterZero(d) : true; }).length) //Otherwise, do the same, but with the other name, _values... + || 0) + ')' //This is the catch-all in case there are no children after a filter + : '' //If this is not a parent, just give an empty string + }); + } + + // if (column.click) + // nodeName.select('span').on('click', column.click); + + }); + + node + .order() + .on('click', function(d) { + dispatch.elementClick({ + row: this, //TODO: decide whether or not this should be consistent with scatter/line events or should be an html link (a href) + data: d, + pos: [d.x, d.y] + }); + }) + .on('dblclick', function(d) { + dispatch.elementDblclick({ + row: this, + data: d, + pos: [d.x, d.y] + }); + }) + .on('mouseover', function(d) { + dispatch.elementMouseover({ + row: this, + data: d, + pos: [d.x, d.y] + }); + }) + .on('mouseout', function(d) { + dispatch.elementMouseout({ + row: this, + data: d, + pos: [d.x, d.y] + }); + }); + + + + + // Toggle children on click. + function click(d, _, unshift) { + d3.event.stopPropagation(); + + if(d3.event.shiftKey && !unshift) { + //If you shift-click, it'll toggle fold all the children, instead of itself + d3.event.shiftKey = false; + d.values && d.values.forEach(function(node){ + if (node.values || node._values) { + click(node, 0, true); + } + }); + return true; + } + if(!hasChildren(d)) { + //download file + //window.location.href = d.url; + return true; + } + if (d.values) { + d._values = d.values; + d.values = null; + } else { + d.values = d._values; + d._values = null; + } + chart.update(); + } + + + function icon(d) { + return (d._values && d._values.length) ? iconOpen : (d.values && d.values.length) ? iconClose : ''; + } + + function folded(d) { + return (d._values && d._values.length); + } + + function hasChildren(d) { + var values = d.values || d._values; + + return (values && values.length); + } + + + }); + + return chart; + } + + + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + chart.options = nv.utils.optionsFunc.bind(chart); + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + scatter.color(color); + return chart; + }; + + chart.id = function(_) { + if (!arguments.length) return id; + id = _; + return chart; + }; + + chart.header = function(_) { + if (!arguments.length) return header; + header = _; + return chart; + }; + + chart.noData = function(_) { + if (!arguments.length) return noData; + noData = _; + return chart; + }; + + chart.filterZero = function(_) { + if (!arguments.length) return filterZero; + filterZero = _; + return chart; + }; + + chart.columns = function(_) { + if (!arguments.length) return columns; + columns = _; + return chart; + }; + + chart.tableClass = function(_) { + if (!arguments.length) return tableClass; + tableClass = _; + return chart; + }; + + chart.iconOpen = function(_){ + if (!arguments.length) return iconOpen; + iconOpen = _; + return chart; + } + + chart.iconClose = function(_){ + if (!arguments.length) return iconClose; + iconClose = _; + return chart; + } + + chart.getUrl = function(_){ + if (!arguments.length) return getUrl; + getUrl = _; + return chart; + } + + //============================================================ + + + 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.defaultColor() + , align = true + , 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) + , dispatch = d3.dispatch('legendClick', 'legendDblclick', 'legendMouseover', 'legendMouseout', 'stateChange') + ; + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + var availableWidth = width - margin.left - margin.right, + container = d3.select(this); + + + //------------------------------------------------------------ + // 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) { return d }); + var seriesEnter = series.enter().append('g').attr('class', 'nv-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); + if (updateState) { + 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}); + } + } + dispatch.stateChange({ + disabled: data.map(function(d) { return !!d.disabled }) + }); + } + }) + .on('dblclick', function(d,i) { + dispatch.legendDblclick(d,i); + if (updateState) { + //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; + }); + d.disabled = false; + dispatch.stateChange({ + disabled: data.map(function(d) { return !!d.disabled }) + }); + } + }); + seriesEnter.append('circle') + .style('stroke-width', 2) + .attr('class','nv-legend-symbol') + .attr('r', 5); + seriesEnter.append('text') + .attr('text-anchor', 'start') + .attr('class','nv-legend-text') + .attr('dy', '.32em') + .attr('dx', '8'); + series.classed('disabled', function(d) { return d.disabled }); + series.exit().remove(); + series.select('circle') + .style('fill', function(d,i) { return d.color || color(d,i)}) + .style('stroke', function(d,i) { return d.color || color(d, i) }); + series.select('text').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 + if (align) { + + var seriesWidths = []; + series.each(function(d,i) { + var legendText = d3.select(this).select('text'); + var nodeTextLength; + try { + nodeTextLength = legendText.node().getComputedTextLength(); + } + catch(e) { + nodeTextLength = nv.utils.calcApproxTextWidth(legendText); + } + + seriesWidths.push(nodeTextLength + 28); // 28 is ~ the width of the circle plus some 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) * 20) + ')'; + }); + + //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) * 20); + + } else { + + var ypos = 5, + newxpos = 5, + maxwidth = 0, + xpos; + series + .attr('transform', function(d, i) { + var length = d3.select(this).select('text').node().getComputedTextLength() + 28; + xpos = newxpos; + + if (width < margin.left + margin.right + xpos + length) { + newxpos = xpos = 5; + ypos += 20; + } + + 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; + + } + + }); + + return chart; + } + + + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + + chart.dispatch = dispatch; + chart.options = nv.utils.optionsFunc.bind(chart); + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.key = function(_) { + if (!arguments.length) return getKey; + getKey = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + return chart; + }; + + chart.align = function(_) { + if (!arguments.length) return align; + align = _; + return chart; + }; + + chart.rightAlign = function(_) { + if (!arguments.length) return rightAlign; + rightAlign = _; + return chart; + }; + + chart.updateState = function(_) { + if (!arguments.length) return updateState; + updateState = _; + return chart; + }; + + chart.radioButtonMode = function(_) { + if (!arguments.length) return radioButtonMode; + radioButtonMode = _; + return 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 + , 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 + ; + + scatter + .size(16) // default size + .sizeDomain([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 + ; + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + var availableWidth = width - margin.left - margin.right, + availableHeight = height - margin.top - margin.bottom, + container = d3.select(this); + + //------------------------------------------------------------ + // 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'); + //.datum(data); // Data automatically trickles down from the wrap + + scatterWrap.transition().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); + + 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('fill-opacity', 1e-6); + groups.exit() + .transition() + .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 + .transition() + .style('stroke-opacity', 1) + .style('fill-opacity', .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 + .transition() + .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))) }) + ); + groups.exit().selectAll('path.nv-line') + .transition() + .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))) }) + ); + linePaths + .transition() + .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(); + + }); + + return chart; + } + + + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + + chart.dispatch = scatter.dispatch; + chart.scatter = scatter; + + d3.rebind(chart, scatter, 'id', 'interactive', 'size', 'xScale', 'yScale', 'zScale', 'xDomain', 'yDomain', 'xRange', 'yRange', + 'sizeDomain', 'forceX', 'forceY', 'forceSize', 'clipVoronoi', 'useVoronoi', 'clipRadius', 'padData','highlightPoint','clearHighlights'); + + chart.options = nv.utils.optionsFunc.bind(chart); + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.x = function(_) { + if (!arguments.length) return getX; + getX = _; + scatter.x(_); + return chart; + }; + + chart.y = function(_) { + if (!arguments.length) return getY; + getY = _; + scatter.y(_); + return chart; + }; + + chart.clipEdge = function(_) { + if (!arguments.length) return clipEdge; + clipEdge = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + scatter.color(color); + return chart; + }; + + chart.interpolate = function(_) { + if (!arguments.length) return interpolate; + interpolate = _; + return chart; + }; + + chart.defined = function(_) { + if (!arguments.length) return defined; + defined = _; + return chart; + }; + + chart.isArea = function(_) { + if (!arguments.length) return isArea; + isArea = d3.functor(_); + return 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() + ; + + 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 + , tooltips = true + , tooltip = function(key, x, y, e, graph) { + return '

' + key + '

' + + '

' + y + ' at ' + x + '

' + } + , x + , y + , state = {} + , defaultState = null + , noData = 'No Data Available.' + , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'stateChange', 'changeState') + , transitionDuration = 250 + ; + + xAxis + .orient('bottom') + .tickPadding(7) + ; + yAxis + .orient((rightAlignYAxis) ? 'right' : 'left') + ; + + //============================================================ + + + //============================================================ + // Private Variables + //------------------------------------------------------------ + + var showTooltip = function(e, offsetElement) { + var left = e.pos[0] + ( offsetElement.offsetLeft || 0 ), + top = e.pos[1] + ( offsetElement.offsetTop || 0), + x = xAxis.tickFormat()(lines.x()(e.point, e.pointIndex)), + y = yAxis.tickFormat()(lines.y()(e.point, e.pointIndex)), + content = tooltip(e.series.key, x, y, e, chart); + + nv.tooltip.show([left, top], content, null, null, offsetElement); + }; + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + var container = d3.select(this), + that = this; + + 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() { 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) { + 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 = 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); + //------------------------------------------------------------ + // Legend + + if (showLegend) { + legend.width(availableWidth); + + g.select('.nv-legendWrap') + .datum(data) + .call(legend); + + if ( margin.top != legend.height()) { + margin.top = legend.height(); + availableHeight = (height || parseInt(container.style('height')) || 400) + - margin.top - margin.bottom; + } + + 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)"); + } + + //------------------------------------------------------------ + // Main Chart Component(s) + + + //------------------------------------------------------------ + //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.transition().call(lines); + + //------------------------------------------------------------ + + + //------------------------------------------------------------ + // Setup Axes + + if (showXAxis) { + xAxis + .scale(x) + .ticks( availableWidth / 100 ) + .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( availableHeight / 36 ) + .tickSize( -availableWidth, 0); + + g.select('.nv-y.nv-axis') + .transition() + .call(yAxis); + } + //------------------------------------------------------------ + + + //============================================================ + // Event Handling/Dispatching (in chart's scope) + //------------------------------------------------------------ + + legend.dispatch.on('stateChange', function(newState) { + state = newState; + 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) + }); + }); + + var xValue = xAxis.tickFormat()(chart.x()(singlePoint,pointIndex)); + interactiveLayer.tooltip + .position({left: pointXLocation + margin.left, top: e.mouseY + margin.top}) + .chartContainer(that.parentNode) + .enabled(tooltips) + .valueFormatter(function(d,i) { + return yAxis.tickFormat()(d); + }) + .data( + { + value: xValue, + series: allData + } + )(); + + interactiveLayer.renderGuideLine(pointXLocation); + + }); + + interactiveLayer.dispatch.on("elementMouseout",function(e) { + dispatch.tooltipHide(); + lines.clearHighlights(); + }); + + dispatch.on('tooltipShow', function(e) { + if (tooltips) showTooltip(e, that.parentNode); + }); + + + 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(); + }); + + //============================================================ + + }); + + return chart; + } + + + //============================================================ + // Event Handling/Dispatching (out of chart's scope) + //------------------------------------------------------------ + + lines.dispatch.on('elementMouseover.tooltip', function(e) { + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top]; + dispatch.tooltipShow(e); + }); + + lines.dispatch.on('elementMouseout.tooltip', function(e) { + dispatch.tooltipHide(e); + }); + + dispatch.on('tooltipHide', function() { + if (tooltips) nv.tooltip.cleanup(); + }); + + //============================================================ + + + //============================================================ + // 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; + + d3.rebind(chart, lines, 'defined', 'isArea', 'x', 'y', 'size', 'xScale', 'yScale', 'xDomain', 'yDomain', 'xRange', 'yRange' + , 'forceX', 'forceY', 'interactive', 'clipEdge', 'clipVoronoi', 'useVoronoi','id', 'interpolate'); + + chart.options = nv.utils.optionsFunc.bind(chart); + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + legend.color(color); + return chart; + }; + + chart.showLegend = function(_) { + if (!arguments.length) return showLegend; + showLegend = _; + return chart; + }; + + chart.showXAxis = function(_) { + if (!arguments.length) return showXAxis; + showXAxis = _; + return chart; + }; + + chart.showYAxis = function(_) { + if (!arguments.length) return showYAxis; + showYAxis = _; + return chart; + }; + + chart.rightAlignYAxis = function(_) { + if(!arguments.length) return rightAlignYAxis; + rightAlignYAxis = _; + yAxis.orient( (_) ? 'right' : 'left'); + return chart; + }; + + chart.useInteractiveGuideline = function(_) { + if(!arguments.length) return useInteractiveGuideline; + useInteractiveGuideline = _; + if (_ === true) { + chart.interactive(false); + chart.useVoronoi(false); + } + return chart; + }; + + chart.tooltips = function(_) { + if (!arguments.length) return tooltips; + tooltips = _; + return chart; + }; + + chart.tooltipContent = function(_) { + if (!arguments.length) return tooltip; + tooltip = _; + return chart; + }; + + chart.state = function(_) { + if (!arguments.length) return state; + state = _; + return chart; + }; + + chart.defaultState = function(_) { + if (!arguments.length) return defaultState; + defaultState = _; + return chart; + }; + + chart.noData = function(_) { + if (!arguments.length) return noData; + noData = _; + return chart; + }; + + chart.transitionDuration = function(_) { + if (!arguments.length) return transitionDuration; + transitionDuration = _; + return chart; + }; + + //============================================================ + + + return chart; +} + +nv.models.linePlusBarChart = function() { + "use strict"; + //============================================================ + // Public Variables with Default Settings + //------------------------------------------------------------ + + var lines = nv.models.line() + , bars = nv.models.historicalBar() + , xAxis = nv.models.axis() + , y1Axis = nv.models.axis() + , y2Axis = nv.models.axis() + , legend = nv.models.legend() + ; + + var margin = {top: 30, right: 60, bottom: 50, left: 60} + , width = null + , height = null + , getX = function(d) { return d.x } + , getY = function(d) { return d.y } + , color = nv.utils.defaultColor() + , showLegend = true + , tooltips = true + , tooltip = function(key, x, y, e, graph) { + return '

' + key + '

' + + '

' + y + ' at ' + x + '

'; + } + , x + , y1 + , y2 + , state = {} + , defaultState = null + , noData = "No Data Available." + , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'stateChange', 'changeState') + ; + + bars + .padData(true) + ; + lines + .clipEdge(false) + .padData(true) + ; + xAxis + .orient('bottom') + .tickPadding(7) + .highlightZero(false) + ; + y1Axis + .orient('left') + ; + y2Axis + .orient('right') + ; + + //============================================================ + + + //============================================================ + // Private Variables + //------------------------------------------------------------ + + var showTooltip = function(e, offsetElement) { + var left = e.pos[0] + ( offsetElement.offsetLeft || 0 ), + top = e.pos[1] + ( offsetElement.offsetTop || 0), + x = xAxis.tickFormat()(lines.x()(e.point, e.pointIndex)), + y = (e.series.bar ? y1Axis : y2Axis).tickFormat()(lines.y()(e.point, e.pointIndex)), + content = tooltip(e.series.key, x, y, e, chart); + + nv.tooltip.show([left, top], content, e.value < 0 ? 'n' : 's', null, offsetElement); + } + ; + + //------------------------------------------------------------ + + + + function chart(selection) { + selection.each(function(data) { + var container = d3.select(this), + that = this; + + 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() { container.transition().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 No Data message if there's nothing to show. + + if (!data || !data.length || !data.filter(function(d) { return d.values.length }).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 + + 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 = xAxis.scale(); + x = dataLines.filter(function(d) { return !d.disabled; }).length && dataLines.filter(function(d) { return !d.disabled; })[0].values.length ? lines.xScale() : bars.xScale(); + //x = dataLines.filter(function(d) { return !d.disabled; }).length ? lines.xScale() : bars.xScale(); //old code before change above + y1 = bars.yScale(); + y2 = lines.yScale(); + + //------------------------------------------------------------ + + //------------------------------------------------------------ + // Setup containers and skeleton of chart + + var wrap = d3.select(this).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-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', 'nv-barsWrap'); + gEnter.append('g').attr('class', 'nv-linesWrap'); + gEnter.append('g').attr('class', 'nv-legendWrap'); + + //------------------------------------------------------------ + + + //------------------------------------------------------------ + // Legend + + if (showLegend) { + legend.width( availableWidth / 2 ); + + g.select('.nv-legendWrap') + .datum(data.map(function(series) { + series.originalKey = series.originalKey === undefined ? series.key : series.originalKey; + series.key = series.originalKey + (series.bar ? ' (left axis)' : ' (right axis)'); + return series; + })) + .call(legend); + + if ( margin.top != legend.height()) { + margin.top = legend.height(); + availableHeight = (height || parseInt(container.style('height')) || 400) + - margin.top - margin.bottom; + } + + g.select('.nv-legendWrap') + .attr('transform', 'translate(' + ( availableWidth / 2 ) + ',' + (-margin.top) +')'); + } + + //------------------------------------------------------------ + + + wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + + + //------------------------------------------------------------ + // Main Chart Component(s) + + + 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 && !data[i].bar })) + + 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 && data[i].bar })) + + + + var barsWrap = g.select('.nv-barsWrap') + .datum(dataBars.length ? dataBars : [{values:[]}]) + + var linesWrap = g.select('.nv-linesWrap') + .datum(dataLines[0] && !dataLines[0].disabled ? dataLines : [{values:[]}] ); + //.datum(!dataLines[0].disabled ? dataLines : [{values:dataLines[0].values.map(function(d) { return [d[0], null] }) }] ); + + d3.transition(barsWrap).call(bars); + d3.transition(linesWrap).call(lines); + + //------------------------------------------------------------ + + + //------------------------------------------------------------ + // Setup Axes + + xAxis + .scale(x) + .ticks( availableWidth / 100 ) + .tickSize(-availableHeight, 0); + + g.select('.nv-x.nv-axis') + .attr('transform', 'translate(0,' + y1.range()[0] + ')'); + d3.transition(g.select('.nv-x.nv-axis')) + .call(xAxis); + + + y1Axis + .scale(y1) + .ticks( availableHeight / 36 ) + .tickSize(-availableWidth, 0); + + d3.transition(g.select('.nv-y1.nv-axis')) + .style('opacity', dataBars.length ? 1 : 0) + .call(y1Axis); + + + y2Axis + .scale(y2) + .ticks( availableHeight / 36 ) + .tickSize(dataBars.length ? 0 : -availableWidth, 0); // Show the y2 rules only if y1 has none + + g.select('.nv-y2.nv-axis') + .style('opacity', dataLines.length ? 1 : 0) + .attr('transform', 'translate(' + availableWidth + ',0)'); + //.attr('transform', 'translate(' + x.range()[1] + ',0)'); + + d3.transition(g.select('.nv-y2.nv-axis')) + .call(y2Axis); + + //------------------------------------------------------------ + + + //============================================================ + // Event Handling/Dispatching (in chart's scope) + //------------------------------------------------------------ + + legend.dispatch.on('stateChange', function(newState) { + state = newState; + dispatch.stateChange(state); + chart.update(); + }); + + dispatch.on('tooltipShow', function(e) { + if (tooltips) showTooltip(e, that.parentNode); + }); + + + // 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(); + }); + + //============================================================ + + + }); + + return chart; + } + + + //============================================================ + // Event Handling/Dispatching (out of chart's scope) + //------------------------------------------------------------ + + lines.dispatch.on('elementMouseover.tooltip', function(e) { + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top]; + dispatch.tooltipShow(e); + }); + + lines.dispatch.on('elementMouseout.tooltip', function(e) { + dispatch.tooltipHide(e); + }); + + bars.dispatch.on('elementMouseover.tooltip', function(e) { + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top]; + dispatch.tooltipShow(e); + }); + + bars.dispatch.on('elementMouseout.tooltip', function(e) { + dispatch.tooltipHide(e); + }); + + dispatch.on('tooltipHide', function() { + if (tooltips) nv.tooltip.cleanup(); + }); + + //============================================================ + + + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + + // expose chart's sub-components + chart.dispatch = dispatch; + chart.legend = legend; + chart.lines = lines; + chart.bars = bars; + chart.xAxis = xAxis; + chart.y1Axis = y1Axis; + chart.y2Axis = y2Axis; + + d3.rebind(chart, lines, 'defined', 'size', 'clipVoronoi', 'interpolate'); + //TODO: consider rebinding x, y and some other stuff, and simply do soemthign lile bars.x(lines.x()), etc. + //d3.rebind(chart, lines, 'x', 'y', 'size', 'xDomain', 'yDomain', 'xRange', 'yRange', 'forceX', 'forceY', 'interactive', 'clipEdge', 'clipVoronoi', 'id'); + + chart.options = nv.utils.optionsFunc.bind(chart); + + chart.x = function(_) { + if (!arguments.length) return getX; + getX = _; + lines.x(_); + bars.x(_); + return chart; + }; + + chart.y = function(_) { + if (!arguments.length) return getY; + getY = _; + lines.y(_); + bars.y(_); + return chart; + }; + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + legend.color(color); + return chart; + }; + + chart.showLegend = function(_) { + if (!arguments.length) return showLegend; + showLegend = _; + return chart; + }; + + chart.tooltips = function(_) { + if (!arguments.length) return tooltips; + tooltips = _; + return chart; + }; + + chart.tooltipContent = function(_) { + if (!arguments.length) return tooltip; + tooltip = _; + return chart; + }; + + chart.state = function(_) { + if (!arguments.length) return state; + state = _; + return chart; + }; + + chart.defaultState = function(_) { + if (!arguments.length) return defaultState; + defaultState = _; + return chart; + }; + + chart.noData = function(_) { + if (!arguments.length) return noData; + noData = _; + return 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() + ; + + 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 = 100 + , x + , y + , x2 + , y2 + , showLegend = true + , brushExtent = null + , tooltips = true + , tooltip = function(key, x, y, e, graph) { + return '

' + key + '

' + + '

' + y + ' at ' + x + '

' + } + , noData = "No Data Available." + , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'brush') + , transitionDuration = 250 + ; + + lines + .clipEdge(true) + ; + lines2 + .interactive(false) + ; + xAxis + .orient('bottom') + .tickPadding(5) + ; + yAxis + .orient('left') + ; + x2Axis + .orient('bottom') + .tickPadding(5) + ; + y2Axis + .orient('left') + ; + //============================================================ + + + //============================================================ + // Private Variables + //------------------------------------------------------------ + + var showTooltip = function(e, offsetElement) { + var left = e.pos[0] + ( offsetElement.offsetLeft || 0 ), + top = e.pos[1] + ( offsetElement.offsetTop || 0), + x = xAxis.tickFormat()(lines.x()(e.point, e.pointIndex)), + y = yAxis.tickFormat()(lines.y()(e.point, e.pointIndex)), + content = tooltip(e.series.key, x, y, e, chart); + + nv.tooltip.show([left, top], content, null, null, offsetElement); + }; + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + var container = d3.select(this), + that = this; + + var availableWidth = (width || parseInt(container.style('width')) || 960) + - margin.left - margin.right, + availableHeight1 = (height || parseInt(container.style('height')) || 400) + - margin.top - margin.bottom - height2, + availableHeight2 = height2 - margin2.top - margin2.bottom; + + chart.update = function() { container.transition().duration(transitionDuration).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) { + 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 + availableHeight1 / 2) + .text(function(d) { return d }); + + 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'); + + 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 = (height || parseInt(container.style('height')) || 400) + - margin.top - margin.bottom - height2; + } + + g.select('.nv-legendWrap') + .attr('transform', 'translate(0,' + (-margin.top) +')') + } + + //------------------------------------------------------------ + + + wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + + + //------------------------------------------------------------ + // 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); + + //------------------------------------------------------------ + + + /* + var focusLinesWrap = g.select('.nv-focus .nv-linesWrap') + .datum(data.filter(function(d) { return !d.disabled })) + + d3.transition(focusLinesWrap).call(lines); + */ + + + //------------------------------------------------------------ + // Setup Main (Focus) Axes + + xAxis + .scale(x) + .ticks( availableWidth / 100 ) + .tickSize(-availableHeight1, 0); + + yAxis + .scale(y) + .ticks( availableHeight1 / 36 ) + .tickSize( -availableWidth, 0); + + g.select('.nv-focus .nv-x.nv-axis') + .attr('transform', 'translate(0,' + availableHeight1 + ')'); + + //------------------------------------------------------------ + + + //------------------------------------------------------------ + // Setup Brush + + brush + .x(x2) + .on('brush', function() { + //When brushing, turn off transitions because chart needs to change immediately. + var oldTransition = chart.transitionDuration(); + chart.transitionDuration(0); + onBrush(); + chart.transitionDuration(oldTransition); + }); + + 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); + + onBrush(); + + //------------------------------------------------------------ + + + //------------------------------------------------------------ + // Setup Secondary (Context) Axes + + x2Axis + .scale(x2) + .ticks( availableWidth / 100 ) + .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( availableHeight2 / 36 ) + .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) { + chart.update(); + }); + + dispatch.on('tooltipShow', function(e) { + if (tooltips) showTooltip(e, that.parentNode); + }); + + //============================================================ + + + //============================================================ + // 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 = x.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(); + 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, + 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(e) { + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top]; + dispatch.tooltipShow(e); + }); + + lines.dispatch.on('elementMouseout.tooltip', function(e) { + dispatch.tooltipHide(e); + }); + + dispatch.on('tooltipHide', function() { + if (tooltips) nv.tooltip.cleanup(); + }); + + //============================================================ + + + //============================================================ + // 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; + + d3.rebind(chart, lines, 'defined', 'isArea', 'size', 'xDomain', 'yDomain', 'xRange', 'yRange', 'forceX', 'forceY', 'interactive', 'clipEdge', 'clipVoronoi', 'id'); + + chart.options = nv.utils.optionsFunc.bind(chart); + + chart.x = function(_) { + if (!arguments.length) return lines.x; + lines.x(_); + lines2.x(_); + return chart; + }; + + chart.y = function(_) { + if (!arguments.length) return lines.y; + lines.y(_); + lines2.y(_); + return chart; + }; + + 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.margin2 = function(_) { + if (!arguments.length) return margin2; + margin2 = _; + return chart; + }; + + chart.width = function(_) { + if (!arguments.length) return width; + width = _; + return chart; + }; + + chart.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.height2 = function(_) { + if (!arguments.length) return height2; + height2 = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color =nv.utils.getColor(_); + legend.color(color); + return chart; + }; + + chart.showLegend = function(_) { + if (!arguments.length) return showLegend; + showLegend = _; + return chart; + }; + + chart.tooltips = function(_) { + if (!arguments.length) return tooltips; + tooltips = _; + return chart; + }; + + chart.tooltipContent = function(_) { + if (!arguments.length) return tooltip; + tooltip = _; + return chart; + }; + + chart.interpolate = function(_) { + if (!arguments.length) return lines.interpolate(); + lines.interpolate(_); + lines2.interpolate(_); + return chart; + }; + + chart.noData = function(_) { + if (!arguments.length) return noData; + noData = _; + return chart; + }; + + // Chart has multiple similar Axes, to prevent code duplication, probably need to link all axis functions manually like below + chart.xTickFormat = function(_) { + if (!arguments.length) return xAxis.tickFormat(); + xAxis.tickFormat(_); + x2Axis.tickFormat(_); + return chart; + }; + + chart.yTickFormat = function(_) { + if (!arguments.length) return yAxis.tickFormat(); + yAxis.tickFormat(_); + y2Axis.tickFormat(_); + return chart; + }; + + chart.brushExtent = function(_) { + if (!arguments.length) return brushExtent; + brushExtent = _; + return chart; + }; + + chart.transitionDuration = function(_) { + if (!arguments.length) return transitionDuration; + transitionDuration = _; + return chart; + }; + + //============================================================ + + + return chart; +} + +nv.models.linePlusBarWithFocusChart = 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() + ; + + var margin = {top: 30, right: 30, bottom: 30, left: 60} + , margin2 = {top: 0, right: 30, bottom: 20, left: 60} + , width = null + , height = null + , height2 = 100 + , getX = function(d) { return d.x } + , getY = function(d) { return d.y } + , color = nv.utils.defaultColor() + , showLegend = true + , extent + , brushExtent = null + , tooltips = true + , tooltip = function(key, x, y, e, graph) { + return '

' + key + '

' + + '

' + y + ' at ' + x + '

'; + } + , x + , x2 + , y1 + , y2 + , y3 + , y4 + , noData = "No Data Available." + , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'brush') + , transitionDuration = 0 + ; + + lines + .clipEdge(true) + ; + lines2 + .interactive(false) + ; + xAxis + .orient('bottom') + .tickPadding(5) + ; + y1Axis + .orient('left') + ; + y2Axis + .orient('right') + ; + x2Axis + .orient('bottom') + .tickPadding(5) + ; + y3Axis + .orient('left') + ; + y4Axis + .orient('right') + ; + + //============================================================ + + + //============================================================ + // Private Variables + //------------------------------------------------------------ + + var showTooltip = function(e, offsetElement) { + if (extent) { + e.pointIndex += Math.ceil(extent[0]); + } + var left = e.pos[0] + ( offsetElement.offsetLeft || 0 ), + top = e.pos[1] + ( offsetElement.offsetTop || 0), + x = xAxis.tickFormat()(lines.x()(e.point, e.pointIndex)), + y = (e.series.bar ? y1Axis : y2Axis).tickFormat()(lines.y()(e.point, e.pointIndex)), + content = tooltip(e.series.key, x, y, e, chart); + + nv.tooltip.show([left, top], content, e.value < 0 ? 'n' : 's', null, offsetElement); + }; + + //------------------------------------------------------------ + + + + function chart(selection) { + selection.each(function(data) { + var container = d3.select(this), + that = this; + + var availableWidth = (width || parseInt(container.style('width')) || 960) + - margin.left - margin.right, + availableHeight1 = (height || parseInt(container.style('height')) || 400) + - margin.top - margin.bottom - height2, + availableHeight2 = height2 - margin2.top - margin2.bottom; + + chart.update = function() { container.transition().duration(transitionDuration).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) { + 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 + availableHeight1 / 2) + .text(function(d) { return d }); + + 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'); + + 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'); + + 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) { + legend.width( availableWidth / 2 ); + + g.select('.nv-legendWrap') + .datum(data.map(function(series) { + series.originalKey = series.originalKey === undefined ? series.key : series.originalKey; + series.key = series.originalKey + (series.bar ? ' (left axis)' : ' (right axis)'); + return series; + })) + .call(legend); + + if ( margin.top != legend.height()) { + margin.top = legend.height(); + availableHeight1 = (height || parseInt(container.style('height')) || 400) + - margin.top - margin.bottom - height2; + } + + g.select('.nv-legendWrap') + .attr('transform', 'translate(' + ( availableWidth / 2 ) + ',' + (-margin.top) +')'); + } + + //------------------------------------------------------------ + + + wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + + + //------------------------------------------------------------ + // Context Components + + 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); + + //------------------------------------------------------------ + + + + //------------------------------------------------------------ + // 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); + + //------------------------------------------------------------ + + //------------------------------------------------------------ + // Setup Secondary (Context) Axes + + x2Axis + .ticks( availableWidth / 100 ) + .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); + + + y3Axis + .scale(y3) + .ticks( availableHeight2 / 36 ) + .tickSize( -availableWidth, 0); + + g.select('.nv-context .nv-y1.nv-axis') + .style('opacity', dataBars.length ? 1 : 0) + .attr('transform', 'translate(0,' + x2.range()[0] + ')'); + + g.select('.nv-context .nv-y1.nv-axis').transition() + .call(y3Axis); + + + 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-y2.nv-axis') + .style('opacity', dataLines.length ? 1 : 0) + .attr('transform', 'translate(' + x2.range()[1] + ',0)'); + + g.select('.nv-context .nv-y2.nv-axis').transition() + .call(y4Axis); + + //------------------------------------------------------------ + + //============================================================ + // Event Handling/Dispatching (in chart's scope) + //------------------------------------------------------------ + + legend.dispatch.on('stateChange', function(newState) { + chart.update(); + }); + + dispatch.on('tooltipShow', function(e) { + if (tooltips) showTooltip(e, that.parentNode); + }); + + //============================================================ + + + //============================================================ + // 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 { + 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( availableWidth / 100 ) + .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( availableHeight1 / 36 ) + .tickSize(-availableWidth, 0); + + g.select('.nv-focus .nv-y1.nv-axis') + .style('opacity', dataBars.length ? 1 : 0); + + + y2Axis + .scale(y2) + .ticks( availableHeight1 / 36 ) + .tickSize(dataBars.length ? 0 : -availableWidth, 0); // Show the y2 rules only if y1 has none + + g.select('.nv-focus .nv-y2.nv-axis') + .style('opacity', dataLines.length ? 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(e) { + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top]; + dispatch.tooltipShow(e); + }); + + lines.dispatch.on('elementMouseout.tooltip', function(e) { + dispatch.tooltipHide(e); + }); + + bars.dispatch.on('elementMouseover.tooltip', function(e) { + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top]; + dispatch.tooltipShow(e); + }); + + bars.dispatch.on('elementMouseout.tooltip', function(e) { + dispatch.tooltipHide(e); + }); + + dispatch.on('tooltipHide', function() { + if (tooltips) nv.tooltip.cleanup(); + }); + + //============================================================ + + + //============================================================ + // 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; + + d3.rebind(chart, lines, 'defined', 'size', 'clipVoronoi', 'interpolate'); + //TODO: consider rebinding x, y and some other stuff, and simply do soemthign lile bars.x(lines.x()), etc. + //d3.rebind(chart, lines, 'x', 'y', 'size', 'xDomain', 'yDomain', 'xRange', 'yRange', 'forceX', 'forceY', 'interactive', 'clipEdge', 'clipVoronoi', 'id'); + + chart.options = nv.utils.optionsFunc.bind(chart); + + chart.x = function(_) { + if (!arguments.length) return getX; + getX = _; + lines.x(_); + bars.x(_); + return chart; + }; + + chart.y = function(_) { + if (!arguments.length) return getY; + getY = _; + lines.y(_); + bars.y(_); + return chart; + }; + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + legend.color(color); + return chart; + }; + + chart.showLegend = function(_) { + if (!arguments.length) return showLegend; + showLegend = _; + return chart; + }; + + chart.tooltips = function(_) { + if (!arguments.length) return tooltips; + tooltips = _; + return chart; + }; + + chart.tooltipContent = function(_) { + if (!arguments.length) return tooltip; + tooltip = _; + return chart; + }; + + chart.noData = function(_) { + if (!arguments.length) return noData; + noData = _; + return chart; + }; + + chart.brushExtent = function(_) { + if (!arguments.length) return brushExtent; + brushExtent = _; + return 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 + , 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 + , clipEdge = true + , stacked = false + , color = 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 + , delay = 1200 + , xDomain + , yDomain + , xRange + , yRange + , groupSpacing = 0.1 + , dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout') + ; + + //============================================================ + + + //============================================================ + // Private Variables + //------------------------------------------------------------ + + var x0, y0 //used to store previous scales + ; + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + var availableWidth = width - margin.left - margin.right, + availableHeight = height - margin.top - margin.bottom, + container = d3.select(this); + + 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) + data = d3.layout.stack() + .offset('zero') + .values(function(d){ return d.values }) + .y(getY) + (!data.length && hideable ? hideable : data); + + + //add series index to each data point for reference + data = data.map(function(series, i) { + series.values = series.values.map(function(point) { + point.series = i; + return point; + }); + return series; + }); + + + //------------------------------------------------------------ + // 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; + 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) { + return d.values.map(function(d,i) { + return { x: getX(d,i), y: getY(d,i), y0: d.y0, y1: d.y1 } + }) + }); + + x .domain(xDomain || d3.merge(seriesData).map(function(d) { return d.x })) + .rangeBands(xRange || [0, availableWidth], groupSpacing); + + //y .domain(yDomain || d3.extent(d3.merge(seriesData).map(function(d) { return d.y + (stacked ? d.y1 : 0) }).concat(forceY))) + y .domain(yDomain || d3.extent(d3.merge(seriesData).map(function(d) { return stacked ? (d.y > 0 ? d.y1 : d.y1 + d.y ) : d.y }).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); + groups.exit() + .transition() + .selectAll('rect.nv-bar') + .delay(function(d,i) { + return i * delay/ data[0].values.length; + }) + .attr('y', function(d) { return stacked ? y0(d.y0) : y0(0) }) + .attr('height', 0) + .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 + .transition() + .style('stroke-opacity', 1) + .style('fill-opacity', .75); + + + var bars = groups.selectAll('rect.nv-bar') + .data(function(d) { return (hideable && !data.length) ? hideable.values : d.values }); + + bars.exit().remove(); + + + var barsEnter = bars.enter().append('rect') + .attr('class', function(d,i) { return getY(d,i) < 0 ? 'nv-bar negative' : 'nv-bar positive'}) + .attr('x', function(d,i,j) { + return stacked ? 0 : (j * x.rangeBand() / data.length ) + }) + .attr('y', function(d) { return y0(stacked ? d.y0 : 0) }) + .attr('height', 0) + .attr('width', x.rangeBand() / (stacked ? 1 : data.length) ) + .attr('transform', function(d,i) { return 'translate(' + x(getX(d,i)) + ',0)'; }) + ; + bars + .style('fill', function(d,i,j){ return color(d, j, i); }) + .style('stroke', function(d,i,j){ return color(d, j, i); }) + .on('mouseover', function(d,i) { //TODO: figure out why j works above, but not here + d3.select(this).classed('hover', true); + dispatch.elementMouseover({ + value: getY(d,i), + point: d, + series: data[d.series], + pos: [x(getX(d,i)) + (x.rangeBand() * (stacked ? data.length / 2 : d.series + .5) / data.length), y(getY(d,i) + (stacked ? d.y0 : 0))], // TODO: Figure out why the value appears to be shifted + pointIndex: i, + seriesIndex: d.series, + e: d3.event + }); + }) + .on('mouseout', function(d,i) { + d3.select(this).classed('hover', false); + dispatch.elementMouseout({ + value: getY(d,i), + point: d, + series: data[d.series], + pointIndex: i, + seriesIndex: d.series, + e: d3.event + }); + }) + .on('click', function(d,i) { + dispatch.elementClick({ + value: getY(d,i), + point: d, + series: data[d.series], + pos: [x(getX(d,i)) + (x.rangeBand() * (stacked ? data.length / 2 : d.series + .5) / data.length), y(getY(d,i) + (stacked ? d.y0 : 0))], // TODO: Figure out why the value appears to be shifted + pointIndex: i, + seriesIndex: d.series, + e: d3.event + }); + d3.event.stopPropagation(); + }) + .on('dblclick', function(d,i) { + dispatch.elementDblClick({ + value: getY(d,i), + point: d, + series: data[d.series], + pos: [x(getX(d,i)) + (x.rangeBand() * (stacked ? data.length / 2 : d.series + .5) / data.length), y(getY(d,i) + (stacked ? d.y0 : 0))], // TODO: Figure out why the value appears to be shifted + pointIndex: i, + seriesIndex: d.series, + e: d3.event + }); + d3.event.stopPropagation(); + }); + bars + .attr('class', function(d,i) { return getY(d,i) < 0 ? 'nv-bar negative' : 'nv-bar positive'}) + .transition() + .attr('transform', function(d,i) { return 'translate(' + x(getX(d,i)) + ',0)'; }) + + 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.transition() + .delay(function(d,i) { + + return i * delay / data[0].values.length; + }) + .attr('y', function(d,i) { + + return y((stacked ? d.y1 : 0)); + }) + .attr('height', function(d,i) { + return Math.max(Math.abs(y(d.y + (stacked ? d.y0 : 0)) - y((stacked ? d.y0 : 0))),1); + }) + .attr('x', function(d,i) { + return stacked ? 0 : (d.series * x.rangeBand() / data.length ) + }) + .attr('width', x.rangeBand() / (stacked ? 1 : data.length) ); + else + bars.transition() + .delay(function(d,i) { + return i * delay/ data[0].values.length; + }) + .attr('x', function(d,i) { + return d.series * x.rangeBand() / data.length + }) + .attr('width', x.rangeBand() / data.length) + .attr('y', function(d,i) { + return getY(d,i) < 0 ? + y(0) : + y(0) - y(getY(d,i)) < 1 ? + y(0) - 1 : + y(getY(d,i)) || 0; + }) + .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(); + + }); + + return chart; + } + + + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + + chart.dispatch = dispatch; + + chart.options = nv.utils.optionsFunc.bind(chart); + + chart.x = function(_) { + if (!arguments.length) return getX; + getX = _; + return chart; + }; + + chart.y = function(_) { + if (!arguments.length) return getY; + getY = _; + return chart; + }; + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.xScale = function(_) { + if (!arguments.length) return x; + x = _; + return chart; + }; + + chart.yScale = function(_) { + if (!arguments.length) return y; + y = _; + return chart; + }; + + chart.xDomain = function(_) { + if (!arguments.length) return xDomain; + xDomain = _; + return chart; + }; + + chart.yDomain = function(_) { + if (!arguments.length) return yDomain; + yDomain = _; + return chart; + }; + + chart.xRange = function(_) { + if (!arguments.length) return xRange; + xRange = _; + return chart; + }; + + chart.yRange = function(_) { + if (!arguments.length) return yRange; + yRange = _; + return chart; + }; + + chart.forceY = function(_) { + if (!arguments.length) return forceY; + forceY = _; + return chart; + }; + + chart.stacked = function(_) { + if (!arguments.length) return stacked; + stacked = _; + return chart; + }; + + chart.clipEdge = function(_) { + if (!arguments.length) return clipEdge; + clipEdge = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + return chart; + }; + + chart.barColor = function(_) { + if (!arguments.length) return barColor; + barColor = nv.utils.getColor(_); + return chart; + }; + + chart.disabled = function(_) { + if (!arguments.length) return disabled; + disabled = _; + return chart; + }; + + chart.id = function(_) { + if (!arguments.length) return id; + id = _; + return chart; + }; + + chart.hideable = function(_) { + if (!arguments.length) return hideable; + hideable = _; + return chart; + }; + + chart.delay = function(_) { + if (!arguments.length) return delay; + delay = _; + return chart; + }; + + chart.groupSpacing = function(_) { + if (!arguments.length) return groupSpacing; + groupSpacing = _; + return 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() + ; + + var margin = {top: 30, right: 20, bottom: 50, left: 60} + , width = null + , height = null + , color = nv.utils.defaultColor() + , showControls = true + , showLegend = true + , showXAxis = true + , showYAxis = true + , rightAlignYAxis = false + , reduceXTicks = true // if false a tick will show for every data point + , staggerLabels = false + , rotateLabels = 0 + , tooltips = true + , tooltip = function(key, x, y, e, graph) { + return '

' + key + '

' + + '

' + y + ' on ' + x + '

' + } + , x //can be accessed via chart.xScale() + , y //can be accessed via chart.yScale() + , state = { stacked: false } + , defaultState = null + , noData = "No Data Available." + , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'stateChange', 'changeState') + , controlWidth = function() { return showControls ? 180 : 0 } + , transitionDuration = 250 + ; + + multibar + .stacked(false) + ; + xAxis + .orient('bottom') + .tickPadding(7) + .highlightZero(true) + .showMaxMin(false) + .tickFormat(function(d) { return d }) + ; + yAxis + .orient((rightAlignYAxis) ? 'right' : 'left') + .tickFormat(d3.format(',.1f')) + ; + + controls.updateState(false); + //============================================================ + + + //============================================================ + // Private Variables + //------------------------------------------------------------ + + var showTooltip = function(e, offsetElement) { + var left = e.pos[0] + ( offsetElement.offsetLeft || 0 ), + top = e.pos[1] + ( offsetElement.offsetTop || 0), + x = xAxis.tickFormat()(multibar.x()(e.point, e.pointIndex)), + y = yAxis.tickFormat()(multibar.y()(e.point, e.pointIndex)), + content = tooltip(e.series.key, x, y, e, chart); + + nv.tooltip.show([left, top], content, e.value < 0 ? 'n' : 's', null, offsetElement); + }; + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + var container = d3.select(this), + that = this; + + 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() { 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) { + 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 = 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()); + + if (multibar.barColor()) + data.forEach(function(series,i) { + series.color = d3.rgb('#ccc').darker(i * 1.5).toString(); + }) + + g.select('.nv-legendWrap') + .datum(data) + .call(legend); + + if ( margin.top != legend.height()) { + margin.top = legend.height(); + availableHeight = (height || parseInt(container.style('height')) || 400) + - margin.top - margin.bottom; + } + + g.select('.nv-legendWrap') + .attr('transform', 'translate(' + controlWidth() + ',' + (-margin.top) +')'); + } + + //------------------------------------------------------------ + + + //------------------------------------------------------------ + // Controls + + if (showControls) { + var controlsData = [ + { key: 'Grouped', disabled: multibar.stacked() }, + { key: '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.transition().call(multibar); + + //------------------------------------------------------------ + + + //------------------------------------------------------------ + // Setup Axes + + if (showXAxis) { + xAxis + .scale(x) + .ticks( availableWidth / 100 ) + .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); + + 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( availableHeight / 36 ) + .tickSize( -availableWidth, 0); + + g.select('.nv-y.nv-axis').transition() + .call(yAxis); + } + + + //------------------------------------------------------------ + + + + //============================================================ + // Event Handling/Dispatching (in chart's scope) + //------------------------------------------------------------ + + legend.dispatch.on('stateChange', function(newState) { + state = newState; + 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); + + chart.update(); + }); + + dispatch.on('tooltipShow', function(e) { + if (tooltips) showTooltip(e, that.parentNode) + }); + + // 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; + } + + chart.update(); + }); + + //============================================================ + + + }); + + return chart; + } + + + //============================================================ + // Event Handling/Dispatching (out of chart's scope) + //------------------------------------------------------------ + + multibar.dispatch.on('elementMouseover.tooltip', function(e) { + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top]; + dispatch.tooltipShow(e); + }); + + multibar.dispatch.on('elementMouseout.tooltip', function(e) { + dispatch.tooltipHide(e); + }); + dispatch.on('tooltipHide', function() { + if (tooltips) nv.tooltip.cleanup(); + }); + + //============================================================ + + + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + + // expose chart's sub-components + chart.dispatch = dispatch; + chart.multibar = multibar; + chart.legend = legend; + chart.xAxis = xAxis; + chart.yAxis = yAxis; + + d3.rebind(chart, multibar, 'x', 'y', 'xDomain', 'yDomain', 'xRange', 'yRange', 'forceX', 'forceY', 'clipEdge', + 'id', 'stacked', 'delay', 'barColor','groupSpacing'); + + chart.options = nv.utils.optionsFunc.bind(chart); + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + legend.color(color); + return chart; + }; + + chart.showControls = function(_) { + if (!arguments.length) return showControls; + showControls = _; + return chart; + }; + + chart.showLegend = function(_) { + if (!arguments.length) return showLegend; + showLegend = _; + return chart; + }; + + chart.showXAxis = function(_) { + if (!arguments.length) return showXAxis; + showXAxis = _; + return chart; + }; + + chart.showYAxis = function(_) { + if (!arguments.length) return showYAxis; + showYAxis = _; + return chart; + }; + + chart.rightAlignYAxis = function(_) { + if(!arguments.length) return rightAlignYAxis; + rightAlignYAxis = _; + yAxis.orient( (_) ? 'right' : 'left'); + return chart; + }; + + chart.reduceXTicks= function(_) { + if (!arguments.length) return reduceXTicks; + reduceXTicks = _; + return chart; + }; + + chart.rotateLabels = function(_) { + if (!arguments.length) return rotateLabels; + rotateLabels = _; + return chart; + } + + chart.staggerLabels = function(_) { + if (!arguments.length) return staggerLabels; + staggerLabels = _; + return chart; + }; + + chart.tooltip = function(_) { + if (!arguments.length) return tooltip; + tooltip = _; + return chart; + }; + + chart.tooltips = function(_) { + if (!arguments.length) return tooltips; + tooltips = _; + return chart; + }; + + chart.tooltipContent = function(_) { + if (!arguments.length) return tooltip; + tooltip = _; + return chart; + }; + + chart.state = function(_) { + if (!arguments.length) return state; + state = _; + return chart; + }; + + chart.defaultState = function(_) { + if (!arguments.length) return defaultState; + defaultState = _; + return chart; + }; + + chart.noData = function(_) { + if (!arguments.length) return noData; + noData = _; + return chart; + }; + + chart.transitionDuration = function(_) { + if (!arguments.length) return transitionDuration; + transitionDuration = _; + return 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 + , 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() + , 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 + , stacked = false + , showValues = false + , valuePadding = 60 + , valueFormat = d3.format(',.2f') + , delay = 1200 + , xDomain + , yDomain + , xRange + , yRange + , dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout') + ; + + //============================================================ + + + //============================================================ + // Private Variables + //------------------------------------------------------------ + + var x0, y0 //used to store previous scales + ; + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + var availableWidth = width - margin.left - margin.right, + availableHeight = height - margin.top - margin.bottom, + container = d3.select(this); + + + if (stacked) + data = d3.layout.stack() + .offset('zero') + .values(function(d){ return d.values }) + .y(getY) + (data); + + + //add series index to each data point for reference + data = data.map(function(series, i) { + series.values = series.values.map(function(point) { + point.series = i; + return point; + }); + return series; + }); + + + + //------------------------------------------------------------ + // 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 } + }) + }); + + x .domain(xDomain || d3.merge(seriesData).map(function(d) { return d.x })) + .rangeBands(xRange || [0, availableHeight], .1); + + //y .domain(yDomain || d3.extent(d3.merge(seriesData).map(function(d) { return d.y + (stacked ? d.y0 : 0) }).concat(forceY))) + y .domain(yDomain || d3.extent(d3.merge(seriesData).map(function(d) { return stacked ? (d.y > 0 ? d.y1 + d.y : d.y1 ) : d.y }).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().transition() + .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.transition() + .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({ + value: getY(d,i), + point: d, + series: data[d.series], + pos: [ y(getY(d,i) + (stacked ? d.y0 : 0)), x(getX(d,i)) + (x.rangeBand() * (stacked ? data.length / 2 : d.series + .5) / data.length) ], + pointIndex: i, + seriesIndex: d.series, + e: d3.event + }); + }) + .on('mouseout', function(d,i) { + d3.select(this).classed('hover', false); + dispatch.elementMouseout({ + value: getY(d,i), + point: d, + series: data[d.series], + pointIndex: i, + seriesIndex: d.series, + e: d3.event + }); + }) + .on('click', function(d,i) { + dispatch.elementClick({ + value: getY(d,i), + point: d, + series: data[d.series], + pos: [x(getX(d,i)) + (x.rangeBand() * (stacked ? data.length / 2 : d.series + .5) / data.length), y(getY(d,i) + (stacked ? d.y0 : 0))], // TODO: Figure out why the value appears to be shifted + pointIndex: i, + seriesIndex: d.series, + e: d3.event + }); + d3.event.stopPropagation(); + }) + .on('dblclick', function(d,i) { + dispatch.elementDblClick({ + value: getY(d,i), + point: d, + series: data[d.series], + pos: [x(getX(d,i)) + (x.rangeBand() * (stacked ? data.length / 2 : d.series + .5) / data.length), y(getY(d,i) + (stacked ? d.y0 : 0))], // TODO: Figure out why the value appears to be shifted + pointIndex: i, + seriesIndex: d.series, + e: d3.event + }); + d3.event.stopPropagation(); + }); + + + 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) { return valueFormat(getY(d,i)) }) + bars.transition() + .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(''); + } + + 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.transition() + .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.transition() + .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(); + + }); + + return chart; + } + + + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + + chart.dispatch = dispatch; + + chart.options = nv.utils.optionsFunc.bind(chart); + + chart.x = function(_) { + if (!arguments.length) return getX; + getX = _; + return chart; + }; + + chart.y = function(_) { + if (!arguments.length) return getY; + getY = _; + return chart; + }; + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.xScale = function(_) { + if (!arguments.length) return x; + x = _; + return chart; + }; + + chart.yScale = function(_) { + if (!arguments.length) return y; + y = _; + return chart; + }; + + chart.xDomain = function(_) { + if (!arguments.length) return xDomain; + xDomain = _; + return chart; + }; + + chart.yDomain = function(_) { + if (!arguments.length) return yDomain; + yDomain = _; + return chart; + }; + + chart.xRange = function(_) { + if (!arguments.length) return xRange; + xRange = _; + return chart; + }; + + chart.yRange = function(_) { + if (!arguments.length) return yRange; + yRange = _; + return chart; + }; + + chart.forceY = function(_) { + if (!arguments.length) return forceY; + forceY = _; + return chart; + }; + + chart.stacked = function(_) { + if (!arguments.length) return stacked; + stacked = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + return chart; + }; + + chart.barColor = function(_) { + if (!arguments.length) return barColor; + barColor = nv.utils.getColor(_); + return chart; + }; + + chart.disabled = function(_) { + if (!arguments.length) return disabled; + disabled = _; + return chart; + }; + + chart.id = function(_) { + if (!arguments.length) return id; + id = _; + return chart; + }; + + chart.delay = function(_) { + if (!arguments.length) return delay; + delay = _; + return chart; + }; + + chart.showValues = function(_) { + if (!arguments.length) return showValues; + showValues = _; + return chart; + }; + + chart.valueFormat= function(_) { + if (!arguments.length) return valueFormat; + valueFormat = _; + return chart; + }; + + chart.valuePadding = function(_) { + if (!arguments.length) return valuePadding; + valuePadding = _; + return 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) + ; + + var margin = {top: 30, right: 20, bottom: 50, left: 60} + , width = null + , height = null + , color = nv.utils.defaultColor() + , showControls = true + , showLegend = true + , stacked = false + , tooltips = true + , tooltip = function(key, x, y, e, graph) { + return '

' + key + ' - ' + x + '

' + + '

' + y + '

' + } + , x //can be accessed via chart.xScale() + , y //can be accessed via chart.yScale() + , state = { stacked: stacked } + , defaultState = null + , noData = 'No Data Available.' + , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'stateChange', 'changeState') + , controlWidth = function() { return showControls ? 180 : 0 } + , transitionDuration = 250 + ; + + multibar + .stacked(stacked) + ; + xAxis + .orient('left') + .tickPadding(5) + .highlightZero(false) + .showMaxMin(false) + .tickFormat(function(d) { return d }) + ; + yAxis + .orient('bottom') + .tickFormat(d3.format(',.1f')) + ; + + controls.updateState(false); + //============================================================ + + + //============================================================ + // Private Variables + //------------------------------------------------------------ + + var showTooltip = function(e, offsetElement) { + var left = e.pos[0] + ( offsetElement.offsetLeft || 0 ), + top = e.pos[1] + ( offsetElement.offsetTop || 0), + x = xAxis.tickFormat()(multibar.x()(e.point, e.pointIndex)), + y = yAxis.tickFormat()(multibar.y()(e.point, e.pointIndex)), + content = tooltip(e.series.key, x, y, e, chart); + + nv.tooltip.show([left, top], content, e.value < 0 ? 'e' : 'w', null, offsetElement); + }; + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + var container = d3.select(this), + that = this; + + 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() { 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 No Data message if there's nothing to show. + + if (!data || !data.length || !data.filter(function(d) { return d.values.length }).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 = 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'); + 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()); + + if (multibar.barColor()) + data.forEach(function(series,i) { + series.color = d3.rgb('#ccc').darker(i * 1.5).toString(); + }) + + g.select('.nv-legendWrap') + .datum(data) + .call(legend); + + if ( margin.top != legend.height()) { + margin.top = legend.height(); + availableHeight = (height || parseInt(container.style('height')) || 400) + - margin.top - margin.bottom; + } + + g.select('.nv-legendWrap') + .attr('transform', 'translate(' + controlWidth() + ',' + (-margin.top) +')'); + } + + //------------------------------------------------------------ + + + //------------------------------------------------------------ + // Controls + + if (showControls) { + var controlsData = [ + { key: 'Grouped', disabled: multibar.stacked() }, + { key: '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 + + xAxis + .scale(x) + .ticks( availableHeight / 24 ) + .tickSize(-availableWidth, 0); + + g.select('.nv-x.nv-axis').transition() + .call(xAxis); + + var xTicks = g.select('.nv-x.nv-axis').selectAll('g'); + + xTicks + .selectAll('line, text') + .style('opacity', 1) + + + yAxis + .scale(y) + .ticks( availableWidth / 100 ) + .tickSize( -availableHeight, 0); + + g.select('.nv-y.nv-axis') + .attr('transform', 'translate(0,' + availableHeight + ')'); + g.select('.nv-y.nv-axis').transition() + .call(yAxis); + + //------------------------------------------------------------ + + + + //============================================================ + // Event Handling/Dispatching (in chart's scope) + //------------------------------------------------------------ + + legend.dispatch.on('stateChange', function(newState) { + state = newState; + 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); + + chart.update(); + }); + + dispatch.on('tooltipShow', function(e) { + if (tooltips) showTooltip(e, that.parentNode); + }); + + // 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; + } + + selection.call(chart); + }); + //============================================================ + + + }); + + return chart; + } + + + //============================================================ + // Event Handling/Dispatching (out of chart's scope) + //------------------------------------------------------------ + + multibar.dispatch.on('elementMouseover.tooltip', function(e) { + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top]; + dispatch.tooltipShow(e); + }); + + multibar.dispatch.on('elementMouseout.tooltip', function(e) { + dispatch.tooltipHide(e); + }); + dispatch.on('tooltipHide', function() { + if (tooltips) nv.tooltip.cleanup(); + }); + + //============================================================ + + + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + + // expose chart's sub-components + chart.dispatch = dispatch; + chart.multibar = multibar; + chart.legend = legend; + chart.xAxis = xAxis; + chart.yAxis = yAxis; + + d3.rebind(chart, multibar, 'x', 'y', 'xDomain', 'yDomain', 'xRange', 'yRange', 'forceX', 'forceY', 'clipEdge', 'id', 'delay', 'showValues', 'valueFormat', 'stacked', 'barColor'); + + chart.options = nv.utils.optionsFunc.bind(chart); + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + legend.color(color); + return chart; + }; + + chart.showControls = function(_) { + if (!arguments.length) return showControls; + showControls = _; + return chart; + }; + + chart.showLegend = function(_) { + if (!arguments.length) return showLegend; + showLegend = _; + return chart; + }; + + chart.tooltip = function(_) { + if (!arguments.length) return tooltip; + tooltip = _; + return chart; + }; + + chart.tooltips = function(_) { + if (!arguments.length) return tooltips; + tooltips = _; + return chart; + }; + + chart.tooltipContent = function(_) { + if (!arguments.length) return tooltip; + tooltip = _; + return chart; + }; + + chart.state = function(_) { + if (!arguments.length) return state; + state = _; + return chart; + }; + + chart.defaultState = function(_) { + if (!arguments.length) return defaultState; + defaultState = _; + return chart; + }; + + chart.noData = function(_) { + if (!arguments.length) return noData; + noData = _; + return chart; + }; + + chart.transitionDuration = function(_) { + if (!arguments.length) return transitionDuration; + transitionDuration = _; + return 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 = d3.scale.category20().range(), + width = null, + height = null, + showLegend = true, + tooltips = true, + tooltip = function(key, x, y, e, graph) { + return '

' + key + '

' + + '

' + y + ' at ' + x + '

' + }, + x, + y, + yDomain1, + yDomain2 + ; //can be accessed via chart.lines.[x/y]Scale() + + //============================================================ + // 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), + + 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), + dispatch = d3.dispatch('tooltipShow', 'tooltipHide'); + + var showTooltip = function(e, offsetElement) { + var left = e.pos[0] + ( offsetElement.offsetLeft || 0 ), + top = e.pos[1] + ( offsetElement.offsetTop || 0), + x = xAxis.tickFormat()(lines1.x()(e.point, e.pointIndex)), + y = ((e.series.yAxis == 2) ? yAxis2 : yAxis1).tickFormat()(lines1.y()(e.point, e.pointIndex)), + content = tooltip(e.series.key, x, y, e, chart); + + nv.tooltip.show([left, top], content, undefined, undefined, offsetElement.offsetParent); + }; + + function chart(selection) { + selection.each(function(data) { + var container = d3.select(this), + that = this; + + chart.update = function() { container.transition().call(chart); }; + chart.container = this; + + var availableWidth = (width || parseInt(container.style('width')) || 960) + - margin.left - margin.right, + availableHeight = (height || parseInt(container.style('height')) || 400) + - margin.top - margin.bottom; + + var dataLines1 = data.filter(function(d) {return !d.disabled && d.type == 'line' && d.yAxis == 1}) + var dataLines2 = data.filter(function(d) {return !d.disabled && d.type == 'line' && d.yAxis == 2}) + var dataBars1 = data.filter(function(d) {return !d.disabled && d.type == 'bar' && d.yAxis == 1}) + var dataBars2 = data.filter(function(d) {return !d.disabled && d.type == 'bar' && d.yAxis == 2}) + var dataStack1 = data.filter(function(d) {return !d.disabled && d.type == 'area' && d.yAxis == 1}) + var dataStack2 = data.filter(function(d) {return !d.disabled && d.type == 'area' && d.yAxis == 2}) + + 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', 'x axis'); + gEnter.append('g').attr('class', 'y1 axis'); + gEnter.append('g').attr('class', 'y2 axis'); + gEnter.append('g').attr('class', 'lines1Wrap'); + gEnter.append('g').attr('class', 'lines2Wrap'); + 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'); + + if (showLegend) { + legend.width( availableWidth / 2 ); + + 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 = (height || parseInt(container.style('height')) || 400) + - margin.top - margin.bottom; + } + + g.select('.legendWrap') + .attr('transform', 'translate(' + ( availableWidth / 2 ) + ',' + (-margin.top) +')'); + } + + + lines1 + .width(availableWidth) + .height(availableHeight) + .interpolate("monotone") + .color(data.map(function(d,i) { + return d.color || color[i % color.length]; + }).filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 1 && data[i].type == 'line'})); + + lines2 + .width(availableWidth) + .height(availableHeight) + .interpolate("monotone") + .color(data.map(function(d,i) { + return d.color || color[i % color.length]; + }).filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 2 && data[i].type == 'line'})); + + bars1 + .width(availableWidth) + .height(availableHeight) + .color(data.map(function(d,i) { + return d.color || color[i % color.length]; + }).filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 1 && data[i].type == 'bar'})); + + bars2 + .width(availableWidth) + .height(availableHeight) + .color(data.map(function(d,i) { + return d.color || color[i % color.length]; + }).filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 2 && data[i].type == 'bar'})); + + stack1 + .width(availableWidth) + .height(availableHeight) + .color(data.map(function(d,i) { + return d.color || color[i % color.length]; + }).filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 1 && data[i].type == 'area'})); + + stack2 + .width(availableWidth) + .height(availableHeight) + .color(data.map(function(d,i) { + return d.color || color[i % color.length]; + }).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) + var bars1Wrap = g.select('.bars1Wrap') + .datum(dataBars1) + var stack1Wrap = g.select('.stack1Wrap') + .datum(dataStack1) + + var lines2Wrap = g.select('.lines2Wrap') + .datum(dataLines2) + var bars2Wrap = g.select('.bars2Wrap') + .datum(dataBars2) + var stack2Wrap = g.select('.stack2Wrap') + .datum(dataStack2) + + 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()) + bars1.yDomain(yScale1.domain()) + stack1.yDomain(yScale1.domain()) + + lines2.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);} + + + + xAxis + .ticks( availableWidth / 100 ) + .tickSize(-availableHeight, 0); + + g.select('.x.axis') + .attr('transform', 'translate(0,' + availableHeight + ')'); + d3.transition(g.select('.x.axis')) + .call(xAxis); + + yAxis1 + .ticks( availableHeight / 36 ) + .tickSize( -availableWidth, 0); + + + d3.transition(g.select('.y1.axis')) + .call(yAxis1); + + yAxis2 + .ticks( availableHeight / 36 ) + .tickSize( -availableWidth, 0); + + d3.transition(g.select('.y2.axis')) + .call(yAxis2); + + g.select('.y2.axis') + .style('opacity', series2.length ? 1 : 0) + .attr('transform', 'translate(' + x.range()[1] + ',0)'); + + legend.dispatch.on('stateChange', function(newState) { + chart.update(); + }); + + dispatch.on('tooltipShow', function(e) { + if (tooltips) showTooltip(e, that.parentNode); + }); + + }); + + return chart; + } + + + //============================================================ + // Event Handling/Dispatching (out of chart's scope) + //------------------------------------------------------------ + + lines1.dispatch.on('elementMouseover.tooltip', function(e) { + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top]; + dispatch.tooltipShow(e); + }); + + lines1.dispatch.on('elementMouseout.tooltip', function(e) { + dispatch.tooltipHide(e); + }); + + lines2.dispatch.on('elementMouseover.tooltip', function(e) { + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top]; + dispatch.tooltipShow(e); + }); + + lines2.dispatch.on('elementMouseout.tooltip', function(e) { + dispatch.tooltipHide(e); + }); + + bars1.dispatch.on('elementMouseover.tooltip', function(e) { + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top]; + dispatch.tooltipShow(e); + }); + + bars1.dispatch.on('elementMouseout.tooltip', function(e) { + dispatch.tooltipHide(e); + }); + + bars2.dispatch.on('elementMouseover.tooltip', function(e) { + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top]; + dispatch.tooltipShow(e); + }); + + bars2.dispatch.on('elementMouseout.tooltip', function(e) { + dispatch.tooltipHide(e); + }); + + stack1.dispatch.on('tooltipShow', function(e) { + //disable tooltips when value ~= 0 + //// TODO: consider removing points from voronoi that have 0 value instead of this hack + if (!Math.round(stack1.y()(e.point) * 100)) { // 100 will not be good for very small numbers... will have to think about making this valu dynamic, based on data range + setTimeout(function() { d3.selectAll('.point.hover').classed('hover', false) }, 0); + return false; + } + + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top], + dispatch.tooltipShow(e); + }); + + stack1.dispatch.on('tooltipHide', function(e) { + dispatch.tooltipHide(e); + }); + + stack2.dispatch.on('tooltipShow', function(e) { + //disable tooltips when value ~= 0 + //// TODO: consider removing points from voronoi that have 0 value instead of this hack + if (!Math.round(stack2.y()(e.point) * 100)) { // 100 will not be good for very small numbers... will have to think about making this valu dynamic, based on data range + setTimeout(function() { d3.selectAll('.point.hover').classed('hover', false) }, 0); + return false; + } + + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top], + dispatch.tooltipShow(e); + }); + + stack2.dispatch.on('tooltipHide', function(e) { + dispatch.tooltipHide(e); + }); + + lines1.dispatch.on('elementMouseover.tooltip', function(e) { + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top]; + dispatch.tooltipShow(e); + }); + + lines1.dispatch.on('elementMouseout.tooltip', function(e) { + dispatch.tooltipHide(e); + }); + + lines2.dispatch.on('elementMouseover.tooltip', function(e) { + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top]; + dispatch.tooltipShow(e); + }); + + lines2.dispatch.on('elementMouseout.tooltip', function(e) { + dispatch.tooltipHide(e); + }); + + dispatch.on('tooltipHide', function() { + if (tooltips) nv.tooltip.cleanup(); + }); + + + + //============================================================ + // Global getters and setters + //------------------------------------------------------------ + + chart.dispatch = dispatch; + chart.lines1 = lines1; + chart.lines2 = lines2; + chart.bars1 = bars1; + chart.bars2 = bars2; + chart.stack1 = stack1; + chart.stack2 = stack2; + chart.xAxis = xAxis; + chart.yAxis1 = yAxis1; + chart.yAxis2 = yAxis2; + chart.options = nv.utils.optionsFunc.bind(chart); + + chart.x = function(_) { + if (!arguments.length) return getX; + getX = _; + lines1.x(_); + bars1.x(_); + return chart; + }; + + chart.y = function(_) { + if (!arguments.length) return getY; + getY = _; + lines1.y(_); + bars1.y(_); + return chart; + }; + + chart.yDomain1 = function(_) { + if (!arguments.length) return yDomain1; + yDomain1 = _; + return chart; + }; + + chart.yDomain2 = function(_) { + if (!arguments.length) return yDomain2; + yDomain2 = _; + return chart; + }; + + chart.margin = function(_) { + if (!arguments.length) return margin; + margin = _; + return chart; + }; + + chart.width = function(_) { + if (!arguments.length) return width; + width = _; + return chart; + }; + + chart.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = _; + legend.color(_); + return chart; + }; + + chart.showLegend = function(_) { + if (!arguments.length) return showLegend; + showLegend = _; + return chart; + }; + + chart.tooltips = function(_) { + if (!arguments.length) return tooltips; + tooltips = _; + return chart; + }; + + chart.tooltipContent = function(_) { + if (!arguments.length) return tooltip; + tooltip = _; + return 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 = 960 + , height = 500 + , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one + , 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() + , xDomain + , yDomain + , xRange + , yRange + , dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout') + ; + + //============================================================ + + //============================================================ + // Private Variables + //------------------------------------------------------------ + + //TODO: store old scales for transitions + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + var availableWidth = width - margin.left - margin.right, + availableHeight = height - margin.top - margin.bottom, + container = d3.select(this); + + + //------------------------------------------------------------ + // 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.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] || y.domain()[0] === y.domain()[1]) singlePoint = true; + 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(); + + + var ticksEnter = 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) { + 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'; + }) + .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)) }) + .on('mouseover', function(d,i) { + d3.select(this).classed('hover', true); + dispatch.elementMouseover({ + point: d, + series: data[0], + pos: [x(getX(d,i)), y(getY(d,i))], // TODO: Figure out why the value appears to be shifted + pointIndex: i, + seriesIndex: 0, + e: d3.event + }); + + }) + .on('mouseout', function(d,i) { + d3.select(this).classed('hover', false); + dispatch.elementMouseout({ + point: d, + series: data[0], + pointIndex: i, + seriesIndex: 0, + e: d3.event + }); + }) + .on('click', function(d,i) { + dispatch.elementClick({ + //label: d[label], + value: getY(d,i), + data: d, + index: i, + pos: [x(getX(d,i)), y(getY(d,i))], + e: d3.event, + id: id + }); + d3.event.stopPropagation(); + }) + .on('dblclick', function(d,i) { + dispatch.elementDblClick({ + //label: d[label], + value: getY(d,i), + data: d, + index: i, + pos: [x(getX(d,i)), y(getY(d,i))], + e: d3.event, + id: id + }); + d3.event.stopPropagation(); + }); + + 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'; + }) + //.attr('width', (availableWidth / data[0].values.length) * .9 ) + + + //d3.transition(ticks) + //.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)) }); + //.order(); // not sure if this makes any sense for this model + + }); + + return chart; + } + + + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + + chart.dispatch = dispatch; + + chart.options = nv.utils.optionsFunc.bind(chart); + + chart.x = function(_) { + if (!arguments.length) return getX; + getX = _; + return chart; + }; + + chart.y = function(_) { + if (!arguments.length) return getY; + getY = _; + return chart; + }; + + chart.open = function(_) { + if (!arguments.length) return getOpen; + getOpen = _; + return chart; + }; + + chart.close = function(_) { + if (!arguments.length) return getClose; + getClose = _; + return chart; + }; + + chart.high = function(_) { + if (!arguments.length) return getHigh; + getHigh = _; + return chart; + }; + + chart.low = function(_) { + if (!arguments.length) return getLow; + getLow = _; + return chart; + }; + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.xScale = function(_) { + if (!arguments.length) return x; + x = _; + return chart; + }; + + chart.yScale = function(_) { + if (!arguments.length) return y; + y = _; + return chart; + }; + + chart.xDomain = function(_) { + if (!arguments.length) return xDomain; + xDomain = _; + return chart; + }; + + chart.yDomain = function(_) { + if (!arguments.length) return yDomain; + yDomain = _; + return chart; + }; + + chart.xRange = function(_) { + if (!arguments.length) return xRange; + xRange = _; + return chart; + }; + + chart.yRange = function(_) { + if (!arguments.length) return yRange; + yRange = _; + return chart; + }; + + chart.forceX = function(_) { + if (!arguments.length) return forceX; + forceX = _; + return chart; + }; + + chart.forceY = function(_) { + if (!arguments.length) return forceY; + forceY = _; + return chart; + }; + + chart.padData = function(_) { + if (!arguments.length) return padData; + padData = _; + return chart; + }; + + chart.clipEdge = function(_) { + if (!arguments.length) return clipEdge; + clipEdge = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + return chart; + }; + + chart.id = function(_) { + if (!arguments.length) return id; + id = _; + return 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 } + , getDescription = function(d) { return d.description } + , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one + , color = nv.utils.defaultColor() + , valueFormat = d3.format(',.2f') + , showLabels = true + , pieLabelsOutside = true + , donutLabelsOutside = false + , labelType = "key" + , labelThreshold = .02 //if slice percentage is under this, don't show label + , donut = false + , labelSunbeamLayout = false + , startAngle = false + , endAngle = false + , donutRatio = 0.5 + , dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout') + ; + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + var availableWidth = width - margin.left - margin.right, + availableHeight = height - margin.top - margin.bottom, + radius = Math.min(availableWidth, availableHeight) / 2, + arcRadius = radius-(radius / 5), + container = d3.select(this); + + + //------------------------------------------------------------ + // Setup containers and skeleton of chart + + //var wrap = container.selectAll('.nv-wrap.nv-pie').data([data]); + 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'); + + gEnter.append('g').attr('class', 'nv-pie'); + + wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + g.select('.nv-pie').attr('transform', 'translate(' + availableWidth / 2 + ',' + availableHeight / 2 + ')'); + + //------------------------------------------------------------ + + + container + .on('click', function(d,i) { + dispatch.chartClick({ + data: d, + index: i, + pos: d3.event, + id: id + }); + }); + + + var arc = d3.svg.arc() + .outerRadius(arcRadius); + + if (startAngle) arc.startAngle(startAngle) + if (endAngle) arc.endAngle(endAngle); + if (donut) arc.innerRadius(radius * donutRatio); + + // 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) }); + + var slices = wrap.select('.nv-pie').selectAll('.nv-slice') + .data(pie); + + slices.exit().remove(); + + var ae = slices.enter().append('g') + .attr('class', 'nv-slice') + .on('mouseover', function(d,i){ + d3.select(this).classed('hover', true); + dispatch.elementMouseover({ + label: getX(d.data), + value: getY(d.data), + point: d.data, + pointIndex: i, + pos: [d3.event.pageX, d3.event.pageY], + id: id + }); + }) + .on('mouseout', function(d,i){ + d3.select(this).classed('hover', false); + dispatch.elementMouseout({ + label: getX(d.data), + value: getY(d.data), + point: d.data, + index: i, + id: id + }); + }) + .on('click', function(d,i) { + dispatch.elementClick({ + label: getX(d.data), + value: getY(d.data), + point: d.data, + index: i, + pos: d3.event, + id: id + }); + d3.event.stopPropagation(); + }) + .on('dblclick', function(d,i) { + dispatch.elementDblClick({ + label: getX(d.data), + value: getY(d.data), + point: d.data, + index: i, + pos: d3.event, + id: id + }); + d3.event.stopPropagation(); + }); + + slices + .attr('fill', function(d,i) { return color(d, i); }) + .attr('stroke', function(d,i) { return color(d, i); }); + + var paths = ae.append('path') + .each(function(d) { this._current = d; }); + //.attr('d', arc); + + d3.transition(slices.select('path')) + .attr('d', arc) + .attrTween('d', arcTween); + + if (showLabels) { + // This does the normal label + var labelsArc = d3.svg.arc().innerRadius(0); + + if (pieLabelsOutside){ labelsArc = arc; } + + if (donutLabelsOutside) { labelsArc = d3.svg.arc().outerRadius(arc.outerRadius()); } + + ae.append("g").classed("nv-label", true) + .each(function(d, i) { + var group = d3.select(this); + + group + .attr('transform', function(d) { + if (labelSunbeamLayout) { + d.outerRadius = arcRadius + 10; // Set Outer Coordinate + d.innerRadius = arcRadius + 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.centroid(d) + ') rotate(' + rotateAngle + ')'; + } else { + d.outerRadius = radius + 10; // Set Outer Coordinate + d.innerRadius = radius + 15; // Set Inner Coordinate + return 'translate(' + labelsArc.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') + + + }); + + slices.select(".nv-label").transition() + .attr('transform', function(d) { + if (labelSunbeamLayout) { + d.outerRadius = arcRadius + 10; // Set Outer Coordinate + d.innerRadius = arcRadius + 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.centroid(d) + ') rotate(' + rotateAngle + ')'; + } else { + d.outerRadius = radius + 10; // Set Outer Coordinate + d.innerRadius = radius + 15; // Set Inner Coordinate + return 'translate(' + labelsArc.centroid(d) + ')' + } + }); + + slices.each(function(d, i) { + var slice = d3.select(this); + + slice + .select(".nv-label 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 + .text(function(d, i) { + var percent = (d.endAngle - d.startAngle) / (2 * Math.PI); + var labelTypes = { + "key" : getX(d.data), + "value": getY(d.data), + "percent": d3.format('%')(percent) + }; + return (d.value && percent > labelThreshold) ? labelTypes[labelType] : ''; + }); + + var textBox = slice.select('text').node().getBBox(); + slice.select(".nv-label rect") + .attr("width", textBox.width + 10) + .attr("height", textBox.height + 10) + .attr("transform", function() { + return "translate(" + [textBox.x - 5, textBox.y - 5] + ")"; + }); + }); + } + + + // 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) { + 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 arc(i(t)); + }; + } + + function tweenPie(b) { + b.innerRadius = 0; + var i = d3.interpolate({startAngle: 0, endAngle: 0}, b); + return function(t) { + return arc(i(t)); + }; + } + + }); + + return chart; + } + + + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + + chart.dispatch = dispatch; + chart.options = nv.utils.optionsFunc.bind(chart); + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.values = function(_) { + nv.log("pie.values() is no longer supported."); + return chart; + }; + + chart.x = function(_) { + if (!arguments.length) return getX; + getX = _; + return chart; + }; + + chart.y = function(_) { + if (!arguments.length) return getY; + getY = d3.functor(_); + return chart; + }; + + chart.description = function(_) { + if (!arguments.length) return getDescription; + getDescription = _; + return chart; + }; + + chart.showLabels = function(_) { + if (!arguments.length) return showLabels; + showLabels = _; + return chart; + }; + + chart.labelSunbeamLayout = function(_) { + if (!arguments.length) return labelSunbeamLayout; + labelSunbeamLayout = _; + return chart; + }; + + chart.donutLabelsOutside = function(_) { + if (!arguments.length) return donutLabelsOutside; + donutLabelsOutside = _; + return chart; + }; + + chart.pieLabelsOutside = function(_) { + if (!arguments.length) return pieLabelsOutside; + pieLabelsOutside = _; + return chart; + }; + + chart.labelType = function(_) { + if (!arguments.length) return labelType; + labelType = _; + labelType = labelType || "key"; + return chart; + }; + + chart.donut = function(_) { + if (!arguments.length) return donut; + donut = _; + return chart; + }; + + chart.donutRatio = function(_) { + if (!arguments.length) return donutRatio; + donutRatio = _; + return chart; + }; + + chart.startAngle = function(_) { + if (!arguments.length) return startAngle; + startAngle = _; + return chart; + }; + + chart.endAngle = function(_) { + if (!arguments.length) return endAngle; + endAngle = _; + return chart; + }; + + chart.id = function(_) { + if (!arguments.length) return id; + id = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + return chart; + }; + + chart.valueFormat = function(_) { + if (!arguments.length) return valueFormat; + valueFormat = _; + return chart; + }; + + chart.labelThreshold = function(_) { + if (!arguments.length) return labelThreshold; + labelThreshold = _; + return chart; + }; + //============================================================ + + + return chart; +} +nv.models.pieChart = function() { + "use strict"; + //============================================================ + // Public Variables with Default Settings + //------------------------------------------------------------ + + var pie = nv.models.pie() + , legend = nv.models.legend() + ; + + var margin = {top: 30, right: 20, bottom: 20, left: 20} + , width = null + , height = null + , showLegend = true + , color = nv.utils.defaultColor() + , tooltips = true + , tooltip = function(key, y, e, graph) { + return '

' + key + '

' + + '

' + y + '

' + } + , state = {} + , defaultState = null + , noData = "No Data Available." + , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'stateChange', 'changeState') + ; + + //============================================================ + + + //============================================================ + // Private Variables + //------------------------------------------------------------ + + var showTooltip = function(e, offsetElement) { + var tooltipLabel = pie.description()(e.point) || pie.x()(e.point) + var left = e.pos[0] + ( (offsetElement && offsetElement.offsetLeft) || 0 ), + top = e.pos[1] + ( (offsetElement && offsetElement.offsetTop) || 0), + y = pie.valueFormat()(pie.y()(e.point)), + content = tooltip(tooltipLabel, y, e, chart); + + nv.tooltip.show([left, top], content, e.value < 0 ? 'n' : 's', null, offsetElement); + }; + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + var container = d3.select(this), + that = this; + + 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() { container.transition().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 No Data message if there's nothing to show. + + if (!data || !data.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 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) { + legend + .width( availableWidth ) + .key(pie.x()); + + wrap.select('.nv-legendWrap') + .datum(data) + .call(legend); + + if ( margin.top != legend.height()) { + margin.top = legend.height(); + availableHeight = (height || parseInt(container.style('height')) || 400) + - margin.top - margin.bottom; + } + + wrap.select('.nv-legendWrap') + .attr('transform', 'translate(0,' + (-margin.top) +')'); + } + + //------------------------------------------------------------ + + + 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) { + state = newState; + dispatch.stateChange(state); + chart.update(); + }); + + pie.dispatch.on('elementMouseout.tooltip', function(e) { + dispatch.tooltipHide(e); + }); + + // 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(); + }); + + //============================================================ + + + }); + + return chart; + } + + //============================================================ + // Event Handling/Dispatching (out of chart's scope) + //------------------------------------------------------------ + + pie.dispatch.on('elementMouseover.tooltip', function(e) { + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top]; + dispatch.tooltipShow(e); + }); + + dispatch.on('tooltipShow', function(e) { + if (tooltips) showTooltip(e); + }); + + dispatch.on('tooltipHide', function() { + if (tooltips) nv.tooltip.cleanup(); + }); + + //============================================================ + + + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + + // expose chart's sub-components + chart.legend = legend; + chart.dispatch = dispatch; + chart.pie = pie; + + d3.rebind(chart, pie, 'valueFormat', 'values', 'x', 'y', 'description', 'id', 'showLabels', 'donutLabelsOutside', 'pieLabelsOutside', 'labelType', 'donut', 'donutRatio', 'labelThreshold'); + chart.options = nv.utils.optionsFunc.bind(chart); + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + legend.color(color); + pie.color(color); + return chart; + }; + + chart.showLegend = function(_) { + if (!arguments.length) return showLegend; + showLegend = _; + return chart; + }; + + chart.tooltips = function(_) { + if (!arguments.length) return tooltips; + tooltips = _; + return chart; + }; + + chart.tooltipContent = function(_) { + if (!arguments.length) return tooltip; + tooltip = _; + return chart; + }; + + chart.state = function(_) { + if (!arguments.length) return state; + state = _; + return chart; + }; + + chart.defaultState = function(_) { + if (!arguments.length) return defaultState; + defaultState = _; + return chart; + }; + + chart.noData = function(_) { + if (!arguments.length) return noData; + noData = _; + return 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 = 960 + , height = 500 + , color = nv.utils.defaultColor() // chooses color + , id = Math.floor(Math.random() * 100000) //Create semi-unique ID incase user doesn't select one + , 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 + , onlyCircles = true // Set to false to use shapes + , 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 + , pointKey = null + , 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 + , 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', 'elementMouseover', 'elementMouseout') + , useVoronoi = true + ; + + //============================================================ + + + //============================================================ + // 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 + ; + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + var availableWidth = width - margin.left - margin.right, + availableHeight = height - margin.top - margin.bottom, + container = d3.select(this); + + //add series index to each data point for reference + data = data.map(function(series, i) { + series.values = series.values.map(function(point) { + point.series = i; + return point; + }); + return series; + }); + + //------------------------------------------------------------ + // 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 || [16, 256]); + + // 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] || y.domain()[0] === y.domain()[1]) singlePoint = true; + 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 + (singlePoint ? ' nv-single-point' : '')); + 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-point-paths'); + + 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 + ')' : ''); + + + function updateInteractiveLayer() { + + if (!interactive) return false; + + var eventElements; + + 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-7, + y(pY)+ Math.random() * 1e-7, + groupIndex, + pointIndex, point]; //temp hack to add noise untill 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! + }) + }) + ); + + + + //inject series and point index for reference into voronoi + if (useVoronoi === true) { + + if (clipVoronoi) { + var pointClipsEnter = wrap.select('defs').selectAll('.nv-point-clips') + .data([id]) + .enter(); + + pointClipsEnter.append('clipPath') + .attr('class', 'nv-point-clips') + .attr('id', 'nv-points-clip-' + id); + + var pointClips = wrap.select('#nv-points-clip-' + id).selectAll('circle') + .data(vertices); + pointClips.enter().append('circle') + .attr('r', clipRadius); + pointClips.exit().remove(); + pointClips + .attr('cx', function(d) { return d[0] }) + .attr('cy', function(d) { return d[1] }); + + wrap.select('.nv-point-paths') + .attr('clip-path', 'url(#nv-points-clip-' + id + ')'); + } + + + if(vertices.length) { + // 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]); + } + + 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] + } + }); + + + var pointPaths = wrap.select('.nv-point-paths').selectAll('path') + .data(voronoi); + pointPaths.enter().append('path') + .attr('class', function(d,i) { return 'nv-path-'+i; }); + pointPaths.exit().remove(); + pointPaths + .attr('d', function(d) { + if (d.data.length === 0) + return 'M 0 0' + else + return 'M' + d.data.join('L') + 'Z'; + }); + + var mouseEventCallback = function(d,mDispatch) { + if (needsUpdate) return 0; + var series = data[d.series]; + if (typeof series === 'undefined') return; + + var point = series.values[d.point]; + + mDispatch({ + point: point, + series: series, + pos: [x(getX(point, d.point)) + margin.left, y(getY(point, d.point)) + margin.top], + seriesIndex: d.series, + pointIndex: d.point + }); + }; + + pointPaths + .on('click', function(d) { + mouseEventCallback(d, dispatch.elementClick); + }) + .on('mouseover', function(d) { + mouseEventCallback(d, dispatch.elementMouseover); + }) + .on('mouseout', function(d, i) { + mouseEventCallback(d, dispatch.elementMouseout); + }); + + + } else { + /* + // bring data in form needed for click handlers + var dataWithPoints = vertices.map(function(d, i) { + return { + 'data': d, + 'series': vertices[i][2], + 'point': vertices[i][3] + } + }); + */ + + // 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('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 + }); + }) + .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 + }); + }); + } + + needsUpdate = false; + } + + 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 + .transition() + .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); + + + if (onlyCircles) { + + var points = groups.selectAll('circle.nv-point') + .data(function(d) { return d.values }, pointKey); + points.enter().append('circle') + .style('fill', function (d,i) { return d.color }) + .style('stroke', function (d,i) { return d.color }) + .attr('cx', function(d,i) { return nv.utils.NaNtoZero(x0(getX(d,i))) }) + .attr('cy', function(d,i) { return nv.utils.NaNtoZero(y0(getY(d,i))) }) + .attr('r', function(d,i) { return Math.sqrt(z(getSize(d,i))/Math.PI) }); + points.exit().remove(); + groups.exit().selectAll('path.nv-point').transition() + .attr('cx', function(d,i) { return nv.utils.NaNtoZero(x(getX(d,i))) }) + .attr('cy', function(d,i) { return nv.utils.NaNtoZero(y(getY(d,i))) }) + .remove(); + points.each(function(d,i) { + d3.select(this) + .classed('nv-point', true) + .classed('nv-point-' + i, true) + .classed('hover',false) + ; + }); + points.transition() + .attr('cx', function(d,i) { return nv.utils.NaNtoZero(x(getX(d,i))) }) + .attr('cy', function(d,i) { return nv.utils.NaNtoZero(y(getY(d,i))) }) + .attr('r', function(d,i) { return Math.sqrt(z(getSize(d,i))/Math.PI) }); + + } else { + + var points = groups.selectAll('path.nv-point') + .data(function(d) { return d.values }); + points.enter().append('path') + .style('fill', function (d,i) { return d.color }) + .style('stroke', function (d,i) { return d.color }) + .attr('transform', function(d,i) { + return 'translate(' + x0(getX(d,i)) + ',' + y0(getY(d,i)) + ')' + }) + .attr('d', + d3.svg.symbol() + .type(getShape) + .size(function(d,i) { return z(getSize(d,i)) }) + ); + points.exit().remove(); + groups.exit().selectAll('path.nv-point') + .transition() + .attr('transform', function(d,i) { + return 'translate(' + x(getX(d,i)) + ',' + y(getY(d,i)) + ')' + }) + .remove(); + points.each(function(d,i) { + d3.select(this) + .classed('nv-point', true) + .classed('nv-point-' + i, true) + .classed('hover',false) + ; + }); + points.transition() + .attr('transform', function(d,i) { + //nv.log(d,i,getX(d,i), x(getX(d,i))); + return 'translate(' + x(getX(d,i)) + ',' + y(getY(d,i)) + ')' + }) + .attr('d', + d3.svg.symbol() + .type(getShape) + .size(function(d,i) { return z(getSize(d,i)) }) + ); + } + + + // 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(); + + }); + + return chart; + } + + + //============================================================ + // Event Handling/Dispatching (out of chart's scope) + //------------------------------------------------------------ + chart.clearHighlights = function() { + //Remove the 'hover' class from all highlighted points. + d3.selectAll(".nv-chart-" + id + " .nv-point.hover").classed("hover",false); + }; + + chart.highlightPoint = function(seriesIndex,pointIndex,isHoverOver) { + d3.select(".nv-chart-" + id + " .nv-series-" + seriesIndex + " .nv-point-" + pointIndex) + .classed("hover",isHoverOver); + }; + + + dispatch.on('elementMouseover.point', function(d) { + if (interactive) chart.highlightPoint(d.seriesIndex,d.pointIndex,true); + }); + + dispatch.on('elementMouseout.point', function(d) { + if (interactive) chart.highlightPoint(d.seriesIndex,d.pointIndex,false); + }); + + //============================================================ + + + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + + chart.dispatch = dispatch; + chart.options = nv.utils.optionsFunc.bind(chart); + + chart.x = function(_) { + if (!arguments.length) return getX; + getX = d3.functor(_); + return chart; + }; + + chart.y = function(_) { + if (!arguments.length) return getY; + getY = d3.functor(_); + return chart; + }; + + chart.size = function(_) { + if (!arguments.length) return getSize; + getSize = d3.functor(_); + return chart; + }; + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.xScale = function(_) { + if (!arguments.length) return x; + x = _; + return chart; + }; + + chart.yScale = function(_) { + if (!arguments.length) return y; + y = _; + return chart; + }; + + chart.zScale = function(_) { + if (!arguments.length) return z; + z = _; + return chart; + }; + + chart.xDomain = function(_) { + if (!arguments.length) return xDomain; + xDomain = _; + return chart; + }; + + chart.yDomain = function(_) { + if (!arguments.length) return yDomain; + yDomain = _; + return chart; + }; + + chart.sizeDomain = function(_) { + if (!arguments.length) return sizeDomain; + sizeDomain = _; + return chart; + }; + + chart.xRange = function(_) { + if (!arguments.length) return xRange; + xRange = _; + return chart; + }; + + chart.yRange = function(_) { + if (!arguments.length) return yRange; + yRange = _; + return chart; + }; + + chart.sizeRange = function(_) { + if (!arguments.length) return sizeRange; + sizeRange = _; + return chart; + }; + + chart.forceX = function(_) { + if (!arguments.length) return forceX; + forceX = _; + return chart; + }; + + chart.forceY = function(_) { + if (!arguments.length) return forceY; + forceY = _; + return chart; + }; + + chart.forceSize = function(_) { + if (!arguments.length) return forceSize; + forceSize = _; + return chart; + }; + + chart.interactive = function(_) { + if (!arguments.length) return interactive; + interactive = _; + return chart; + }; + + chart.pointKey = function(_) { + if (!arguments.length) return pointKey; + pointKey = _; + return chart; + }; + + chart.pointActive = function(_) { + if (!arguments.length) return pointActive; + pointActive = _; + return chart; + }; + + chart.padData = function(_) { + if (!arguments.length) return padData; + padData = _; + return chart; + }; + + chart.padDataOuter = function(_) { + if (!arguments.length) return padDataOuter; + padDataOuter = _; + return chart; + }; + + chart.clipEdge = function(_) { + if (!arguments.length) return clipEdge; + clipEdge = _; + return chart; + }; + + chart.clipVoronoi= function(_) { + if (!arguments.length) return clipVoronoi; + clipVoronoi = _; + return chart; + }; + + chart.useVoronoi= function(_) { + if (!arguments.length) return useVoronoi; + useVoronoi = _; + if (useVoronoi === false) { + clipVoronoi = false; + } + return chart; + }; + + chart.clipRadius = function(_) { + if (!arguments.length) return clipRadius; + clipRadius = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + return chart; + }; + + chart.shape = function(_) { + if (!arguments.length) return getShape; + getShape = _; + return chart; + }; + + chart.onlyCircles = function(_) { + if (!arguments.length) return onlyCircles; + onlyCircles = _; + return chart; + }; + + chart.id = function(_) { + if (!arguments.length) return id; + id = _; + return chart; + }; + + chart.singlePoint = function(_) { + if (!arguments.length) return singlePoint; + singlePoint = _; + return 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() + , controls = nv.models.legend() + , distX = nv.models.distribution() + , distY = nv.models.distribution() + ; + + var margin = {top: 30, right: 20, bottom: 50, left: 75} + , width = null + , height = null + , color = nv.utils.defaultColor() + , x = d3.fisheye ? d3.fisheye.scale(d3.scale.linear).distortion(0) : scatter.xScale() + , y = d3.fisheye ? d3.fisheye.scale(d3.scale.linear).distortion(0) : scatter.yScale() + , xPadding = 0 + , yPadding = 0 + , showDistX = false + , showDistY = false + , showLegend = true + , showXAxis = true + , showYAxis = true + , rightAlignYAxis = false + , showControls = !!d3.fisheye + , fisheye = 0 + , pauseFisheye = false + , tooltips = true + , tooltipX = function(key, x, y) { return '' + x + '' } + , tooltipY = function(key, x, y) { return '' + y + '' } + , tooltip = null + , state = {} + , defaultState = null + , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'stateChange', 'changeState') + , noData = "No Data Available." + , transitionDuration = 250 + ; + + scatter + .xScale(x) + .yScale(y) + ; + xAxis + .orient('bottom') + .tickPadding(10) + ; + yAxis + .orient((rightAlignYAxis) ? 'right' : 'left') + .tickPadding(10) + ; + distX + .axis('x') + ; + distY + .axis('y') + ; + + controls.updateState(false); + + //============================================================ + + + //============================================================ + // Private Variables + //------------------------------------------------------------ + + var x0, y0; + + var showTooltip = function(e, offsetElement) { + //TODO: make tooltip style an option between single or dual on axes (maybe on all charts with axes?) + + var left = e.pos[0] + ( offsetElement.offsetLeft || 0 ), + top = e.pos[1] + ( offsetElement.offsetTop || 0), + leftX = e.pos[0] + ( offsetElement.offsetLeft || 0 ), + topX = y.range()[0] + margin.top + ( offsetElement.offsetTop || 0), + leftY = x.range()[0] + margin.left + ( offsetElement.offsetLeft || 0 ), + topY = e.pos[1] + ( offsetElement.offsetTop || 0), + xVal = xAxis.tickFormat()(scatter.x()(e.point, e.pointIndex)), + yVal = yAxis.tickFormat()(scatter.y()(e.point, e.pointIndex)); + + if( tooltipX != null ) + nv.tooltip.show([leftX, topX], tooltipX(e.series.key, xVal, yVal, e, chart), 'n', 1, offsetElement, 'x-nvtooltip'); + if( tooltipY != null ) + nv.tooltip.show([leftY, topY], tooltipY(e.series.key, xVal, yVal, e, chart), 'e', 1, offsetElement, 'y-nvtooltip'); + if( tooltip != null ) + nv.tooltip.show([left, top], tooltip(e.series.key, xVal, yVal, e, chart), e.value < 0 ? 'n' : 's', null, offsetElement); + }; + + var controlsData = [ + { key: 'Magnify', disabled: true } + ]; + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + var container = d3.select(this), + that = this; + + 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() { 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) { + 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 + + x0 = x0 || x; + y0 = y0 || y; + + //------------------------------------------------------------ + + + //------------------------------------------------------------ + // 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'); + + 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-distWrap'); + gEnter.append('g').attr('class', 'nv-legendWrap'); + gEnter.append('g').attr('class', 'nv-controlsWrap'); + + //------------------------------------------------------------ + + + //------------------------------------------------------------ + // Legend + + if (showLegend) { + var legendWidth = (showControls) ? availableWidth / 2 : availableWidth; + legend.width(legendWidth); + + wrap.select('.nv-legendWrap') + .datum(data) + .call(legend); + + if ( margin.top != legend.height()) { + margin.top = legend.height(); + availableHeight = (height || parseInt(container.style('height')) || 400) + - margin.top - margin.bottom; + } + + wrap.select('.nv-legendWrap') + .attr('transform', 'translate(' + (availableWidth - legendWidth) + ',' + (-margin.top) +')'); + } + + //------------------------------------------------------------ + + + //------------------------------------------------------------ + // Controls + + if (showControls) { + controls.width(180).color(['#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) + + scatter + .width(availableWidth) + .height(availableHeight) + .color(data.map(function(d,i) { + return d.color || color(d, i); + }).filter(function(d,i) { return !data[i].disabled })); + + if (xPadding !== 0) + scatter.xDomain(null); + + if (yPadding !== 0) + scatter.yDomain(null); + + wrap.select('.nv-scatterWrap') + .datum(data.filter(function(d) { return !d.disabled })) + .call(scatter); + + //Adjust for x and y padding + if (xPadding !== 0) { + var xRange = x.domain()[1] - x.domain()[0]; + scatter.xDomain([x.domain()[0] - (xPadding * xRange), x.domain()[1] + (xPadding * xRange)]); + } + + if (yPadding !== 0) { + var yRange = y.domain()[1] - y.domain()[0]; + scatter.yDomain([y.domain()[0] - (yPadding * yRange), y.domain()[1] + (yPadding * yRange)]); + } + + //Only need to update the scatter again if x/yPadding changed the domain. + if (yPadding !== 0 || xPadding !== 0) { + wrap.select('.nv-scatterWrap') + .datum(data.filter(function(d) { return !d.disabled })) + .call(scatter); + } + + //------------------------------------------------------------ + + + //------------------------------------------------------------ + // Setup Axes + if (showXAxis) { + xAxis + .scale(x) + .ticks( xAxis.ticks() && xAxis.ticks().length ? xAxis.ticks() : availableWidth / 100 ) + .tickSize( -availableHeight , 0); + + g.select('.nv-x.nv-axis') + .attr('transform', 'translate(0,' + y.range()[0] + ')') + .call(xAxis); + + } + + if (showYAxis) { + yAxis + .scale(y) + .ticks( yAxis.ticks() && yAxis.ticks().length ? yAxis.ticks() : availableHeight / 36 ) + .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); + } + + //------------------------------------------------------------ + + + + + if (d3.fisheye) { + g.select('.nv-background') + .attr('width', availableWidth) + .attr('height', availableHeight); + + g.select('.nv-background').on('mousemove', updateFisheye); + g.select('.nv-background').on('click', function() { pauseFisheye = !pauseFisheye;}); + scatter.dispatch.on('elementClick.freezeFisheye', function() { + pauseFisheye = !pauseFisheye; + }); + } + + + function updateFisheye() { + if (pauseFisheye) { + g.select('.nv-point-paths').style('pointer-events', 'all'); + return false; + } + + g.select('.nv-point-paths').style('pointer-events', 'none' ); + + var mouse = d3.mouse(this); + x.distortion(fisheye).focus(mouse[0]); + y.distortion(fisheye).focus(mouse[1]); + + g.select('.nv-scatterWrap') + .call(scatter); + + if (showXAxis) + g.select('.nv-x.nv-axis').call(xAxis); + + if (showYAxis) + g.select('.nv-y.nv-axis').call(yAxis); + + g.select('.nv-distributionX') + .datum(data.filter(function(d) { return !d.disabled })) + .call(distX); + g.select('.nv-distributionY') + .datum(data.filter(function(d) { return !d.disabled })) + .call(distY); + } + + + + //============================================================ + // Event Handling/Dispatching (in chart's scope) + //------------------------------------------------------------ + + controls.dispatch.on('legendClick', function(d,i) { + d.disabled = !d.disabled; + + fisheye = d.disabled ? 0 : 2.5; + g.select('.nv-background') .style('pointer-events', d.disabled ? 'none' : 'all'); + g.select('.nv-point-paths').style('pointer-events', d.disabled ? 'all' : 'none' ); + + if (d.disabled) { + x.distortion(fisheye).focus(0); + y.distortion(fisheye).focus(0); + + g.select('.nv-scatterWrap').call(scatter); + g.select('.nv-x.nv-axis').call(xAxis); + g.select('.nv-y.nv-axis').call(yAxis); + } else { + pauseFisheye = false; + } + + chart.update(); + }); + + legend.dispatch.on('stateChange', function(newState) { + state.disabled = newState.disabled; + dispatch.stateChange(state); + chart.update(); + }); + + scatter.dispatch.on('elementMouseover.tooltip', function(e) { + d3.select('.nv-chart-' + scatter.id() + ' .nv-series-' + e.seriesIndex + ' .nv-distx-' + e.pointIndex) + .attr('y1', function(d,i) { return e.pos[1] - availableHeight;}); + d3.select('.nv-chart-' + scatter.id() + ' .nv-series-' + e.seriesIndex + ' .nv-disty-' + e.pointIndex) + .attr('x2', e.pos[0] + distX.size()); + + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top]; + dispatch.tooltipShow(e); + }); + + dispatch.on('tooltipShow', function(e) { + if (tooltips) showTooltip(e, that.parentNode); + }); + + // 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(); + }); + + //============================================================ + + + //store old scales for use in transitions on update + x0 = x.copy(); + y0 = y.copy(); + + + }); + + return chart; + } + + + //============================================================ + // Event Handling/Dispatching (out of chart's scope) + //------------------------------------------------------------ + + scatter.dispatch.on('elementMouseout.tooltip', function(e) { + dispatch.tooltipHide(e); + + d3.select('.nv-chart-' + scatter.id() + ' .nv-series-' + e.seriesIndex + ' .nv-distx-' + e.pointIndex) + .attr('y1', 0); + d3.select('.nv-chart-' + scatter.id() + ' .nv-series-' + e.seriesIndex + ' .nv-disty-' + e.pointIndex) + .attr('x2', distY.size()); + }); + dispatch.on('tooltipHide', function() { + if (tooltips) nv.tooltip.cleanup(); + }); + + //============================================================ + + + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + + // expose chart's sub-components + chart.dispatch = dispatch; + chart.scatter = scatter; + chart.legend = legend; + chart.controls = controls; + chart.xAxis = xAxis; + chart.yAxis = yAxis; + chart.distX = distX; + chart.distY = distY; + + d3.rebind(chart, scatter, 'id', 'interactive', 'pointActive', 'x', 'y', 'shape', 'size', 'xScale', 'yScale', 'zScale', 'xDomain', 'yDomain', 'xRange', 'yRange', 'sizeDomain', 'sizeRange', 'forceX', 'forceY', 'forceSize', 'clipVoronoi', 'clipRadius', 'useVoronoi'); + chart.options = nv.utils.optionsFunc.bind(chart); + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + legend.color(color); + distX.color(color); + distY.color(color); + return chart; + }; + + chart.showDistX = function(_) { + if (!arguments.length) return showDistX; + showDistX = _; + return chart; + }; + + chart.showDistY = function(_) { + if (!arguments.length) return showDistY; + showDistY = _; + return chart; + }; + + chart.showControls = function(_) { + if (!arguments.length) return showControls; + showControls = _; + return chart; + }; + + chart.showLegend = function(_) { + if (!arguments.length) return showLegend; + showLegend = _; + return chart; + }; + + chart.showXAxis = function(_) { + if (!arguments.length) return showXAxis; + showXAxis = _; + return chart; + }; + + chart.showYAxis = function(_) { + if (!arguments.length) return showYAxis; + showYAxis = _; + return chart; + }; + + chart.rightAlignYAxis = function(_) { + if(!arguments.length) return rightAlignYAxis; + rightAlignYAxis = _; + yAxis.orient( (_) ? 'right' : 'left'); + return chart; + }; + + + chart.fisheye = function(_) { + if (!arguments.length) return fisheye; + fisheye = _; + return chart; + }; + + chart.xPadding = function(_) { + if (!arguments.length) return xPadding; + xPadding = _; + return chart; + }; + + chart.yPadding = function(_) { + if (!arguments.length) return yPadding; + yPadding = _; + return chart; + }; + + chart.tooltips = function(_) { + if (!arguments.length) return tooltips; + tooltips = _; + return chart; + }; + + chart.tooltipContent = function(_) { + if (!arguments.length) return tooltip; + tooltip = _; + return chart; + }; + + chart.tooltipXContent = function(_) { + if (!arguments.length) return tooltipX; + tooltipX = _; + return chart; + }; + + chart.tooltipYContent = function(_) { + if (!arguments.length) return tooltipY; + tooltipY = _; + return chart; + }; + + chart.state = function(_) { + if (!arguments.length) return state; + state = _; + return chart; + }; + + chart.defaultState = function(_) { + if (!arguments.length) return defaultState; + defaultState = _; + return chart; + }; + + chart.noData = function(_) { + if (!arguments.length) return noData; + noData = _; + return chart; + }; + + chart.transitionDuration = function(_) { + if (!arguments.length) return transitionDuration; + transitionDuration = _; + return chart; + }; + + //============================================================ + + + return chart; +} + +nv.models.scatterPlusLineChart = 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() + , controls = nv.models.legend() + , distX = nv.models.distribution() + , distY = nv.models.distribution() + ; + + var margin = {top: 30, right: 20, bottom: 50, left: 75} + , width = null + , height = null + , color = nv.utils.defaultColor() + , x = d3.fisheye ? d3.fisheye.scale(d3.scale.linear).distortion(0) : scatter.xScale() + , y = d3.fisheye ? d3.fisheye.scale(d3.scale.linear).distortion(0) : scatter.yScale() + , showDistX = false + , showDistY = false + , showLegend = true + , showXAxis = true + , showYAxis = true + , rightAlignYAxis = false + , showControls = !!d3.fisheye + , fisheye = 0 + , pauseFisheye = false + , tooltips = true + , tooltipX = function(key, x, y) { return '' + x + '' } + , tooltipY = function(key, x, y) { return '' + y + '' } + , tooltip = function(key, x, y, date) { return '

' + key + '

' + + '

' + date + '

' } + , state = {} + , defaultState = null + , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'stateChange', 'changeState') + , noData = "No Data Available." + , transitionDuration = 250 + ; + + scatter + .xScale(x) + .yScale(y) + ; + xAxis + .orient('bottom') + .tickPadding(10) + ; + yAxis + .orient((rightAlignYAxis) ? 'right' : 'left') + .tickPadding(10) + ; + distX + .axis('x') + ; + distY + .axis('y') + ; + + controls.updateState(false); + //============================================================ + + + //============================================================ + // Private Variables + //------------------------------------------------------------ + + var x0, y0; + + var showTooltip = function(e, offsetElement) { + //TODO: make tooltip style an option between single or dual on axes (maybe on all charts with axes?) + + var left = e.pos[0] + ( offsetElement.offsetLeft || 0 ), + top = e.pos[1] + ( offsetElement.offsetTop || 0), + leftX = e.pos[0] + ( offsetElement.offsetLeft || 0 ), + topX = y.range()[0] + margin.top + ( offsetElement.offsetTop || 0), + leftY = x.range()[0] + margin.left + ( offsetElement.offsetLeft || 0 ), + topY = e.pos[1] + ( offsetElement.offsetTop || 0), + xVal = xAxis.tickFormat()(scatter.x()(e.point, e.pointIndex)), + yVal = yAxis.tickFormat()(scatter.y()(e.point, e.pointIndex)); + + if( tooltipX != null ) + nv.tooltip.show([leftX, topX], tooltipX(e.series.key, xVal, yVal, e, chart), 'n', 1, offsetElement, 'x-nvtooltip'); + if( tooltipY != null ) + nv.tooltip.show([leftY, topY], tooltipY(e.series.key, xVal, yVal, e, chart), 'e', 1, offsetElement, 'y-nvtooltip'); + if( tooltip != null ) + nv.tooltip.show([left, top], tooltip(e.series.key, xVal, yVal, e.point.tooltip, e, chart), e.value < 0 ? 'n' : 's', null, offsetElement); + }; + + var controlsData = [ + { key: 'Magnify', disabled: true } + ]; + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + var container = d3.select(this), + that = this; + + 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() { 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) { + 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 = 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-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'); + gEnter.append('g').attr('class', 'nv-controlsWrap'); + + wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + + if (rightAlignYAxis) { + g.select(".nv-y.nv-axis") + .attr("transform", "translate(" + availableWidth + ",0)"); + } + + //------------------------------------------------------------ + + + //------------------------------------------------------------ + // Legend + + if (showLegend) { + legend.width( availableWidth / 2 ); + + wrap.select('.nv-legendWrap') + .datum(data) + .call(legend); + + if ( margin.top != legend.height()) { + margin.top = legend.height(); + availableHeight = (height || parseInt(container.style('height')) || 400) + - margin.top - margin.bottom; + } + + wrap.select('.nv-legendWrap') + .attr('transform', 'translate(' + (availableWidth / 2) + ',' + (-margin.top) +')'); + } + + //------------------------------------------------------------ + + + //------------------------------------------------------------ + // Controls + + if (showControls) { + controls.width(180).color(['#444']); + g.select('.nv-controlsWrap') + .datum(controlsData) + .attr('transform', 'translate(0,' + (-margin.top) +')') + .call(controls); + } + + //------------------------------------------------------------ + + + //------------------------------------------------------------ + // Main Chart Component(s) + + scatter + .width(availableWidth) + .height(availableHeight) + .color(data.map(function(d,i) { + return d.color || color(d, i); + }).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]}); + var regLineEnter = regLine.enter() + .append('line').attr('class', 'nv-regLine') + .style('stroke-opacity', 0); + + regLine + .transition() + .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( xAxis.ticks() ? xAxis.ticks() : availableWidth / 100 ) + .tickSize( -availableHeight , 0); + + g.select('.nv-x.nv-axis') + .attr('transform', 'translate(0,' + y.range()[0] + ')') + .call(xAxis); + } + + if (showYAxis) { + yAxis + .scale(y) + .ticks( yAxis.ticks() ? yAxis.ticks() : availableHeight / 36 ) + .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); + } + + //------------------------------------------------------------ + + + + + if (d3.fisheye) { + g.select('.nv-background') + .attr('width', availableWidth) + .attr('height', availableHeight) + ; + + g.select('.nv-background').on('mousemove', updateFisheye); + g.select('.nv-background').on('click', function() { pauseFisheye = !pauseFisheye;}); + scatter.dispatch.on('elementClick.freezeFisheye', function() { + pauseFisheye = !pauseFisheye; + }); + } + + + function updateFisheye() { + if (pauseFisheye) { + g.select('.nv-point-paths').style('pointer-events', 'all'); + return false; + } + + g.select('.nv-point-paths').style('pointer-events', 'none' ); + + var mouse = d3.mouse(this); + x.distortion(fisheye).focus(mouse[0]); + y.distortion(fisheye).focus(mouse[1]); + + g.select('.nv-scatterWrap') + .datum(data.filter(function(d) { return !d.disabled })) + .call(scatter); + + if (showXAxis) + g.select('.nv-x.nv-axis').call(xAxis); + + if (showYAxis) + g.select('.nv-y.nv-axis').call(yAxis); + + g.select('.nv-distributionX') + .datum(data.filter(function(d) { return !d.disabled })) + .call(distX); + g.select('.nv-distributionY') + .datum(data.filter(function(d) { return !d.disabled })) + .call(distY); + } + + + + //============================================================ + // Event Handling/Dispatching (in chart's scope) + //------------------------------------------------------------ + + controls.dispatch.on('legendClick', function(d,i) { + d.disabled = !d.disabled; + + fisheye = d.disabled ? 0 : 2.5; + g.select('.nv-background') .style('pointer-events', d.disabled ? 'none' : 'all'); + g.select('.nv-point-paths').style('pointer-events', d.disabled ? 'all' : 'none' ); + + if (d.disabled) { + x.distortion(fisheye).focus(0); + y.distortion(fisheye).focus(0); + + g.select('.nv-scatterWrap').call(scatter); + g.select('.nv-x.nv-axis').call(xAxis); + g.select('.nv-y.nv-axis').call(yAxis); + } else { + pauseFisheye = false; + } + + chart.update(); + }); + + legend.dispatch.on('stateChange', function(newState) { + state = newState; + dispatch.stateChange(state); + chart.update(); + }); + + + scatter.dispatch.on('elementMouseover.tooltip', function(e) { + d3.select('.nv-chart-' + scatter.id() + ' .nv-series-' + e.seriesIndex + ' .nv-distx-' + e.pointIndex) + .attr('y1', e.pos[1] - availableHeight); + d3.select('.nv-chart-' + scatter.id() + ' .nv-series-' + e.seriesIndex + ' .nv-disty-' + e.pointIndex) + .attr('x2', e.pos[0] + distX.size()); + + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top]; + dispatch.tooltipShow(e); + }); + + dispatch.on('tooltipShow', function(e) { + if (tooltips) showTooltip(e, that.parentNode); + }); + + // 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(); + }); + + //============================================================ + + + //store old scales for use in transitions on update + x0 = x.copy(); + y0 = y.copy(); + + + }); + + return chart; + } + + + //============================================================ + // Event Handling/Dispatching (out of chart's scope) + //------------------------------------------------------------ + + scatter.dispatch.on('elementMouseout.tooltip', function(e) { + dispatch.tooltipHide(e); + + d3.select('.nv-chart-' + scatter.id() + ' .nv-series-' + e.seriesIndex + ' .nv-distx-' + e.pointIndex) + .attr('y1', 0); + d3.select('.nv-chart-' + scatter.id() + ' .nv-series-' + e.seriesIndex + ' .nv-disty-' + e.pointIndex) + .attr('x2', distY.size()); + }); + dispatch.on('tooltipHide', function() { + if (tooltips) nv.tooltip.cleanup(); + }); + + //============================================================ + + + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + + // expose chart's sub-components + chart.dispatch = dispatch; + chart.scatter = scatter; + chart.legend = legend; + chart.controls = controls; + chart.xAxis = xAxis; + chart.yAxis = yAxis; + chart.distX = distX; + chart.distY = distY; + + d3.rebind(chart, scatter, 'id', 'interactive', 'pointActive', 'x', 'y', 'shape', 'size', 'xScale', 'yScale', 'zScale', 'xDomain', 'yDomain', 'xRange', 'yRange', 'sizeDomain', 'sizeRange', 'forceX', 'forceY', 'forceSize', 'clipVoronoi', 'clipRadius', 'useVoronoi'); + + chart.options = nv.utils.optionsFunc.bind(chart); + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + legend.color(color); + distX.color(color); + distY.color(color); + return chart; + }; + + chart.showDistX = function(_) { + if (!arguments.length) return showDistX; + showDistX = _; + return chart; + }; + + chart.showDistY = function(_) { + if (!arguments.length) return showDistY; + showDistY = _; + return chart; + }; + + chart.showControls = function(_) { + if (!arguments.length) return showControls; + showControls = _; + return chart; + }; + + chart.showLegend = function(_) { + if (!arguments.length) return showLegend; + showLegend = _; + return chart; + }; + + chart.showXAxis = function(_) { + if (!arguments.length) return showXAxis; + showXAxis = _; + return chart; + }; + + chart.showYAxis = function(_) { + if (!arguments.length) return showYAxis; + showYAxis = _; + return chart; + }; + + chart.rightAlignYAxis = function(_) { + if(!arguments.length) return rightAlignYAxis; + rightAlignYAxis = _; + yAxis.orient( (_) ? 'right' : 'left'); + return chart; + }; + + chart.fisheye = function(_) { + if (!arguments.length) return fisheye; + fisheye = _; + return chart; + }; + + chart.tooltips = function(_) { + if (!arguments.length) return tooltips; + tooltips = _; + return chart; + }; + + chart.tooltipContent = function(_) { + if (!arguments.length) return tooltip; + tooltip = _; + return chart; + }; + + chart.tooltipXContent = function(_) { + if (!arguments.length) return tooltipX; + tooltipX = _; + return chart; + }; + + chart.tooltipYContent = function(_) { + if (!arguments.length) return tooltipY; + tooltipY = _; + return chart; + }; + + chart.state = function(_) { + if (!arguments.length) return state; + state = _; + return chart; + }; + + chart.defaultState = function(_) { + if (!arguments.length) return defaultState; + defaultState = _; + return chart; + }; + + chart.noData = function(_) { + if (!arguments.length) return noData; + noData = _; + return chart; + }; + + chart.transitionDuration = function(_) { + if (!arguments.length) return transitionDuration; + transitionDuration = _; + return 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 + , 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); + + + //------------------------------------------------------------ + // 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.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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.x = function(_) { + if (!arguments.length) return getX; + getX = d3.functor(_); + return chart; + }; + + chart.y = function(_) { + if (!arguments.length) return getY; + getY = d3.functor(_); + return chart; + }; + + chart.xScale = function(_) { + if (!arguments.length) return x; + x = _; + return chart; + }; + + chart.yScale = function(_) { + if (!arguments.length) return y; + y = _; + return chart; + }; + + chart.xDomain = function(_) { + if (!arguments.length) return xDomain; + xDomain = _; + return chart; + }; + + chart.yDomain = function(_) { + if (!arguments.length) return yDomain; + yDomain = _; + return chart; + }; + + chart.xRange = function(_) { + if (!arguments.length) return xRange; + xRange = _; + return chart; + }; + + chart.yRange = function(_) { + if (!arguments.length) return yRange; + yRange = _; + return chart; + }; + + chart.animate = function(_) { + if (!arguments.length) return animate; + animate = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + return 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') + , showValue = true + , alignValue = true + , rightAlignValue = false + , noData = "No Data Available." + ; + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + var container = d3.select(this); + + 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() { chart(selection) }; + chart.container = this; + + + //------------------------------------------------------------ + // Display No Data message if there's nothing to show. + + if (!data || !data.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(); + } + + 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); + + //------------------------------------------------------------ + + + 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(); }); + //.on('mouseout', function() { index = null; 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); + + + + function updateValueLine() { //index is currently global (within the chart), may or may not keep it that way + 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; + + d3.rebind(chart, sparkline, 'x', 'y', 'xScale', 'yScale', 'color'); + + chart.options = nv.utils.optionsFunc.bind(chart); + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.xTickFormat = function(_) { + if (!arguments.length) return xTickFormat; + xTickFormat = _; + return chart; + }; + + chart.yTickFormat = function(_) { + if (!arguments.length) return yTickFormat; + yTickFormat = _; + return chart; + }; + + chart.showValue = function(_) { + if (!arguments.length) return showValue; + showValue = _; + return chart; + }; + + chart.alignValue = function(_) { + if (!arguments.length) return alignValue; + alignValue = _; + return chart; + }; + + chart.rightAlignValue = function(_) { + if (!arguments.length) return rightAlignValue; + rightAlignValue = _; + return chart; + }; + + chart.noData = function(_) { + if (!arguments.length) return noData; + noData = _; + return 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 + , 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() + , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'areaClick', 'areaMouseover', 'areaMouseout') + ; + + scatter + .size(2.2) // default size + .sizeDomain([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) + ************************************/ + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + var availableWidth = width - margin.left - margin.right, + availableHeight = height - margin.top - margin.bottom, + container = d3.select(this); + + //------------------------------------------------------------ + // Setup Scales + + x = scatter.xScale(); + y = scatter.yScale(); + + //------------------------------------------------------------ + + + // Injecting point index into each point because d3.layout.stack().out does not give index + data = data.map(function(aseries, i) { + aseries.seriesIndex = i; + aseries.values = aseries.values.map(function(d, j) { + d.index = j; + d.seriesIndex = i; + return d; + }) + return aseries; + }); + + 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) { + var yHeight = (getY(d) === 0) ? 0 : y; + d.display = { + y: yHeight, + 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 + ')'); + + //------------------------------------------------------------ + + + 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: i + }); + }) + .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: i + }); + }) + .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: i + }); + }) + path.exit().transition() + .attr('d', function(d,i) { return zeroArea(d.values,i) }) + .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.transition() + .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); + }); + + //============================================================ + + }); + + + return chart; + } + + + //============================================================ + // Event Handling/Dispatching (out of chart's scope) + //------------------------------------------------------------ + + scatter.dispatch.on('elementClick.area', function(e) { + dispatch.areaClick(e); + }) + scatter.dispatch.on('elementMouseover.tooltip', function(e) { + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top], + dispatch.tooltipShow(e); + }); + scatter.dispatch.on('elementMouseout.tooltip', function(e) { + dispatch.tooltipHide(e); + }); + + //============================================================ + + + //============================================================ + // Global getters and setters + //------------------------------------------------------------ + + chart.dispatch = dispatch; + chart.scatter = scatter; + + d3.rebind(chart, scatter, 'interactive', 'size', 'xScale', 'yScale', 'zScale', 'xDomain', 'yDomain', 'xRange', 'yRange', + 'sizeDomain', 'forceX', 'forceY', 'forceSize', 'clipVoronoi', 'useVoronoi','clipRadius','highlightPoint','clearHighlights'); + + chart.options = nv.utils.optionsFunc.bind(chart); + + chart.x = function(_) { + if (!arguments.length) return getX; + getX = d3.functor(_); + return chart; + }; + + chart.y = function(_) { + if (!arguments.length) return getY; + getY = d3.functor(_); + return chart; + } + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.clipEdge = function(_) { + if (!arguments.length) return clipEdge; + clipEdge = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + return chart; + }; + + chart.offset = function(_) { + if (!arguments.length) return offset; + offset = _; + return chart; + }; + + chart.order = function(_) { + if (!arguments.length) return order; + order = _; + return chart; + }; + + //shortcut for offset + order + chart.style = function(_) { + if (!arguments.length) return style; + 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; + } + + return chart; + }; + + chart.interpolate = function(_) { + if (!arguments.length) return interpolate; + interpolate = _; + return 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() + ; + + var margin = {top: 30, right: 25, bottom: 50, left: 60} + , width = null + , height = null + , color = nv.utils.defaultColor() // a function that takes in d, i and returns color + , showControls = true + , showLegend = true + , showXAxis = true + , showYAxis = true + , rightAlignYAxis = false + , useInteractiveGuideline = false + , tooltips = true + , tooltip = function(key, x, y, e, graph) { + return '

' + key + '

' + + '

' + y + ' on ' + x + '

' + } + , x //can be accessed via chart.xScale() + , y //can be accessed via chart.yScale() + , yAxisTickFormat = d3.format(',.2f') + , state = { style: stacked.style() } + , defaultState = null + , noData = 'No Data Available.' + , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'stateChange', 'changeState') + , controlWidth = 250 + , cData = ['Stacked','Stream','Expanded'] + , transitionDuration = 250 + ; + + xAxis + .orient('bottom') + .tickPadding(7) + ; + yAxis + .orient((rightAlignYAxis) ? 'right' : 'left') + ; + + controls.updateState(false); + //============================================================ + + + //============================================================ + // Private Variables + //------------------------------------------------------------ + + var showTooltip = function(e, offsetElement) { + var left = e.pos[0] + ( offsetElement.offsetLeft || 0 ), + top = e.pos[1] + ( offsetElement.offsetTop || 0), + x = xAxis.tickFormat()(stacked.x()(e.point, e.pointIndex)), + y = yAxis.tickFormat()(stacked.y()(e.point, e.pointIndex)), + content = tooltip(e.series.key, x, y, e, chart); + + nv.tooltip.show([left, top], content, e.value < 0 ? 'n' : 's', null, offsetElement); + }; + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + var container = d3.select(this), + that = this; + + 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() { 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 No Data message if there's nothing to show. + + if (!data || !data.length || !data.filter(function(d) { return d.values.length }).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 = 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 = (height || parseInt(container.style('height')) || 400) + - margin.top - margin.bottom; + } + + g.select('.nv-legendWrap') + .attr('transform', 'translate(' + (availableWidth-legendWidth) + ',' + (-margin.top) +')'); + } + + //------------------------------------------------------------ + + + //------------------------------------------------------------ + // Controls + + if (showControls) { + var controlsData = [ + { key: 'Stacked', disabled: stacked.offset() != 'zero' }, + { key: 'Stream', disabled: stacked.offset() != 'wiggle' }, + { key: 'Expanded', disabled: stacked.offset() != 'expand' } + ]; + + controlWidth = (cData.length/3) * 260; + + controlsData = controlsData.filter(function(d) { + return cData.indexOf(d.key) > -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 = (height || parseInt(container.style('height')) || 400) + - margin.top - margin.bottom; + } + + + 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)"); + } + + //------------------------------------------------------------ + // Main Chart Component(s) + + //------------------------------------------------------------ + //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( availableWidth / 100 ) + .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) { + yAxis + .scale(y) + .ticks(stacked.offset() == 'wiggle' ? 0 : availableHeight / 36) + .tickSize(-availableWidth, 0) + .setTickFormat(stacked.offset() == 'expand' ? d3.format('%') : yAxisTickFormat); + + 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 = data.map(function(d) { + d.disabled = false; + return d + }); + else + data = data.map(function(d,i) { + d.disabled = (i != e.seriesIndex); + return d + }); + + state.disabled = data.map(function(d) { return !!d.disabled }); + dispatch.stateChange(state); + + chart.update(); + }); + + legend.dispatch.on('stateChange', function(newState) { + state.disabled = newState.disabled; + 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 'Stacked': + stacked.style('stack'); + break; + case 'Stream': + stacked.style('stream'); + break; + case 'Expanded': + stacked.style('expand'); + break; + } + + 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()); + stacked.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) + }); + }); + + var xValue = xAxis.tickFormat()(chart.x()(singlePoint,pointIndex)); + interactiveLayer.tooltip + .position({left: pointXLocation + margin.left, top: e.mouseY + margin.top}) + .chartContainer(that.parentNode) + .enabled(tooltips) + .valueFormatter(function(d,i) { + return yAxis.tickFormat()(d); + }) + .data( + { + value: xValue, + series: allData + } + )(); + + interactiveLayer.renderGuideLine(pointXLocation); + + }); + + interactiveLayer.dispatch.on("elementMouseout",function(e) { + dispatch.tooltipHide(); + stacked.clearHighlights(); + }); + + + dispatch.on('tooltipShow', function(e) { + if (tooltips) showTooltip(e, that.parentNode); + }); + + // 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.style !== 'undefined') { + stacked.style(e.style); + } + + chart.update(); + }); + + }); + + + return chart; + } + + + //============================================================ + // Event Handling/Dispatching (out of chart's scope) + //------------------------------------------------------------ + + stacked.dispatch.on('tooltipShow', function(e) { + //disable tooltips when value ~= 0 + //// TODO: consider removing points from voronoi that have 0 value instead of this hack + /* + if (!Math.round(stacked.y()(e.point) * 100)) { // 100 will not be good for very small numbers... will have to think about making this valu dynamic, based on data range + setTimeout(function() { d3.selectAll('.point.hover').classed('hover', false) }, 0); + return false; + } + */ + + e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top], + dispatch.tooltipShow(e); + }); + + stacked.dispatch.on('tooltipHide', function(e) { + dispatch.tooltipHide(e); + }); + + dispatch.on('tooltipHide', function() { + if (tooltips) nv.tooltip.cleanup(); + }); + + //============================================================ + + + //============================================================ + // 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; + + d3.rebind(chart, stacked, 'x', 'y', 'size', 'xScale', 'yScale', 'xDomain', 'yDomain', 'xRange', 'yRange', 'sizeDomain', 'interactive', 'useVoronoi', 'offset', 'order', 'style', 'clipEdge', 'forceX', 'forceY', 'forceSize', 'interpolate'); + + chart.options = nv.utils.optionsFunc.bind(chart); + + 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.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + legend.color(color); + stacked.color(color); + return chart; + }; + + chart.showControls = function(_) { + if (!arguments.length) return showControls; + showControls = _; + return chart; + }; + + chart.showLegend = function(_) { + if (!arguments.length) return showLegend; + showLegend = _; + return chart; + }; + + chart.showXAxis = function(_) { + if (!arguments.length) return showXAxis; + showXAxis = _; + return chart; + }; + + chart.showYAxis = function(_) { + if (!arguments.length) return showYAxis; + showYAxis = _; + return chart; + }; + + chart.rightAlignYAxis = function(_) { + if(!arguments.length) return rightAlignYAxis; + rightAlignYAxis = _; + yAxis.orient( (_) ? 'right' : 'left'); + return chart; + }; + + chart.useInteractiveGuideline = function(_) { + if(!arguments.length) return useInteractiveGuideline; + useInteractiveGuideline = _; + if (_ === true) { + chart.interactive(false); + chart.useVoronoi(false); + } + return chart; + }; + + chart.tooltip = function(_) { + if (!arguments.length) return tooltip; + tooltip = _; + return chart; + }; + + chart.tooltips = function(_) { + if (!arguments.length) return tooltips; + tooltips = _; + return chart; + }; + + chart.tooltipContent = function(_) { + if (!arguments.length) return tooltip; + tooltip = _; + return chart; + }; + + chart.state = function(_) { + if (!arguments.length) return state; + state = _; + return chart; + }; + + chart.defaultState = function(_) { + if (!arguments.length) return defaultState; + defaultState = _; + return chart; + }; + + chart.noData = function(_) { + if (!arguments.length) return noData; + noData = _; + return chart; + }; + + chart.transitionDuration = function(_) { + if (!arguments.length) return transitionDuration; + transitionDuration = _; + return chart; + }; + + chart.controlsData = function(_) { + if (!arguments.length) return cData; + cData = _; + return chart; + }; + + yAxis.setTickFormat = yAxis.tickFormat; + + yAxis.tickFormat = function(_) { + if (!arguments.length) return yAxisTickFormat; + yAxisTickFormat = _; + return yAxis; + }; + + + //============================================================ + + return chart; +} +})(); \ No newline at end of file