paulo@89: /* nvd3 version 1.8.1 (https://github.com/novus/nvd3) 2015-06-15 */ paulo@89: (function(){ paulo@89: paulo@89: // set up main nv object paulo@89: var nv = {}; paulo@89: paulo@89: // the major global objects under the nv namespace paulo@89: nv.dev = false; //set false when in production paulo@89: nv.tooltip = nv.tooltip || {}; // For the tooltip system paulo@89: nv.utils = nv.utils || {}; // Utility subsystem paulo@89: nv.models = nv.models || {}; //stores all the possible models/components paulo@89: nv.charts = {}; //stores all the ready to use charts paulo@89: nv.logs = {}; //stores some statistics and potential error messages paulo@89: nv.dom = {}; //DOM manipulation functions paulo@89: paulo@89: nv.dispatch = d3.dispatch('render_start', 'render_end'); paulo@89: paulo@89: // Function bind polyfill paulo@89: // Needed ONLY for phantomJS as it's missing until version 2.0 which is unreleased as of this comment paulo@89: // https://github.com/ariya/phantomjs/issues/10522 paulo@89: // http://kangax.github.io/compat-table/es5/#Function.prototype.bind paulo@89: // phantomJS is used for running the test suite paulo@89: if (!Function.prototype.bind) { paulo@89: Function.prototype.bind = function (oThis) { paulo@89: if (typeof this !== "function") { paulo@89: // closest thing possible to the ECMAScript 5 internal IsCallable function paulo@89: throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); paulo@89: } paulo@89: paulo@89: var aArgs = Array.prototype.slice.call(arguments, 1), paulo@89: fToBind = this, paulo@89: fNOP = function () {}, paulo@89: fBound = function () { paulo@89: return fToBind.apply(this instanceof fNOP && oThis paulo@89: ? this paulo@89: : oThis, paulo@89: aArgs.concat(Array.prototype.slice.call(arguments))); paulo@89: }; paulo@89: paulo@89: fNOP.prototype = this.prototype; paulo@89: fBound.prototype = new fNOP(); paulo@89: return fBound; paulo@89: }; paulo@89: } paulo@89: paulo@89: // Development render timers - disabled if dev = false paulo@89: if (nv.dev) { paulo@89: nv.dispatch.on('render_start', function(e) { paulo@89: nv.logs.startTime = +new Date(); paulo@89: }); paulo@89: paulo@89: nv.dispatch.on('render_end', function(e) { paulo@89: nv.logs.endTime = +new Date(); paulo@89: nv.logs.totalTime = nv.logs.endTime - nv.logs.startTime; paulo@89: nv.log('total', nv.logs.totalTime); // used for development, to keep track of graph generation times paulo@89: }); paulo@89: } paulo@89: paulo@89: // Logs all arguments, and returns the last so you can test things in place paulo@89: // Note: in IE8 console.log is an object not a function, and if modernizr is used paulo@89: // then calling Function.prototype.bind with with anything other than a function paulo@89: // causes a TypeError to be thrown. paulo@89: nv.log = function() { paulo@89: if (nv.dev && window.console && console.log && console.log.apply) paulo@89: console.log.apply(console, arguments); paulo@89: else if (nv.dev && window.console && typeof console.log == "function" && Function.prototype.bind) { paulo@89: var log = Function.prototype.bind.call(console.log, console); paulo@89: log.apply(console, arguments); paulo@89: } paulo@89: return arguments[arguments.length - 1]; paulo@89: }; paulo@89: paulo@89: // print console warning, should be used by deprecated functions paulo@89: nv.deprecated = function(name, info) { paulo@89: if (console && console.warn) { paulo@89: console.warn('nvd3 warning: `' + name + '` has been deprecated. ', info || ''); paulo@89: } paulo@89: }; paulo@89: paulo@89: // The nv.render function is used to queue up chart rendering paulo@89: // in non-blocking async functions. paulo@89: // When all queued charts are done rendering, nv.dispatch.render_end is invoked. paulo@89: nv.render = function render(step) { paulo@89: // number of graphs to generate in each timeout loop paulo@89: step = step || 1; paulo@89: paulo@89: nv.render.active = true; paulo@89: nv.dispatch.render_start(); paulo@89: paulo@89: var renderLoop = function() { paulo@89: var chart, graph; paulo@89: paulo@89: for (var i = 0; i < step && (graph = nv.render.queue[i]); i++) { paulo@89: chart = graph.generate(); paulo@89: if (typeof graph.callback == typeof(Function)) graph.callback(chart); paulo@89: } paulo@89: paulo@89: nv.render.queue.splice(0, i); paulo@89: paulo@89: if (nv.render.queue.length) { paulo@89: setTimeout(renderLoop); paulo@89: } paulo@89: else { paulo@89: nv.dispatch.render_end(); paulo@89: nv.render.active = false; paulo@89: } paulo@89: }; paulo@89: paulo@89: setTimeout(renderLoop); paulo@89: }; paulo@89: paulo@89: nv.render.active = false; paulo@89: nv.render.queue = []; paulo@89: paulo@89: /* paulo@89: Adds a chart to the async rendering queue. This method can take arguments in two forms: paulo@89: nv.addGraph({ paulo@89: generate: paulo@89: callback: paulo@89: }) paulo@89: paulo@89: or paulo@89: paulo@89: nv.addGraph(, ) paulo@89: paulo@89: The generate function should contain code that creates the NVD3 model, sets options paulo@89: on it, adds data to an SVG element, and invokes the chart model. The generate function paulo@89: should return the chart model. See examples/lineChart.html for a usage example. paulo@89: paulo@89: The callback function is optional, and it is called when the generate function completes. paulo@89: */ paulo@89: nv.addGraph = function(obj) { paulo@89: if (typeof arguments[0] === typeof(Function)) { paulo@89: obj = {generate: arguments[0], callback: arguments[1]}; paulo@89: } paulo@89: paulo@89: nv.render.queue.push(obj); paulo@89: paulo@89: if (!nv.render.active) { paulo@89: nv.render(); paulo@89: } paulo@89: }; paulo@89: paulo@89: // Node/CommonJS exports paulo@89: if (typeof(module) !== 'undefined' && typeof(exports) !== 'undefined') { paulo@89: module.exports = nv; paulo@89: } paulo@89: paulo@89: if (typeof(window) !== 'undefined') { paulo@89: window.nv = nv; paulo@89: } paulo@89: /* Facade for queueing DOM write operations paulo@89: * with Fastdom (https://github.com/wilsonpage/fastdom) paulo@89: * if available. paulo@89: * This could easily be extended to support alternate paulo@89: * implementations in the future. paulo@89: */ paulo@89: nv.dom.write = function(callback) { paulo@89: if (window.fastdom !== undefined) { paulo@89: return fastdom.write(callback); paulo@89: } paulo@89: return callback(); paulo@89: }; paulo@89: paulo@89: /* Facade for queueing DOM read operations paulo@89: * with Fastdom (https://github.com/wilsonpage/fastdom) paulo@89: * if available. paulo@89: * This could easily be extended to support alternate paulo@89: * implementations in the future. paulo@89: */ paulo@89: nv.dom.read = function(callback) { paulo@89: if (window.fastdom !== undefined) { paulo@89: return fastdom.read(callback); paulo@89: } paulo@89: return callback(); paulo@89: };/* Utility class to handle creation of an interactive layer. paulo@89: This places a rectangle on top of the chart. When you mouse move over it, it sends a dispatch paulo@89: containing the X-coordinate. It can also render a vertical line where the mouse is located. paulo@89: paulo@89: dispatch.elementMousemove is the important event to latch onto. It is fired whenever the mouse moves over paulo@89: the rectangle. The dispatch is given one object which contains the mouseX/Y location. paulo@89: It also has 'pointXValue', which is the conversion of mouseX to the x-axis scale. paulo@89: */ paulo@89: nv.interactiveGuideline = function() { paulo@89: "use strict"; paulo@89: paulo@89: var tooltip = nv.models.tooltip(); paulo@89: tooltip.duration(0).hideDelay(0)._isInteractiveLayer(true).hidden(false); paulo@89: paulo@89: //Public settings paulo@89: var width = null; paulo@89: var height = null; paulo@89: paulo@89: //Please pass in the bounding chart's top and left margins paulo@89: //This is important for calculating the correct mouseX/Y positions. paulo@89: var margin = {left: 0, top: 0} paulo@89: , xScale = d3.scale.linear() paulo@89: , dispatch = d3.dispatch('elementMousemove', 'elementMouseout', 'elementClick', 'elementDblclick') paulo@89: , showGuideLine = true; paulo@89: //Must pass in the bounding chart's container. paulo@89: //The mousemove event is attached to this container. paulo@89: var svgContainer = null; paulo@89: paulo@89: // check if IE by looking for activeX paulo@89: var isMSIE = "ActiveXObject" in window; paulo@89: paulo@89: paulo@89: function layer(selection) { paulo@89: selection.each(function(data) { paulo@89: var container = d3.select(this); paulo@89: var availableWidth = (width || 960), availableHeight = (height || 400); paulo@89: var wrap = container.selectAll("g.nv-wrap.nv-interactiveLineLayer") paulo@89: .data([data]); paulo@89: var wrapEnter = wrap.enter() paulo@89: .append("g").attr("class", " nv-wrap nv-interactiveLineLayer"); paulo@89: wrapEnter.append("g").attr("class","nv-interactiveGuideLine"); paulo@89: paulo@89: if (!svgContainer) { paulo@89: return; paulo@89: } paulo@89: paulo@89: function mouseHandler() { paulo@89: var d3mouse = d3.mouse(this); paulo@89: var mouseX = d3mouse[0]; paulo@89: var mouseY = d3mouse[1]; paulo@89: var subtractMargin = true; paulo@89: var mouseOutAnyReason = false; paulo@89: if (isMSIE) { paulo@89: /* paulo@89: D3.js (or maybe SVG.getScreenCTM) has a nasty bug in Internet Explorer 10. paulo@89: d3.mouse() returns incorrect X,Y mouse coordinates when mouse moving paulo@89: over a rect in IE 10. paulo@89: However, d3.event.offsetX/Y also returns the mouse coordinates paulo@89: relative to the triggering . So we use offsetX/Y on IE. paulo@89: */ paulo@89: mouseX = d3.event.offsetX; paulo@89: mouseY = d3.event.offsetY; paulo@89: paulo@89: /* paulo@89: On IE, if you attach a mouse event listener to the container, paulo@89: it will actually trigger it for all the child elements (like , , etc). paulo@89: When this happens on IE, the offsetX/Y is set to where ever the child element paulo@89: is located. paulo@89: As a result, we do NOT need to subtract margins to figure out the mouse X/Y paulo@89: position under this scenario. Removing the line below *will* cause paulo@89: the interactive layer to not work right on IE. paulo@89: */ paulo@89: if(d3.event.target.tagName !== "svg") { paulo@89: subtractMargin = false; paulo@89: } paulo@89: paulo@89: if (d3.event.target.className.baseVal.match("nv-legend")) { paulo@89: mouseOutAnyReason = true; paulo@89: } paulo@89: paulo@89: } paulo@89: paulo@89: if(subtractMargin) { paulo@89: mouseX -= margin.left; paulo@89: mouseY -= margin.top; paulo@89: } paulo@89: paulo@89: /* If mouseX/Y is outside of the chart's bounds, paulo@89: trigger a mouseOut event. paulo@89: */ paulo@89: if (mouseX < 0 || mouseY < 0 paulo@89: || mouseX > availableWidth || mouseY > availableHeight paulo@89: || (d3.event.relatedTarget && d3.event.relatedTarget.ownerSVGElement === undefined) paulo@89: || mouseOutAnyReason paulo@89: ) { paulo@89: paulo@89: if (isMSIE) { paulo@89: if (d3.event.relatedTarget paulo@89: && d3.event.relatedTarget.ownerSVGElement === undefined paulo@89: && (d3.event.relatedTarget.className === undefined paulo@89: || d3.event.relatedTarget.className.match(tooltip.nvPointerEventsClass))) { paulo@89: paulo@89: return; paulo@89: } paulo@89: } paulo@89: dispatch.elementMouseout({ paulo@89: mouseX: mouseX, paulo@89: mouseY: mouseY paulo@89: }); paulo@89: layer.renderGuideLine(null); //hide the guideline paulo@89: tooltip.hidden(true); paulo@89: return; paulo@89: } else { paulo@89: tooltip.hidden(false); paulo@89: } paulo@89: paulo@89: var pointXValue = xScale.invert(mouseX); paulo@89: dispatch.elementMousemove({ paulo@89: mouseX: mouseX, paulo@89: mouseY: mouseY, paulo@89: pointXValue: pointXValue paulo@89: }); paulo@89: paulo@89: //If user double clicks the layer, fire a elementDblclick paulo@89: if (d3.event.type === "dblclick") { paulo@89: dispatch.elementDblclick({ paulo@89: mouseX: mouseX, paulo@89: mouseY: mouseY, paulo@89: pointXValue: pointXValue paulo@89: }); paulo@89: } paulo@89: paulo@89: // if user single clicks the layer, fire elementClick paulo@89: if (d3.event.type === 'click') { paulo@89: dispatch.elementClick({ paulo@89: mouseX: mouseX, paulo@89: mouseY: mouseY, paulo@89: pointXValue: pointXValue paulo@89: }); paulo@89: } paulo@89: } paulo@89: paulo@89: svgContainer paulo@89: .on("touchmove",mouseHandler) paulo@89: .on("mousemove",mouseHandler, true) paulo@89: .on("mouseout" ,mouseHandler,true) paulo@89: .on("dblclick" ,mouseHandler) paulo@89: .on("click", mouseHandler) paulo@89: ; paulo@89: paulo@89: layer.guideLine = null; paulo@89: //Draws a vertical guideline at the given X postion. paulo@89: layer.renderGuideLine = function(x) { paulo@89: if (!showGuideLine) return; paulo@89: if (layer.guideLine && layer.guideLine.attr("x1") === x) return; paulo@89: nv.dom.write(function() { paulo@89: var line = wrap.select(".nv-interactiveGuideLine") paulo@89: .selectAll("line") paulo@89: .data((x != null) ? [nv.utils.NaNtoZero(x)] : [], String); paulo@89: line.enter() paulo@89: .append("line") paulo@89: .attr("class", "nv-guideline") paulo@89: .attr("x1", function(d) { return d;}) paulo@89: .attr("x2", function(d) { return d;}) paulo@89: .attr("y1", availableHeight) paulo@89: .attr("y2",0); paulo@89: line.exit().remove(); paulo@89: }); paulo@89: } paulo@89: }); paulo@89: } paulo@89: paulo@89: layer.dispatch = dispatch; paulo@89: layer.tooltip = tooltip; paulo@89: paulo@89: layer.margin = function(_) { paulo@89: if (!arguments.length) return margin; paulo@89: margin.top = typeof _.top != 'undefined' ? _.top : margin.top; paulo@89: margin.left = typeof _.left != 'undefined' ? _.left : margin.left; paulo@89: return layer; paulo@89: }; paulo@89: paulo@89: layer.width = function(_) { paulo@89: if (!arguments.length) return width; paulo@89: width = _; paulo@89: return layer; paulo@89: }; paulo@89: paulo@89: layer.height = function(_) { paulo@89: if (!arguments.length) return height; paulo@89: height = _; paulo@89: return layer; paulo@89: }; paulo@89: paulo@89: layer.xScale = function(_) { paulo@89: if (!arguments.length) return xScale; paulo@89: xScale = _; paulo@89: return layer; paulo@89: }; paulo@89: paulo@89: layer.showGuideLine = function(_) { paulo@89: if (!arguments.length) return showGuideLine; paulo@89: showGuideLine = _; paulo@89: return layer; paulo@89: }; paulo@89: paulo@89: layer.svgContainer = function(_) { paulo@89: if (!arguments.length) return svgContainer; paulo@89: svgContainer = _; paulo@89: return layer; paulo@89: }; paulo@89: paulo@89: return layer; paulo@89: }; paulo@89: paulo@89: /* Utility class that uses d3.bisect to find the index in a given array, where a search value can be inserted. paulo@89: This is different from normal bisectLeft; this function finds the nearest index to insert the search value. paulo@89: paulo@89: For instance, lets say your array is [1,2,3,5,10,30], and you search for 28. paulo@89: Normal d3.bisectLeft will return 4, because 28 is inserted after the number 10. But interactiveBisect will return 5 paulo@89: because 28 is closer to 30 than 10. paulo@89: paulo@89: Unit tests can be found in: interactiveBisectTest.html paulo@89: paulo@89: Has the following known issues: paulo@89: * Will not work if the data points move backwards (ie, 10,9,8,7, etc) or if the data points are in random order. paulo@89: * Won't work if there are duplicate x coordinate values. paulo@89: */ paulo@89: nv.interactiveBisect = function (values, searchVal, xAccessor) { paulo@89: "use strict"; paulo@89: if (! (values instanceof Array)) { paulo@89: return null; paulo@89: } paulo@89: var _xAccessor; paulo@89: if (typeof xAccessor !== 'function') { paulo@89: _xAccessor = function(d) { paulo@89: return d.x; paulo@89: } paulo@89: } else { paulo@89: _xAccessor = xAccessor; paulo@89: } paulo@89: var _cmp = function(d, v) { paulo@89: // Accessors are no longer passed the index of the element along with paulo@89: // the element itself when invoked by d3.bisector. paulo@89: // paulo@89: // Starting at D3 v3.4.4, d3.bisector() started inspecting the paulo@89: // function passed to determine if it should consider it an accessor paulo@89: // or a comparator. This meant that accessors that take two arguments paulo@89: // (expecting an index as the second parameter) are treated as paulo@89: // comparators where the second argument is the search value against paulo@89: // which the first argument is compared. paulo@89: return _xAccessor(d) - v; paulo@89: }; paulo@89: paulo@89: var bisect = d3.bisector(_cmp).left; paulo@89: var index = d3.max([0, bisect(values,searchVal) - 1]); paulo@89: var currentValue = _xAccessor(values[index]); paulo@89: paulo@89: if (typeof currentValue === 'undefined') { paulo@89: currentValue = index; paulo@89: } paulo@89: paulo@89: if (currentValue === searchVal) { paulo@89: return index; //found exact match paulo@89: } paulo@89: paulo@89: var nextIndex = d3.min([index+1, values.length - 1]); paulo@89: var nextValue = _xAccessor(values[nextIndex]); paulo@89: paulo@89: if (typeof nextValue === 'undefined') { paulo@89: nextValue = nextIndex; paulo@89: } paulo@89: paulo@89: if (Math.abs(nextValue - searchVal) >= Math.abs(currentValue - searchVal)) { paulo@89: return index; paulo@89: } else { paulo@89: return nextIndex paulo@89: } paulo@89: }; paulo@89: paulo@89: /* paulo@89: Returns the index in the array "values" that is closest to searchVal. paulo@89: Only returns an index if searchVal is within some "threshold". paulo@89: Otherwise, returns null. paulo@89: */ paulo@89: nv.nearestValueIndex = function (values, searchVal, threshold) { paulo@89: "use strict"; paulo@89: var yDistMax = Infinity, indexToHighlight = null; paulo@89: values.forEach(function(d,i) { paulo@89: var delta = Math.abs(searchVal - d); paulo@89: if ( d != null && delta <= yDistMax && delta < threshold) { paulo@89: yDistMax = delta; paulo@89: indexToHighlight = i; paulo@89: } paulo@89: }); paulo@89: return indexToHighlight; paulo@89: }; paulo@89: /* Tooltip rendering model for nvd3 charts. paulo@89: window.nv.models.tooltip is the updated,new way to render tooltips. paulo@89: paulo@89: window.nv.tooltip.show is the old tooltip code. paulo@89: window.nv.tooltip.* also has various helper methods. paulo@89: */ paulo@89: (function() { paulo@89: "use strict"; paulo@89: paulo@89: /* Model which can be instantiated to handle tooltip rendering. paulo@89: Example usage: paulo@89: var tip = nv.models.tooltip().gravity('w').distance(23) paulo@89: .data(myDataObject); paulo@89: paulo@89: tip(); //just invoke the returned function to render tooltip. paulo@89: */ paulo@89: nv.models.tooltip = function() { paulo@89: paulo@89: /* paulo@89: Tooltip data. If data is given in the proper format, a consistent tooltip is generated. paulo@89: Example Format of data: paulo@89: { paulo@89: key: "Date", paulo@89: value: "August 2009", paulo@89: series: [ paulo@89: {key: "Series 1", value: "Value 1", color: "#000"}, paulo@89: {key: "Series 2", value: "Value 2", color: "#00f"} paulo@89: ] paulo@89: } paulo@89: */ paulo@89: var data = null; paulo@89: var gravity = 'w' //Can be 'n','s','e','w'. Determines how tooltip is positioned. paulo@89: , distance = 25 //Distance to offset tooltip from the mouse location. paulo@89: , snapDistance = 0 //Tolerance allowed before tooltip is moved from its current position (creates 'snapping' effect) paulo@89: , fixedTop = null //If not null, this fixes the top position of the tooltip. paulo@89: , classes = null //Attaches additional CSS classes to the tooltip DIV that is created. paulo@89: , chartContainer = null //Parent dom element of the SVG that holds the chart. paulo@89: , hidden = true // start off hidden, toggle with hide/show functions below paulo@89: , hideDelay = 400 // delay before the tooltip hides after calling hide() paulo@89: , tooltip = null // d3 select of tooltipElem below paulo@89: , tooltipElem = null //actual DOM element representing the tooltip. paulo@89: , position = {left: null, top: null} //Relative position of the tooltip inside chartContainer. paulo@89: , offset = {left: 0, top: 0} //Offset of tooltip against the pointer paulo@89: , enabled = true //True -> tooltips are rendered. False -> don't render tooltips. paulo@89: , duration = 100 // duration for tooltip movement paulo@89: , headerEnabled = true paulo@89: ; paulo@89: paulo@89: // set to true by interactive layer to adjust tooltip positions paulo@89: // eventually we should probably fix interactive layer to get the position better. paulo@89: // for now this is needed if you want to set chartContainer for normal tooltips, else it "fixes" it to broken paulo@89: var isInteractiveLayer = false; paulo@89: paulo@89: //Generates a unique id when you create a new tooltip() object paulo@89: var id = "nvtooltip-" + Math.floor(Math.random() * 100000); paulo@89: paulo@89: //CSS class to specify whether element should not have mouse events. paulo@89: var nvPointerEventsClass = "nv-pointer-events-none"; paulo@89: paulo@89: //Format function for the tooltip values column paulo@89: var valueFormatter = function(d,i) { paulo@89: return d; paulo@89: }; paulo@89: paulo@89: //Format function for the tooltip header value. paulo@89: var headerFormatter = function(d) { paulo@89: return d; paulo@89: }; paulo@89: paulo@89: var keyFormatter = function(d, i) { paulo@89: return d; paulo@89: }; paulo@89: paulo@89: //By default, the tooltip model renders a beautiful table inside a DIV. paulo@89: //You can override this function if a custom tooltip is desired. paulo@89: var contentGenerator = function(d) { paulo@89: if (d === null) { paulo@89: return ''; paulo@89: } paulo@89: paulo@89: var table = d3.select(document.createElement("table")); paulo@89: if (headerEnabled) { paulo@89: var theadEnter = table.selectAll("thead") paulo@89: .data([d]) paulo@89: .enter().append("thead"); paulo@89: paulo@89: theadEnter.append("tr") paulo@89: .append("td") paulo@89: .attr("colspan", 3) paulo@89: .append("strong") paulo@89: .classed("x-value", true) paulo@89: .html(headerFormatter(d.value)); paulo@89: } paulo@89: paulo@89: var tbodyEnter = table.selectAll("tbody") paulo@89: .data([d]) paulo@89: .enter().append("tbody"); paulo@89: paulo@89: var trowEnter = tbodyEnter.selectAll("tr") paulo@89: .data(function(p) { return p.series}) paulo@89: .enter() paulo@89: .append("tr") paulo@89: .classed("highlight", function(p) { return p.highlight}); paulo@89: paulo@89: trowEnter.append("td") paulo@89: .classed("legend-color-guide",true) paulo@89: .append("div") paulo@89: .style("background-color", function(p) { return p.color}); paulo@89: paulo@89: trowEnter.append("td") paulo@89: .classed("key",true) paulo@89: .html(function(p, i) {return keyFormatter(p.key, i)}); paulo@89: paulo@89: trowEnter.append("td") paulo@89: .classed("value",true) paulo@89: .html(function(p, i) { return valueFormatter(p.value, i) }); paulo@89: paulo@89: paulo@89: trowEnter.selectAll("td").each(function(p) { paulo@89: if (p.highlight) { paulo@89: var opacityScale = d3.scale.linear().domain([0,1]).range(["#fff",p.color]); paulo@89: var opacity = 0.6; paulo@89: d3.select(this) paulo@89: .style("border-bottom-color", opacityScale(opacity)) paulo@89: .style("border-top-color", opacityScale(opacity)) paulo@89: ; paulo@89: } paulo@89: }); paulo@89: paulo@89: var html = table.node().outerHTML; paulo@89: if (d.footer !== undefined) paulo@89: html += ""; paulo@89: return html; paulo@89: paulo@89: }; paulo@89: paulo@89: var dataSeriesExists = function(d) { paulo@89: if (d && d.series) { paulo@89: if (d.series instanceof Array) { paulo@89: return !!d.series.length; paulo@89: } paulo@89: // if object, it's okay just convert to array of the object paulo@89: if (d.series instanceof Object) { paulo@89: d.series = [d.series]; paulo@89: return true; paulo@89: } paulo@89: } paulo@89: return false; paulo@89: }; paulo@89: paulo@89: var calcTooltipPosition = function(pos) { paulo@89: if (!tooltipElem) return; paulo@89: paulo@89: nv.dom.read(function() { paulo@89: var height = parseInt(tooltipElem.offsetHeight, 10), paulo@89: width = parseInt(tooltipElem.offsetWidth, 10), paulo@89: windowWidth = nv.utils.windowSize().width, paulo@89: windowHeight = nv.utils.windowSize().height, paulo@89: scrollTop = window.pageYOffset, paulo@89: scrollLeft = window.pageXOffset, paulo@89: left, top; paulo@89: paulo@89: windowHeight = window.innerWidth >= document.body.scrollWidth ? windowHeight : windowHeight - 16; paulo@89: windowWidth = window.innerHeight >= document.body.scrollHeight ? windowWidth : windowWidth - 16; paulo@89: paulo@89: paulo@89: //Helper functions to find the total offsets of a given DOM element. paulo@89: //Looks up the entire ancestry of an element, up to the first relatively positioned element. paulo@89: var tooltipTop = function ( Elem ) { paulo@89: var offsetTop = top; paulo@89: do { paulo@89: if( !isNaN( Elem.offsetTop ) ) { paulo@89: offsetTop += (Elem.offsetTop); paulo@89: } paulo@89: Elem = Elem.offsetParent; paulo@89: } while( Elem ); paulo@89: return offsetTop; paulo@89: }; paulo@89: var tooltipLeft = function ( Elem ) { paulo@89: var offsetLeft = left; paulo@89: do { paulo@89: if( !isNaN( Elem.offsetLeft ) ) { paulo@89: offsetLeft += (Elem.offsetLeft); paulo@89: } paulo@89: Elem = Elem.offsetParent; paulo@89: } while( Elem ); paulo@89: return offsetLeft; paulo@89: }; paulo@89: paulo@89: // calculate position based on gravity paulo@89: var tLeft, tTop; paulo@89: switch (gravity) { paulo@89: case 'e': paulo@89: left = pos[0] - width - distance; paulo@89: top = pos[1] - (height / 2); paulo@89: tLeft = tooltipLeft(tooltipElem); paulo@89: tTop = tooltipTop(tooltipElem); paulo@89: if (tLeft < scrollLeft) left = pos[0] + distance > scrollLeft ? pos[0] + distance : scrollLeft - tLeft + left; paulo@89: if (tTop < scrollTop) top = scrollTop - tTop + top; paulo@89: if (tTop + height > scrollTop + windowHeight) top = scrollTop + windowHeight - tTop + top - height; paulo@89: break; paulo@89: case 'w': paulo@89: left = pos[0] + distance; paulo@89: top = pos[1] - (height / 2); paulo@89: tLeft = tooltipLeft(tooltipElem); paulo@89: tTop = tooltipTop(tooltipElem); paulo@89: if (tLeft + width > windowWidth) left = pos[0] - width - distance; paulo@89: if (tTop < scrollTop) top = scrollTop + 5; paulo@89: if (tTop + height > scrollTop + windowHeight) top = scrollTop + windowHeight - tTop + top - height; paulo@89: break; paulo@89: case 'n': paulo@89: left = pos[0] - (width / 2) - 5; paulo@89: top = pos[1] + distance; paulo@89: tLeft = tooltipLeft(tooltipElem); paulo@89: tTop = tooltipTop(tooltipElem); paulo@89: if (tLeft < scrollLeft) left = scrollLeft + 5; paulo@89: if (tLeft + width > windowWidth) left = left - width/2 + 5; paulo@89: if (tTop + height > scrollTop + windowHeight) top = scrollTop + windowHeight - tTop + top - height; paulo@89: break; paulo@89: case 's': paulo@89: left = pos[0] - (width / 2); paulo@89: top = pos[1] - height - distance; paulo@89: tLeft = tooltipLeft(tooltipElem); paulo@89: tTop = tooltipTop(tooltipElem); paulo@89: if (tLeft < scrollLeft) left = scrollLeft + 5; paulo@89: if (tLeft + width > windowWidth) left = left - width/2 + 5; paulo@89: if (scrollTop > tTop) top = scrollTop; paulo@89: break; paulo@89: case 'none': paulo@89: left = pos[0]; paulo@89: top = pos[1] - distance; paulo@89: tLeft = tooltipLeft(tooltipElem); paulo@89: tTop = tooltipTop(tooltipElem); paulo@89: break; paulo@89: } paulo@89: paulo@89: // adjust tooltip offsets paulo@89: left -= offset.left; paulo@89: top -= offset.top; paulo@89: paulo@89: // using tooltip.style('transform') returns values un-usable for tween paulo@89: var box = tooltipElem.getBoundingClientRect(); paulo@89: var scrollTop = window.pageYOffset || document.documentElement.scrollTop; paulo@89: var scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; paulo@89: var old_translate = 'translate(' + (box.left + scrollLeft) + 'px, ' + (box.top + scrollTop) + 'px)'; paulo@89: var new_translate = 'translate(' + left + 'px, ' + top + 'px)'; paulo@89: var translateInterpolator = d3.interpolateString(old_translate, new_translate); paulo@89: paulo@89: var is_hidden = tooltip.style('opacity') < 0.1; paulo@89: paulo@89: // delay hiding a bit to avoid flickering paulo@89: if (hidden) { paulo@89: tooltip paulo@89: .transition() paulo@89: .delay(hideDelay) paulo@89: .duration(0) paulo@89: .style('opacity', 0); paulo@89: } else { paulo@89: tooltip paulo@89: .interrupt() // cancel running transitions paulo@89: .transition() paulo@89: .duration(is_hidden ? 0 : duration) paulo@89: // using tween since some versions of d3 can't auto-tween a translate on a div paulo@89: .styleTween('transform', function (d) { paulo@89: return translateInterpolator; paulo@89: }, 'important') paulo@89: // Safari has its own `-webkit-transform` and does not support `transform` paulo@89: // transform tooltip without transition only in Safari paulo@89: .style('-webkit-transform', new_translate) paulo@89: .style('opacity', 1); paulo@89: } paulo@89: paulo@89: paulo@89: paulo@89: }); paulo@89: }; paulo@89: paulo@89: //In situations where the chart is in a 'viewBox', re-position the tooltip based on how far chart is zoomed. paulo@89: function convertViewBoxRatio() { paulo@89: if (chartContainer) { paulo@89: var svg = d3.select(chartContainer); paulo@89: if (svg.node().tagName !== "svg") { paulo@89: svg = svg.select("svg"); paulo@89: } paulo@89: var viewBox = (svg.node()) ? svg.attr('viewBox') : null; paulo@89: if (viewBox) { paulo@89: viewBox = viewBox.split(' '); paulo@89: var ratio = parseInt(svg.style('width'), 10) / viewBox[2]; paulo@89: paulo@89: position.left = position.left * ratio; paulo@89: position.top = position.top * ratio; paulo@89: } paulo@89: } paulo@89: } paulo@89: paulo@89: //Creates new tooltip container, or uses existing one on DOM. paulo@89: function initTooltip() { paulo@89: if (!tooltip) { paulo@89: var body; paulo@89: if (chartContainer) { paulo@89: body = chartContainer; paulo@89: } else { paulo@89: body = document.body; paulo@89: } paulo@89: //Create new tooltip div if it doesn't exist on DOM. paulo@89: tooltip = d3.select(body).append("div") paulo@89: .attr("class", "nvtooltip " + (classes ? classes : "xy-tooltip")) paulo@89: .attr("id", id); paulo@89: tooltip.style("top", 0).style("left", 0); paulo@89: tooltip.style('opacity', 0); paulo@89: tooltip.selectAll("div, table, td, tr").classed(nvPointerEventsClass, true); paulo@89: tooltip.classed(nvPointerEventsClass, true); paulo@89: tooltipElem = tooltip.node(); paulo@89: } paulo@89: } paulo@89: paulo@89: //Draw the tooltip onto the DOM. paulo@89: function nvtooltip() { paulo@89: if (!enabled) return; paulo@89: if (!dataSeriesExists(data)) return; paulo@89: paulo@89: convertViewBoxRatio(); paulo@89: paulo@89: var left = position.left; paulo@89: var top = (fixedTop !== null) ? fixedTop : position.top; paulo@89: paulo@89: nv.dom.write(function () { paulo@89: initTooltip(); paulo@89: // generate data and set it into tooltip paulo@89: // Bonus - If you override contentGenerator and return falsey you can use something like paulo@89: // React or Knockout to bind the data for your tooltip paulo@89: var newContent = contentGenerator(data); paulo@89: if (newContent) { paulo@89: tooltipElem.innerHTML = newContent; paulo@89: } paulo@89: paulo@89: if (chartContainer && isInteractiveLayer) { paulo@89: nv.dom.read(function() { paulo@89: var svgComp = chartContainer.getElementsByTagName("svg")[0]; paulo@89: var svgOffset = {left:0,top:0}; paulo@89: if (svgComp) { paulo@89: var svgBound = svgComp.getBoundingClientRect(); paulo@89: var chartBound = chartContainer.getBoundingClientRect(); paulo@89: var svgBoundTop = svgBound.top; paulo@89: paulo@89: //Defensive code. Sometimes, svgBoundTop can be a really negative paulo@89: // number, like -134254. That's a bug. paulo@89: // If such a number is found, use zero instead. FireFox bug only paulo@89: if (svgBoundTop < 0) { paulo@89: var containerBound = chartContainer.getBoundingClientRect(); paulo@89: svgBoundTop = (Math.abs(svgBoundTop) > containerBound.height) ? 0 : svgBoundTop; paulo@89: } paulo@89: svgOffset.top = Math.abs(svgBoundTop - chartBound.top); paulo@89: svgOffset.left = Math.abs(svgBound.left - chartBound.left); paulo@89: } paulo@89: //If the parent container is an overflow
with scrollbars, subtract the scroll offsets. paulo@89: //You need to also add any offset between the element and its containing
paulo@89: //Finally, add any offset of the containing
on the whole page. paulo@89: left += chartContainer.offsetLeft + svgOffset.left - 2*chartContainer.scrollLeft; paulo@89: top += chartContainer.offsetTop + svgOffset.top - 2*chartContainer.scrollTop; paulo@89: paulo@89: if (snapDistance && snapDistance > 0) { paulo@89: top = Math.floor(top/snapDistance) * snapDistance; paulo@89: } paulo@89: calcTooltipPosition([left,top]); paulo@89: }); paulo@89: } else { paulo@89: calcTooltipPosition([left,top]); paulo@89: } paulo@89: }); paulo@89: paulo@89: return nvtooltip; paulo@89: } paulo@89: paulo@89: nvtooltip.nvPointerEventsClass = nvPointerEventsClass; paulo@89: nvtooltip.options = nv.utils.optionsFunc.bind(nvtooltip); paulo@89: paulo@89: nvtooltip._options = Object.create({}, { paulo@89: // simple read/write options paulo@89: duration: {get: function(){return duration;}, set: function(_){duration=_;}}, paulo@89: gravity: {get: function(){return gravity;}, set: function(_){gravity=_;}}, paulo@89: distance: {get: function(){return distance;}, set: function(_){distance=_;}}, paulo@89: snapDistance: {get: function(){return snapDistance;}, set: function(_){snapDistance=_;}}, paulo@89: classes: {get: function(){return classes;}, set: function(_){classes=_;}}, paulo@89: chartContainer: {get: function(){return chartContainer;}, set: function(_){chartContainer=_;}}, paulo@89: fixedTop: {get: function(){return fixedTop;}, set: function(_){fixedTop=_;}}, paulo@89: enabled: {get: function(){return enabled;}, set: function(_){enabled=_;}}, paulo@89: hideDelay: {get: function(){return hideDelay;}, set: function(_){hideDelay=_;}}, paulo@89: contentGenerator: {get: function(){return contentGenerator;}, set: function(_){contentGenerator=_;}}, paulo@89: valueFormatter: {get: function(){return valueFormatter;}, set: function(_){valueFormatter=_;}}, paulo@89: headerFormatter: {get: function(){return headerFormatter;}, set: function(_){headerFormatter=_;}}, paulo@89: keyFormatter: {get: function(){return keyFormatter;}, set: function(_){keyFormatter=_;}}, paulo@89: headerEnabled: {get: function(){return headerEnabled;}, set: function(_){headerEnabled=_;}}, paulo@89: paulo@89: // internal use only, set by interactive layer to adjust position. paulo@89: _isInteractiveLayer: {get: function(){return isInteractiveLayer;}, set: function(_){isInteractiveLayer=!!_;}}, paulo@89: paulo@89: // options with extra logic paulo@89: position: {get: function(){return position;}, set: function(_){ paulo@89: position.left = _.left !== undefined ? _.left : position.left; paulo@89: position.top = _.top !== undefined ? _.top : position.top; paulo@89: }}, paulo@89: offset: {get: function(){return offset;}, set: function(_){ paulo@89: offset.left = _.left !== undefined ? _.left : offset.left; paulo@89: offset.top = _.top !== undefined ? _.top : offset.top; paulo@89: }}, paulo@89: hidden: {get: function(){return hidden;}, set: function(_){ paulo@89: if (hidden != _) { paulo@89: hidden = !!_; paulo@89: nvtooltip(); paulo@89: } paulo@89: }}, paulo@89: data: {get: function(){return data;}, set: function(_){ paulo@89: // if showing a single data point, adjust data format with that paulo@89: if (_.point) { paulo@89: _.value = _.point.x; paulo@89: _.series = _.series || {}; paulo@89: _.series.value = _.point.y; paulo@89: _.series.color = _.point.color || _.series.color; paulo@89: } paulo@89: data = _; paulo@89: }}, paulo@89: paulo@89: // read only properties paulo@89: tooltipElem: {get: function(){return tooltipElem;}, set: function(_){}}, paulo@89: id: {get: function(){return id;}, set: function(_){}} paulo@89: }); paulo@89: paulo@89: nv.utils.initOptions(nvtooltip); paulo@89: return nvtooltip; paulo@89: }; paulo@89: paulo@89: })(); paulo@89: paulo@89: paulo@89: /* paulo@89: Gets the browser window size paulo@89: paulo@89: Returns object with height and width properties paulo@89: */ paulo@89: nv.utils.windowSize = function() { paulo@89: // Sane defaults paulo@89: var size = {width: 640, height: 480}; paulo@89: paulo@89: // Most recent browsers use paulo@89: if (window.innerWidth && window.innerHeight) { paulo@89: size.width = window.innerWidth; paulo@89: size.height = window.innerHeight; paulo@89: return (size); paulo@89: } paulo@89: paulo@89: // IE can use depending on mode it is in paulo@89: if (document.compatMode=='CSS1Compat' && paulo@89: document.documentElement && paulo@89: document.documentElement.offsetWidth ) { paulo@89: paulo@89: size.width = document.documentElement.offsetWidth; paulo@89: size.height = document.documentElement.offsetHeight; paulo@89: return (size); paulo@89: } paulo@89: paulo@89: // Earlier IE uses Doc.body paulo@89: if (document.body && document.body.offsetWidth) { paulo@89: size.width = document.body.offsetWidth; paulo@89: size.height = document.body.offsetHeight; paulo@89: return (size); paulo@89: } paulo@89: paulo@89: return (size); paulo@89: }; paulo@89: paulo@89: /* paulo@89: Binds callback function to run when window is resized paulo@89: */ paulo@89: nv.utils.windowResize = function(handler) { paulo@89: if (window.addEventListener) { paulo@89: window.addEventListener('resize', handler); paulo@89: } else { paulo@89: nv.log("ERROR: Failed to bind to window.resize with: ", handler); paulo@89: } paulo@89: // return object with clear function to remove the single added callback. paulo@89: return { paulo@89: callback: handler, paulo@89: clear: function() { paulo@89: window.removeEventListener('resize', handler); paulo@89: } paulo@89: } paulo@89: }; paulo@89: paulo@89: paulo@89: /* paulo@89: Backwards compatible way to implement more d3-like coloring of graphs. paulo@89: Can take in nothing, an array, or a function/scale paulo@89: To use a normal scale, get the range and pass that because we must be able paulo@89: to take two arguments and use the index to keep backward compatibility paulo@89: */ paulo@89: nv.utils.getColor = function(color) { paulo@89: //if you pass in nothing, get default colors back paulo@89: if (color === undefined) { paulo@89: return nv.utils.defaultColor(); paulo@89: paulo@89: //if passed an array, turn it into a color scale paulo@89: // use isArray, instanceof fails if d3 range is created in an iframe paulo@89: } else if(Array.isArray(color)) { paulo@89: var color_scale = d3.scale.ordinal().range(color); paulo@89: return function(d, i) { paulo@89: var key = i === undefined ? d : i; paulo@89: return d.color || color_scale(key); paulo@89: }; paulo@89: paulo@89: //if passed a function or scale, return it, or whatever it may be paulo@89: //external libs, such as angularjs-nvd3-directives use this paulo@89: } else { paulo@89: //can't really help it if someone passes rubbish as color paulo@89: return color; paulo@89: } paulo@89: }; paulo@89: paulo@89: paulo@89: /* paulo@89: Default color chooser uses a color scale of 20 colors from D3 paulo@89: https://github.com/mbostock/d3/wiki/Ordinal-Scales#categorical-colors paulo@89: */ paulo@89: nv.utils.defaultColor = function() { paulo@89: // get range of the scale so we'll turn it into our own function. paulo@89: return nv.utils.getColor(d3.scale.category20().range()); paulo@89: }; paulo@89: paulo@89: paulo@89: /* paulo@89: Returns a color function that takes the result of 'getKey' for each series and paulo@89: looks for a corresponding color from the dictionary paulo@89: */ paulo@89: nv.utils.customTheme = function(dictionary, getKey, defaultColors) { paulo@89: // use default series.key if getKey is undefined paulo@89: getKey = getKey || function(series) { return series.key }; paulo@89: defaultColors = defaultColors || d3.scale.category20().range(); paulo@89: paulo@89: // start at end of default color list and walk back to index 0 paulo@89: var defIndex = defaultColors.length; paulo@89: paulo@89: return function(series, index) { paulo@89: var key = getKey(series); paulo@89: if (typeof dictionary[key] === 'function') { paulo@89: return dictionary[key](); paulo@89: } else if (dictionary[key] !== undefined) { paulo@89: return dictionary[key]; paulo@89: } else { paulo@89: // no match in dictionary, use a default color paulo@89: if (!defIndex) { paulo@89: // used all the default colors, start over paulo@89: defIndex = defaultColors.length; paulo@89: } paulo@89: defIndex = defIndex - 1; paulo@89: return defaultColors[defIndex]; paulo@89: } paulo@89: }; paulo@89: }; paulo@89: paulo@89: paulo@89: /* paulo@89: From the PJAX example on d3js.org, while this is not really directly needed paulo@89: it's a very cool method for doing pjax, I may expand upon it a little bit, paulo@89: open to suggestions on anything that may be useful paulo@89: */ paulo@89: nv.utils.pjax = function(links, content) { paulo@89: paulo@89: var load = function(href) { paulo@89: d3.html(href, function(fragment) { paulo@89: var target = d3.select(content).node(); paulo@89: target.parentNode.replaceChild( paulo@89: d3.select(fragment).select(content).node(), paulo@89: target); paulo@89: nv.utils.pjax(links, content); paulo@89: }); paulo@89: }; paulo@89: paulo@89: d3.selectAll(links).on("click", function() { paulo@89: history.pushState(this.href, this.textContent, this.href); paulo@89: load(this.href); paulo@89: d3.event.preventDefault(); paulo@89: }); paulo@89: paulo@89: d3.select(window).on("popstate", function() { paulo@89: if (d3.event.state) { paulo@89: load(d3.event.state); paulo@89: } paulo@89: }); paulo@89: }; paulo@89: paulo@89: paulo@89: /* paulo@89: For when we want to approximate the width in pixels for an SVG:text element. paulo@89: Most common instance is when the element is in a display:none; container. paulo@89: Forumla is : text.length * font-size * constant_factor paulo@89: */ paulo@89: nv.utils.calcApproxTextWidth = function (svgTextElem) { paulo@89: if (typeof svgTextElem.style === 'function' paulo@89: && typeof svgTextElem.text === 'function') { paulo@89: paulo@89: var fontSize = parseInt(svgTextElem.style("font-size").replace("px",""), 10); paulo@89: var textLength = svgTextElem.text().length; paulo@89: return textLength * fontSize * 0.5; paulo@89: } paulo@89: return 0; paulo@89: }; paulo@89: paulo@89: paulo@89: /* paulo@89: Numbers that are undefined, null or NaN, convert them to zeros. paulo@89: */ paulo@89: nv.utils.NaNtoZero = function(n) { paulo@89: if (typeof n !== 'number' paulo@89: || isNaN(n) paulo@89: || n === null paulo@89: || n === Infinity paulo@89: || n === -Infinity) { paulo@89: paulo@89: return 0; paulo@89: } paulo@89: return n; paulo@89: }; paulo@89: paulo@89: /* paulo@89: Add a way to watch for d3 transition ends to d3 paulo@89: */ paulo@89: d3.selection.prototype.watchTransition = function(renderWatch){ paulo@89: var args = [this].concat([].slice.call(arguments, 1)); paulo@89: return renderWatch.transition.apply(renderWatch, args); paulo@89: }; paulo@89: paulo@89: paulo@89: /* paulo@89: Helper object to watch when d3 has rendered something paulo@89: */ paulo@89: nv.utils.renderWatch = function(dispatch, duration) { paulo@89: if (!(this instanceof nv.utils.renderWatch)) { paulo@89: return new nv.utils.renderWatch(dispatch, duration); paulo@89: } paulo@89: paulo@89: var _duration = duration !== undefined ? duration : 250; paulo@89: var renderStack = []; paulo@89: var self = this; paulo@89: paulo@89: this.models = function(models) { paulo@89: models = [].slice.call(arguments, 0); paulo@89: models.forEach(function(model){ paulo@89: model.__rendered = false; paulo@89: (function(m){ paulo@89: m.dispatch.on('renderEnd', function(arg){ paulo@89: m.__rendered = true; paulo@89: self.renderEnd('model'); paulo@89: }); paulo@89: })(model); paulo@89: paulo@89: if (renderStack.indexOf(model) < 0) { paulo@89: renderStack.push(model); paulo@89: } paulo@89: }); paulo@89: return this; paulo@89: }; paulo@89: paulo@89: this.reset = function(duration) { paulo@89: if (duration !== undefined) { paulo@89: _duration = duration; paulo@89: } paulo@89: renderStack = []; paulo@89: }; paulo@89: paulo@89: this.transition = function(selection, args, duration) { paulo@89: args = arguments.length > 1 ? [].slice.call(arguments, 1) : []; paulo@89: paulo@89: if (args.length > 1) { paulo@89: duration = args.pop(); paulo@89: } else { paulo@89: duration = _duration !== undefined ? _duration : 250; paulo@89: } paulo@89: selection.__rendered = false; paulo@89: paulo@89: if (renderStack.indexOf(selection) < 0) { paulo@89: renderStack.push(selection); paulo@89: } paulo@89: paulo@89: if (duration === 0) { paulo@89: selection.__rendered = true; paulo@89: selection.delay = function() { return this; }; paulo@89: selection.duration = function() { return this; }; paulo@89: return selection; paulo@89: } else { paulo@89: if (selection.length === 0) { paulo@89: selection.__rendered = true; paulo@89: } else if (selection.every( function(d){ return !d.length; } )) { paulo@89: selection.__rendered = true; paulo@89: } else { paulo@89: selection.__rendered = false; paulo@89: } paulo@89: paulo@89: var n = 0; paulo@89: return selection paulo@89: .transition() paulo@89: .duration(duration) paulo@89: .each(function(){ ++n; }) paulo@89: .each('end', function(d, i) { paulo@89: if (--n === 0) { paulo@89: selection.__rendered = true; paulo@89: self.renderEnd.apply(this, args); paulo@89: } paulo@89: }); paulo@89: } paulo@89: }; paulo@89: paulo@89: this.renderEnd = function() { paulo@89: if (renderStack.every( function(d){ return d.__rendered; } )) { paulo@89: renderStack.forEach( function(d){ d.__rendered = false; }); paulo@89: dispatch.renderEnd.apply(this, arguments); paulo@89: } paulo@89: } paulo@89: paulo@89: }; paulo@89: paulo@89: paulo@89: /* paulo@89: Takes multiple objects and combines them into the first one (dst) paulo@89: example: nv.utils.deepExtend({a: 1}, {a: 2, b: 3}, {c: 4}); paulo@89: gives: {a: 2, b: 3, c: 4} paulo@89: */ paulo@89: nv.utils.deepExtend = function(dst){ paulo@89: var sources = arguments.length > 1 ? [].slice.call(arguments, 1) : []; paulo@89: sources.forEach(function(source) { paulo@89: for (var key in source) { paulo@89: var isArray = dst[key] instanceof Array; paulo@89: var isObject = typeof dst[key] === 'object'; paulo@89: var srcObj = typeof source[key] === 'object'; paulo@89: paulo@89: if (isObject && !isArray && srcObj) { paulo@89: nv.utils.deepExtend(dst[key], source[key]); paulo@89: } else { paulo@89: dst[key] = source[key]; paulo@89: } paulo@89: } paulo@89: }); paulo@89: }; paulo@89: paulo@89: paulo@89: /* paulo@89: state utility object, used to track d3 states in the models paulo@89: */ paulo@89: nv.utils.state = function(){ paulo@89: if (!(this instanceof nv.utils.state)) { paulo@89: return new nv.utils.state(); paulo@89: } paulo@89: var state = {}; paulo@89: var _self = this; paulo@89: var _setState = function(){}; paulo@89: var _getState = function(){ return {}; }; paulo@89: var init = null; paulo@89: var changed = null; paulo@89: paulo@89: this.dispatch = d3.dispatch('change', 'set'); paulo@89: paulo@89: this.dispatch.on('set', function(state){ paulo@89: _setState(state, true); paulo@89: }); paulo@89: paulo@89: this.getter = function(fn){ paulo@89: _getState = fn; paulo@89: return this; paulo@89: }; paulo@89: paulo@89: this.setter = function(fn, callback) { paulo@89: if (!callback) { paulo@89: callback = function(){}; paulo@89: } paulo@89: _setState = function(state, update){ paulo@89: fn(state); paulo@89: if (update) { paulo@89: callback(); paulo@89: } paulo@89: }; paulo@89: return this; paulo@89: }; paulo@89: paulo@89: this.init = function(state){ paulo@89: init = init || {}; paulo@89: nv.utils.deepExtend(init, state); paulo@89: }; paulo@89: paulo@89: var _set = function(){ paulo@89: var settings = _getState(); paulo@89: paulo@89: if (JSON.stringify(settings) === JSON.stringify(state)) { paulo@89: return false; paulo@89: } paulo@89: paulo@89: for (var key in settings) { paulo@89: if (state[key] === undefined) { paulo@89: state[key] = {}; paulo@89: } paulo@89: state[key] = settings[key]; paulo@89: changed = true; paulo@89: } paulo@89: return true; paulo@89: }; paulo@89: paulo@89: this.update = function(){ paulo@89: if (init) { paulo@89: _setState(init, false); paulo@89: init = null; paulo@89: } paulo@89: if (_set.call(this)) { paulo@89: this.dispatch.change(state); paulo@89: } paulo@89: }; paulo@89: paulo@89: }; paulo@89: paulo@89: paulo@89: /* paulo@89: Snippet of code you can insert into each nv.models.* to give you the ability to paulo@89: do things like: paulo@89: chart.options({ paulo@89: showXAxis: true, paulo@89: tooltips: true paulo@89: }); paulo@89: paulo@89: To enable in the chart: paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: */ paulo@89: nv.utils.optionsFunc = function(args) { paulo@89: if (args) { paulo@89: d3.map(args).forEach((function(key,value) { paulo@89: if (typeof this[key] === "function") { paulo@89: this[key](value); paulo@89: } paulo@89: }).bind(this)); paulo@89: } paulo@89: return this; paulo@89: }; paulo@89: paulo@89: paulo@89: /* paulo@89: numTicks: requested number of ticks paulo@89: data: the chart data paulo@89: paulo@89: returns the number of ticks to actually use on X axis, based on chart data paulo@89: to avoid duplicate ticks with the same value paulo@89: */ paulo@89: nv.utils.calcTicksX = function(numTicks, data) { paulo@89: // find max number of values from all data streams paulo@89: var numValues = 1; paulo@89: var i = 0; paulo@89: for (i; i < data.length; i += 1) { paulo@89: var stream_len = data[i] && data[i].values ? data[i].values.length : 0; paulo@89: numValues = stream_len > numValues ? stream_len : numValues; paulo@89: } paulo@89: nv.log("Requested number of ticks: ", numTicks); paulo@89: nv.log("Calculated max values to be: ", numValues); paulo@89: // make sure we don't have more ticks than values to avoid duplicates paulo@89: numTicks = numTicks > numValues ? numTicks = numValues - 1 : numTicks; paulo@89: // make sure we have at least one tick paulo@89: numTicks = numTicks < 1 ? 1 : numTicks; paulo@89: // make sure it's an integer paulo@89: numTicks = Math.floor(numTicks); paulo@89: nv.log("Calculating tick count as: ", numTicks); paulo@89: return numTicks; paulo@89: }; paulo@89: paulo@89: paulo@89: /* paulo@89: returns number of ticks to actually use on Y axis, based on chart data paulo@89: */ paulo@89: nv.utils.calcTicksY = function(numTicks, data) { paulo@89: // currently uses the same logic but we can adjust here if needed later paulo@89: return nv.utils.calcTicksX(numTicks, data); paulo@89: }; paulo@89: paulo@89: paulo@89: /* paulo@89: Add a particular option from an options object onto chart paulo@89: Options exposed on a chart are a getter/setter function that returns chart paulo@89: on set to mimic typical d3 option chaining, e.g. svg.option1('a').option2('b'); paulo@89: paulo@89: option objects should be generated via Object.create() to provide paulo@89: the option of manipulating data via get/set functions. paulo@89: */ paulo@89: nv.utils.initOption = function(chart, name) { paulo@89: // if it's a call option, just call it directly, otherwise do get/set paulo@89: if (chart._calls && chart._calls[name]) { paulo@89: chart[name] = chart._calls[name]; paulo@89: } else { paulo@89: chart[name] = function (_) { paulo@89: if (!arguments.length) return chart._options[name]; paulo@89: chart._overrides[name] = true; paulo@89: chart._options[name] = _; paulo@89: return chart; paulo@89: }; paulo@89: // calling the option as _option will ignore if set by option already paulo@89: // so nvd3 can set options internally but the stop if set manually paulo@89: chart['_' + name] = function(_) { paulo@89: if (!arguments.length) return chart._options[name]; paulo@89: if (!chart._overrides[name]) { paulo@89: chart._options[name] = _; paulo@89: } paulo@89: return chart; paulo@89: } paulo@89: } paulo@89: }; paulo@89: paulo@89: paulo@89: /* paulo@89: Add all options in an options object to the chart paulo@89: */ paulo@89: nv.utils.initOptions = function(chart) { paulo@89: chart._overrides = chart._overrides || {}; paulo@89: var ops = Object.getOwnPropertyNames(chart._options || {}); paulo@89: var calls = Object.getOwnPropertyNames(chart._calls || {}); paulo@89: ops = ops.concat(calls); paulo@89: for (var i in ops) { paulo@89: nv.utils.initOption(chart, ops[i]); paulo@89: } paulo@89: }; paulo@89: paulo@89: paulo@89: /* paulo@89: Inherit options from a D3 object paulo@89: d3.rebind makes calling the function on target actually call it on source paulo@89: Also use _d3options so we can track what we inherit for documentation and chained inheritance paulo@89: */ paulo@89: nv.utils.inheritOptionsD3 = function(target, d3_source, oplist) { paulo@89: target._d3options = oplist.concat(target._d3options || []); paulo@89: oplist.unshift(d3_source); paulo@89: oplist.unshift(target); paulo@89: d3.rebind.apply(this, oplist); paulo@89: }; paulo@89: paulo@89: paulo@89: /* paulo@89: Remove duplicates from an array paulo@89: */ paulo@89: nv.utils.arrayUnique = function(a) { paulo@89: return a.sort().filter(function(item, pos) { paulo@89: return !pos || item != a[pos - 1]; paulo@89: }); paulo@89: }; paulo@89: paulo@89: paulo@89: /* paulo@89: Keeps a list of custom symbols to draw from in addition to d3.svg.symbol paulo@89: Necessary since d3 doesn't let you extend its list -_- paulo@89: Add new symbols by doing nv.utils.symbols.set('name', function(size){...}); paulo@89: */ paulo@89: nv.utils.symbolMap = d3.map(); paulo@89: paulo@89: paulo@89: /* paulo@89: Replaces d3.svg.symbol so that we can look both there and our own map paulo@89: */ paulo@89: nv.utils.symbol = function() { paulo@89: var type, paulo@89: size = 64; paulo@89: function symbol(d,i) { paulo@89: var t = type.call(this,d,i); paulo@89: var s = size.call(this,d,i); paulo@89: if (d3.svg.symbolTypes.indexOf(t) !== -1) { paulo@89: return d3.svg.symbol().type(t).size(s)(); paulo@89: } else { paulo@89: return nv.utils.symbolMap.get(t)(s); paulo@89: } paulo@89: } paulo@89: symbol.type = function(_) { paulo@89: if (!arguments.length) return type; paulo@89: type = d3.functor(_); paulo@89: return symbol; paulo@89: }; paulo@89: symbol.size = function(_) { paulo@89: if (!arguments.length) return size; paulo@89: size = d3.functor(_); paulo@89: return symbol; paulo@89: }; paulo@89: return symbol; paulo@89: }; paulo@89: paulo@89: paulo@89: /* paulo@89: Inherit option getter/setter functions from source to target paulo@89: d3.rebind makes calling the function on target actually call it on source paulo@89: Also track via _inherited and _d3options so we can track what we inherit paulo@89: for documentation generation purposes and chained inheritance paulo@89: */ paulo@89: nv.utils.inheritOptions = function(target, source) { paulo@89: // inherit all the things paulo@89: var ops = Object.getOwnPropertyNames(source._options || {}); paulo@89: var calls = Object.getOwnPropertyNames(source._calls || {}); paulo@89: var inherited = source._inherited || []; paulo@89: var d3ops = source._d3options || []; paulo@89: var args = ops.concat(calls).concat(inherited).concat(d3ops); paulo@89: args.unshift(source); paulo@89: args.unshift(target); paulo@89: d3.rebind.apply(this, args); paulo@89: // pass along the lists to keep track of them, don't allow duplicates paulo@89: target._inherited = nv.utils.arrayUnique(ops.concat(calls).concat(inherited).concat(ops).concat(target._inherited || [])); paulo@89: target._d3options = nv.utils.arrayUnique(d3ops.concat(target._d3options || [])); paulo@89: }; paulo@89: paulo@89: paulo@89: /* paulo@89: Runs common initialize code on the svg before the chart builds paulo@89: */ paulo@89: nv.utils.initSVG = function(svg) { paulo@89: svg.classed({'nvd3-svg':true}); paulo@89: }; paulo@89: paulo@89: paulo@89: /* paulo@89: Sanitize and provide default for the container height. paulo@89: */ paulo@89: nv.utils.sanitizeHeight = function(height, container) { paulo@89: return (height || parseInt(container.style('height'), 10) || 400); paulo@89: }; paulo@89: paulo@89: paulo@89: /* paulo@89: Sanitize and provide default for the container width. paulo@89: */ paulo@89: nv.utils.sanitizeWidth = function(width, container) { paulo@89: return (width || parseInt(container.style('width'), 10) || 960); paulo@89: }; paulo@89: paulo@89: paulo@89: /* paulo@89: Calculate the available height for a chart. paulo@89: */ paulo@89: nv.utils.availableHeight = function(height, container, margin) { paulo@89: return nv.utils.sanitizeHeight(height, container) - margin.top - margin.bottom; paulo@89: }; paulo@89: paulo@89: /* paulo@89: Calculate the available width for a chart. paulo@89: */ paulo@89: nv.utils.availableWidth = function(width, container, margin) { paulo@89: return nv.utils.sanitizeWidth(width, container) - margin.left - margin.right; paulo@89: }; paulo@89: paulo@89: /* paulo@89: Clear any rendered chart components and display a chart's 'noData' message paulo@89: */ paulo@89: nv.utils.noData = function(chart, container) { paulo@89: var opt = chart.options(), paulo@89: margin = opt.margin(), paulo@89: noData = opt.noData(), paulo@89: data = (noData == null) ? ["No Data Available."] : [noData], paulo@89: height = nv.utils.availableHeight(opt.height(), container, margin), paulo@89: width = nv.utils.availableWidth(opt.width(), container, margin), paulo@89: x = margin.left + width/2, paulo@89: y = margin.top + height/2; paulo@89: paulo@89: //Remove any previously created chart components paulo@89: container.selectAll('g').remove(); paulo@89: paulo@89: var noDataText = container.selectAll('.nv-noData').data(data); paulo@89: paulo@89: noDataText.enter().append('text') paulo@89: .attr('class', 'nvd3 nv-noData') paulo@89: .attr('dy', '-.7em') paulo@89: .style('text-anchor', 'middle'); paulo@89: paulo@89: noDataText paulo@89: .attr('x', x) paulo@89: .attr('y', y) paulo@89: .text(function(t){ return t; }); paulo@89: }; paulo@89: paulo@89: nv.models.axis = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var axis = d3.svg.axis(); paulo@89: var scale = d3.scale.linear(); paulo@89: paulo@89: var margin = {top: 0, right: 0, bottom: 0, left: 0} paulo@89: , width = 75 //only used for tickLabel currently paulo@89: , height = 60 //only used for tickLabel currently paulo@89: , axisLabelText = null paulo@89: , showMaxMin = true //TODO: showMaxMin should be disabled on all ordinal scaled axes paulo@89: , rotateLabels = 0 paulo@89: , rotateYLabel = true paulo@89: , staggerLabels = false paulo@89: , isOrdinal = false paulo@89: , ticks = null paulo@89: , axisLabelDistance = 0 paulo@89: , duration = 250 paulo@89: , dispatch = d3.dispatch('renderEnd') paulo@89: ; paulo@89: axis paulo@89: .scale(scale) paulo@89: .orient('bottom') paulo@89: .tickFormat(function(d) { return d }) paulo@89: ; paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var scale0; paulo@89: var renderWatch = nv.utils.renderWatch(dispatch, duration); paulo@89: paulo@89: function chart(selection) { paulo@89: renderWatch.reset(); paulo@89: selection.each(function(data) { paulo@89: var container = d3.select(this); paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-axis').data([data]); paulo@89: var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-axis'); paulo@89: var gEnter = wrapEnter.append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: if (ticks !== null) paulo@89: axis.ticks(ticks); paulo@89: else if (axis.orient() == 'top' || axis.orient() == 'bottom') paulo@89: axis.ticks(Math.abs(scale.range()[1] - scale.range()[0]) / 100); paulo@89: paulo@89: //TODO: consider calculating width/height based on whether or not label is added, for reference in charts using this component paulo@89: g.watchTransition(renderWatch, 'axis').call(axis); paulo@89: paulo@89: scale0 = scale0 || axis.scale(); paulo@89: paulo@89: var fmt = axis.tickFormat(); paulo@89: if (fmt == null) { paulo@89: fmt = scale0.tickFormat(); paulo@89: } paulo@89: paulo@89: var axisLabel = g.selectAll('text.nv-axislabel') paulo@89: .data([axisLabelText || null]); paulo@89: axisLabel.exit().remove(); paulo@89: paulo@89: var xLabelMargin; paulo@89: var axisMaxMin; paulo@89: var w; paulo@89: switch (axis.orient()) { paulo@89: case 'top': paulo@89: axisLabel.enter().append('text').attr('class', 'nv-axislabel'); paulo@89: if (scale.range().length < 2) { paulo@89: w = 0; paulo@89: } else if (scale.range().length === 2) { paulo@89: w = scale.range()[1]; paulo@89: } else { paulo@89: w = scale.range()[scale.range().length-1]+(scale.range()[1]-scale.range()[0]); paulo@89: } paulo@89: axisLabel paulo@89: .attr('text-anchor', 'middle') paulo@89: .attr('y', 0) paulo@89: .attr('x', w/2); paulo@89: if (showMaxMin) { paulo@89: axisMaxMin = wrap.selectAll('g.nv-axisMaxMin') paulo@89: .data(scale.domain()); paulo@89: axisMaxMin.enter().append('g').attr('class',function(d,i){ paulo@89: return ['nv-axisMaxMin','nv-axisMaxMin-x',(i == 0 ? 'nv-axisMin-x':'nv-axisMax-x')].join(' ') paulo@89: }).append('text'); paulo@89: axisMaxMin.exit().remove(); paulo@89: axisMaxMin paulo@89: .attr('transform', function(d,i) { paulo@89: return 'translate(' + nv.utils.NaNtoZero(scale(d)) + ',0)' paulo@89: }) paulo@89: .select('text') paulo@89: .attr('dy', '-0.5em') paulo@89: .attr('y', -axis.tickPadding()) paulo@89: .attr('text-anchor', 'middle') paulo@89: .text(function(d,i) { paulo@89: var v = fmt(d); paulo@89: return ('' + v).match('NaN') ? '' : v; paulo@89: }); paulo@89: axisMaxMin.watchTransition(renderWatch, 'min-max top') paulo@89: .attr('transform', function(d,i) { paulo@89: return 'translate(' + nv.utils.NaNtoZero(scale.range()[i]) + ',0)' paulo@89: }); paulo@89: } paulo@89: break; paulo@89: case 'bottom': paulo@89: xLabelMargin = axisLabelDistance + 36; paulo@89: var maxTextWidth = 30; paulo@89: var textHeight = 0; paulo@89: var xTicks = g.selectAll('g').select("text"); paulo@89: var rotateLabelsRule = ''; paulo@89: if (rotateLabels%360) { paulo@89: //Calculate the longest xTick width paulo@89: xTicks.each(function(d,i){ paulo@89: var box = this.getBoundingClientRect(); paulo@89: var width = box.width; paulo@89: textHeight = box.height; paulo@89: if(width > maxTextWidth) maxTextWidth = width; paulo@89: }); paulo@89: rotateLabelsRule = 'rotate(' + rotateLabels + ' 0,' + (textHeight/2 + axis.tickPadding()) + ')'; paulo@89: //Convert to radians before calculating sin. Add 30 to margin for healthy padding. paulo@89: var sin = Math.abs(Math.sin(rotateLabels*Math.PI/180)); paulo@89: xLabelMargin = (sin ? sin*maxTextWidth : maxTextWidth)+30; paulo@89: //Rotate all xTicks paulo@89: xTicks paulo@89: .attr('transform', rotateLabelsRule) paulo@89: .style('text-anchor', rotateLabels%360 > 0 ? 'start' : 'end'); paulo@89: } paulo@89: axisLabel.enter().append('text').attr('class', 'nv-axislabel'); paulo@89: if (scale.range().length < 2) { paulo@89: w = 0; paulo@89: } else if (scale.range().length === 2) { paulo@89: w = scale.range()[1]; paulo@89: } else { paulo@89: w = scale.range()[scale.range().length-1]+(scale.range()[1]-scale.range()[0]); paulo@89: } paulo@89: axisLabel paulo@89: .attr('text-anchor', 'middle') paulo@89: .attr('y', xLabelMargin) paulo@89: .attr('x', w/2); paulo@89: if (showMaxMin) { paulo@89: //if (showMaxMin && !isOrdinal) { paulo@89: axisMaxMin = wrap.selectAll('g.nv-axisMaxMin') paulo@89: //.data(scale.domain()) paulo@89: .data([scale.domain()[0], scale.domain()[scale.domain().length - 1]]); paulo@89: axisMaxMin.enter().append('g').attr('class',function(d,i){ paulo@89: return ['nv-axisMaxMin','nv-axisMaxMin-x',(i == 0 ? 'nv-axisMin-x':'nv-axisMax-x')].join(' ') paulo@89: }).append('text'); paulo@89: axisMaxMin.exit().remove(); paulo@89: axisMaxMin paulo@89: .attr('transform', function(d,i) { paulo@89: return 'translate(' + nv.utils.NaNtoZero((scale(d) + (isOrdinal ? scale.rangeBand() / 2 : 0))) + ',0)' paulo@89: }) paulo@89: .select('text') paulo@89: .attr('dy', '.71em') paulo@89: .attr('y', axis.tickPadding()) paulo@89: .attr('transform', rotateLabelsRule) paulo@89: .style('text-anchor', rotateLabels ? (rotateLabels%360 > 0 ? 'start' : 'end') : 'middle') paulo@89: .text(function(d,i) { paulo@89: var v = fmt(d); paulo@89: return ('' + v).match('NaN') ? '' : v; paulo@89: }); paulo@89: axisMaxMin.watchTransition(renderWatch, 'min-max bottom') paulo@89: .attr('transform', function(d,i) { paulo@89: return 'translate(' + nv.utils.NaNtoZero((scale(d) + (isOrdinal ? scale.rangeBand() / 2 : 0))) + ',0)' paulo@89: }); paulo@89: } paulo@89: if (staggerLabels) paulo@89: xTicks paulo@89: .attr('transform', function(d,i) { paulo@89: return 'translate(0,' + (i % 2 == 0 ? '0' : '12') + ')' paulo@89: }); paulo@89: paulo@89: break; paulo@89: case 'right': paulo@89: axisLabel.enter().append('text').attr('class', 'nv-axislabel'); paulo@89: axisLabel paulo@89: .style('text-anchor', rotateYLabel ? 'middle' : 'begin') paulo@89: .attr('transform', rotateYLabel ? 'rotate(90)' : '') paulo@89: .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 paulo@89: .attr('x', rotateYLabel ? (d3.max(scale.range()) / 2) : axis.tickPadding()); paulo@89: if (showMaxMin) { paulo@89: axisMaxMin = wrap.selectAll('g.nv-axisMaxMin') paulo@89: .data(scale.domain()); paulo@89: axisMaxMin.enter().append('g').attr('class',function(d,i){ paulo@89: return ['nv-axisMaxMin','nv-axisMaxMin-y',(i == 0 ? 'nv-axisMin-y':'nv-axisMax-y')].join(' ') paulo@89: }).append('text') paulo@89: .style('opacity', 0); paulo@89: axisMaxMin.exit().remove(); paulo@89: axisMaxMin paulo@89: .attr('transform', function(d,i) { paulo@89: return 'translate(0,' + nv.utils.NaNtoZero(scale(d)) + ')' paulo@89: }) paulo@89: .select('text') paulo@89: .attr('dy', '.32em') paulo@89: .attr('y', 0) paulo@89: .attr('x', axis.tickPadding()) paulo@89: .style('text-anchor', 'start') paulo@89: .text(function(d, i) { paulo@89: var v = fmt(d); paulo@89: return ('' + v).match('NaN') ? '' : v; paulo@89: }); paulo@89: axisMaxMin.watchTransition(renderWatch, 'min-max right') paulo@89: .attr('transform', function(d,i) { paulo@89: return 'translate(0,' + nv.utils.NaNtoZero(scale.range()[i]) + ')' paulo@89: }) paulo@89: .select('text') paulo@89: .style('opacity', 1); paulo@89: } paulo@89: break; paulo@89: case 'left': paulo@89: /* paulo@89: //For dynamically placing the label. Can be used with dynamically-sized chart axis margins paulo@89: var yTicks = g.selectAll('g').select("text"); paulo@89: yTicks.each(function(d,i){ paulo@89: var labelPadding = this.getBoundingClientRect().width + axis.tickPadding() + 16; paulo@89: if(labelPadding > width) width = labelPadding; paulo@89: }); paulo@89: */ paulo@89: axisLabel.enter().append('text').attr('class', 'nv-axislabel'); paulo@89: axisLabel paulo@89: .style('text-anchor', rotateYLabel ? 'middle' : 'end') paulo@89: .attr('transform', rotateYLabel ? 'rotate(-90)' : '') paulo@89: .attr('y', rotateYLabel ? (-Math.max(margin.left, width) + 25 - (axisLabelDistance || 0)) : -10) paulo@89: .attr('x', rotateYLabel ? (-d3.max(scale.range()) / 2) : -axis.tickPadding()); paulo@89: if (showMaxMin) { paulo@89: axisMaxMin = wrap.selectAll('g.nv-axisMaxMin') paulo@89: .data(scale.domain()); paulo@89: axisMaxMin.enter().append('g').attr('class',function(d,i){ paulo@89: return ['nv-axisMaxMin','nv-axisMaxMin-y',(i == 0 ? 'nv-axisMin-y':'nv-axisMax-y')].join(' ') paulo@89: }).append('text') paulo@89: .style('opacity', 0); paulo@89: axisMaxMin.exit().remove(); paulo@89: axisMaxMin paulo@89: .attr('transform', function(d,i) { paulo@89: return 'translate(0,' + nv.utils.NaNtoZero(scale0(d)) + ')' paulo@89: }) paulo@89: .select('text') paulo@89: .attr('dy', '.32em') paulo@89: .attr('y', 0) paulo@89: .attr('x', -axis.tickPadding()) paulo@89: .attr('text-anchor', 'end') paulo@89: .text(function(d,i) { paulo@89: var v = fmt(d); paulo@89: return ('' + v).match('NaN') ? '' : v; paulo@89: }); paulo@89: axisMaxMin.watchTransition(renderWatch, 'min-max right') paulo@89: .attr('transform', function(d,i) { paulo@89: return 'translate(0,' + nv.utils.NaNtoZero(scale.range()[i]) + ')' paulo@89: }) paulo@89: .select('text') paulo@89: .style('opacity', 1); paulo@89: } paulo@89: break; paulo@89: } paulo@89: axisLabel.text(function(d) { return d }); paulo@89: paulo@89: if (showMaxMin && (axis.orient() === 'left' || axis.orient() === 'right')) { paulo@89: //check if max and min overlap other values, if so, hide the values that overlap paulo@89: g.selectAll('g') // the g's wrapping each tick paulo@89: .each(function(d,i) { paulo@89: d3.select(this).select('text').attr('opacity', 1); paulo@89: 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! paulo@89: if (d > 1e-10 || d < -1e-10) // accounts for minor floating point errors... though could be problematic if the scale is EXTREMELY SMALL paulo@89: d3.select(this).attr('opacity', 0); paulo@89: paulo@89: d3.select(this).select('text').attr('opacity', 0); // Don't remove the ZERO line!! paulo@89: } paulo@89: }); paulo@89: paulo@89: //if Max and Min = 0 only show min, Issue #281 paulo@89: if (scale.domain()[0] == scale.domain()[1] && scale.domain()[0] == 0) { paulo@89: wrap.selectAll('g.nv-axisMaxMin').style('opacity', function (d, i) { paulo@89: return !i ? 1 : 0 paulo@89: }); paulo@89: } paulo@89: } paulo@89: paulo@89: if (showMaxMin && (axis.orient() === 'top' || axis.orient() === 'bottom')) { paulo@89: var maxMinRange = []; paulo@89: wrap.selectAll('g.nv-axisMaxMin') paulo@89: .each(function(d,i) { paulo@89: try { paulo@89: if (i) // i== 1, max position paulo@89: maxMinRange.push(scale(d) - this.getBoundingClientRect().width - 4); //assuming the max and min labels are as wide as the next tick (with an extra 4 pixels just in case) paulo@89: else // i==0, min position paulo@89: maxMinRange.push(scale(d) + this.getBoundingClientRect().width + 4) paulo@89: }catch (err) { paulo@89: if (i) // i== 1, max position paulo@89: 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) paulo@89: else // i==0, min position paulo@89: maxMinRange.push(scale(d) + 4); paulo@89: } paulo@89: }); paulo@89: // the g's wrapping each tick paulo@89: g.selectAll('g').each(function(d, i) { paulo@89: if (scale(d) < maxMinRange[0] || scale(d) > maxMinRange[1]) { paulo@89: if (d > 1e-10 || d < -1e-10) // accounts for minor floating point errors... though could be problematic if the scale is EXTREMELY SMALL paulo@89: d3.select(this).remove(); paulo@89: else paulo@89: d3.select(this).select('text').remove(); // Don't remove the ZERO line!! paulo@89: } paulo@89: }); paulo@89: } paulo@89: paulo@89: //Highlight zero tick line paulo@89: g.selectAll('.tick') paulo@89: .filter(function (d) { paulo@89: /* paulo@89: The filter needs to return only ticks at or near zero. paulo@89: Numbers like 0.00001 need to count as zero as well, paulo@89: and the arithmetic trick below solves that. paulo@89: */ paulo@89: return !parseFloat(Math.round(d * 100000) / 1000000) && (d !== undefined) paulo@89: }) paulo@89: .classed('zero', true); paulo@89: paulo@89: //store old scales for use in transitions on update paulo@89: scale0 = scale.copy(); paulo@89: paulo@89: }); paulo@89: paulo@89: renderWatch.renderEnd('axis immediate'); paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: // expose chart's sub-components paulo@89: chart.axis = axis; paulo@89: chart.dispatch = dispatch; paulo@89: paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: axisLabelDistance: {get: function(){return axisLabelDistance;}, set: function(_){axisLabelDistance=_;}}, paulo@89: staggerLabels: {get: function(){return staggerLabels;}, set: function(_){staggerLabels=_;}}, paulo@89: rotateLabels: {get: function(){return rotateLabels;}, set: function(_){rotateLabels=_;}}, paulo@89: rotateYLabel: {get: function(){return rotateYLabel;}, set: function(_){rotateYLabel=_;}}, paulo@89: showMaxMin: {get: function(){return showMaxMin;}, set: function(_){showMaxMin=_;}}, paulo@89: axisLabel: {get: function(){return axisLabelText;}, set: function(_){axisLabelText=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: ticks: {get: function(){return ticks;}, set: function(_){ticks=_;}}, paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: duration: {get: function(){return duration;}, set: function(_){ paulo@89: duration=_; paulo@89: renderWatch.reset(duration); paulo@89: }}, paulo@89: scale: {get: function(){return scale;}, set: function(_){ paulo@89: scale = _; paulo@89: axis.scale(scale); paulo@89: isOrdinal = typeof scale.rangeBands === 'function'; paulo@89: nv.utils.inheritOptionsD3(chart, scale, ['domain', 'range', 'rangeBand', 'rangeBands']); paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.initOptions(chart); paulo@89: nv.utils.inheritOptionsD3(chart, axis, ['orient', 'tickValues', 'tickSubdivide', 'tickSize', 'tickPadding', 'tickFormat']); paulo@89: nv.utils.inheritOptionsD3(chart, scale, ['domain', 'range', 'rangeBand', 'rangeBands']); paulo@89: paulo@89: return chart; paulo@89: }; paulo@89: nv.models.boxPlot = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var margin = {top: 0, right: 0, bottom: 0, left: 0} paulo@89: , width = 960 paulo@89: , height = 500 paulo@89: , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one paulo@89: , x = d3.scale.ordinal() paulo@89: , y = d3.scale.linear() paulo@89: , getX = function(d) { return d.x } paulo@89: , getY = function(d) { return d.y } paulo@89: , color = nv.utils.defaultColor() paulo@89: , container = null paulo@89: , xDomain paulo@89: , yDomain paulo@89: , xRange paulo@89: , yRange paulo@89: , dispatch = d3.dispatch('elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd') paulo@89: , duration = 250 paulo@89: , maxBoxWidth = null paulo@89: ; paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var x0, y0; paulo@89: var renderWatch = nv.utils.renderWatch(dispatch, duration); paulo@89: paulo@89: function chart(selection) { paulo@89: renderWatch.reset(); paulo@89: selection.each(function(data) { paulo@89: var availableWidth = width - margin.left - margin.right, paulo@89: availableHeight = height - margin.top - margin.bottom; paulo@89: paulo@89: container = d3.select(this); paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: // Setup Scales paulo@89: x .domain(xDomain || data.map(function(d,i) { return getX(d,i); })) paulo@89: .rangeBands(xRange || [0, availableWidth], .1); paulo@89: paulo@89: // if we know yDomain, no need to calculate paulo@89: var yData = [] paulo@89: if (!yDomain) { paulo@89: // (y-range is based on quartiles, whiskers and outliers) paulo@89: paulo@89: // lower values paulo@89: var yMin = d3.min(data.map(function(d) { paulo@89: var min_arr = []; paulo@89: paulo@89: min_arr.push(d.values.Q1); paulo@89: if (d.values.hasOwnProperty('whisker_low') && d.values.whisker_low !== null) { min_arr.push(d.values.whisker_low); } paulo@89: if (d.values.hasOwnProperty('outliers') && d.values.outliers !== null) { min_arr = min_arr.concat(d.values.outliers); } paulo@89: paulo@89: return d3.min(min_arr); paulo@89: })); paulo@89: paulo@89: // upper values paulo@89: var yMax = d3.max(data.map(function(d) { paulo@89: var max_arr = []; paulo@89: paulo@89: max_arr.push(d.values.Q3); paulo@89: if (d.values.hasOwnProperty('whisker_high') && d.values.whisker_high !== null) { max_arr.push(d.values.whisker_high); } paulo@89: if (d.values.hasOwnProperty('outliers') && d.values.outliers !== null) { max_arr = max_arr.concat(d.values.outliers); } paulo@89: paulo@89: return d3.max(max_arr); paulo@89: })); paulo@89: paulo@89: yData = [ yMin, yMax ] ; paulo@89: } paulo@89: paulo@89: y.domain(yDomain || yData); paulo@89: y.range(yRange || [availableHeight, 0]); paulo@89: paulo@89: //store old scales if they exist paulo@89: x0 = x0 || x; paulo@89: y0 = y0 || y.copy().range([y(0),y(0)]); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap').data([data]); paulo@89: var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap'); paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: var boxplots = wrap.selectAll('.nv-boxplot').data(function(d) { return d }); paulo@89: var boxEnter = boxplots.enter().append('g').style('stroke-opacity', 1e-6).style('fill-opacity', 1e-6); paulo@89: boxplots paulo@89: .attr('class', 'nv-boxplot') paulo@89: .attr('transform', function(d,i,j) { return 'translate(' + (x(getX(d,i)) + x.rangeBand() * .05) + ', 0)'; }) paulo@89: .classed('hover', function(d) { return d.hover }); paulo@89: boxplots paulo@89: .watchTransition(renderWatch, 'nv-boxplot: boxplots') paulo@89: .style('stroke-opacity', 1) paulo@89: .style('fill-opacity', .75) paulo@89: .delay(function(d,i) { return i * duration / data.length }) paulo@89: .attr('transform', function(d,i) { paulo@89: return 'translate(' + (x(getX(d,i)) + x.rangeBand() * .05) + ', 0)'; paulo@89: }); paulo@89: boxplots.exit().remove(); paulo@89: paulo@89: // ----- add the SVG elements for each boxPlot ----- paulo@89: paulo@89: // conditionally append whisker lines paulo@89: boxEnter.each(function(d,i) { paulo@89: var box = d3.select(this); paulo@89: paulo@89: ['low', 'high'].forEach(function(key) { paulo@89: if (d.values.hasOwnProperty('whisker_' + key) && d.values['whisker_' + key] !== null) { paulo@89: box.append('line') paulo@89: .style('stroke', (d.color) ? d.color : color(d,i)) paulo@89: .attr('class', 'nv-boxplot-whisker nv-boxplot-' + key); paulo@89: paulo@89: box.append('line') paulo@89: .style('stroke', (d.color) ? d.color : color(d,i)) paulo@89: .attr('class', 'nv-boxplot-tick nv-boxplot-' + key); paulo@89: } paulo@89: }); paulo@89: }); paulo@89: paulo@89: // outliers paulo@89: // TODO: support custom colors here paulo@89: var outliers = boxplots.selectAll('.nv-boxplot-outlier').data(function(d) { paulo@89: if (d.values.hasOwnProperty('outliers') && d.values.outliers !== null) { return d.values.outliers; } paulo@89: else { return []; } paulo@89: }); paulo@89: outliers.enter().append('circle') paulo@89: .style('fill', function(d,i,j) { return color(d,j) }).style('stroke', function(d,i,j) { return color(d,j) }) paulo@89: .on('mouseover', function(d,i,j) { paulo@89: d3.select(this).classed('hover', true); paulo@89: dispatch.elementMouseover({ paulo@89: series: { key: d, color: color(d,j) }, paulo@89: e: d3.event paulo@89: }); paulo@89: }) paulo@89: .on('mouseout', function(d,i,j) { paulo@89: d3.select(this).classed('hover', false); paulo@89: dispatch.elementMouseout({ paulo@89: series: { key: d, color: color(d,j) }, paulo@89: e: d3.event paulo@89: }); paulo@89: }) paulo@89: .on('mousemove', function(d,i) { paulo@89: dispatch.elementMousemove({e: d3.event}); paulo@89: }); paulo@89: paulo@89: outliers.attr('class', 'nv-boxplot-outlier'); paulo@89: outliers paulo@89: .watchTransition(renderWatch, 'nv-boxplot: nv-boxplot-outlier') paulo@89: .attr('cx', x.rangeBand() * .45) paulo@89: .attr('cy', function(d,i,j) { return y(d); }) paulo@89: .attr('r', '3'); paulo@89: outliers.exit().remove(); paulo@89: paulo@89: var box_width = function() { return (maxBoxWidth === null ? x.rangeBand() * .9 : Math.min(75, x.rangeBand() * .9)); }; paulo@89: var box_left = function() { return x.rangeBand() * .45 - box_width()/2; }; paulo@89: var box_right = function() { return x.rangeBand() * .45 + box_width()/2; }; paulo@89: paulo@89: // update whisker lines and ticks paulo@89: ['low', 'high'].forEach(function(key) { paulo@89: var endpoint = (key === 'low') ? 'Q1' : 'Q3'; paulo@89: paulo@89: boxplots.select('line.nv-boxplot-whisker.nv-boxplot-' + key) paulo@89: .watchTransition(renderWatch, 'nv-boxplot: boxplots') paulo@89: .attr('x1', x.rangeBand() * .45 ) paulo@89: .attr('y1', function(d,i) { return y(d.values['whisker_' + key]); }) paulo@89: .attr('x2', x.rangeBand() * .45 ) paulo@89: .attr('y2', function(d,i) { return y(d.values[endpoint]); }); paulo@89: paulo@89: boxplots.select('line.nv-boxplot-tick.nv-boxplot-' + key) paulo@89: .watchTransition(renderWatch, 'nv-boxplot: boxplots') paulo@89: .attr('x1', box_left ) paulo@89: .attr('y1', function(d,i) { return y(d.values['whisker_' + key]); }) paulo@89: .attr('x2', box_right ) paulo@89: .attr('y2', function(d,i) { return y(d.values['whisker_' + key]); }); paulo@89: }); paulo@89: paulo@89: ['low', 'high'].forEach(function(key) { paulo@89: boxEnter.selectAll('.nv-boxplot-' + key) paulo@89: .on('mouseover', function(d,i,j) { paulo@89: d3.select(this).classed('hover', true); paulo@89: dispatch.elementMouseover({ paulo@89: series: { key: d.values['whisker_' + key], color: color(d,j) }, paulo@89: e: d3.event paulo@89: }); paulo@89: }) paulo@89: .on('mouseout', function(d,i,j) { paulo@89: d3.select(this).classed('hover', false); paulo@89: dispatch.elementMouseout({ paulo@89: series: { key: d.values['whisker_' + key], color: color(d,j) }, paulo@89: e: d3.event paulo@89: }); paulo@89: }) paulo@89: .on('mousemove', function(d,i) { paulo@89: dispatch.elementMousemove({e: d3.event}); paulo@89: }); paulo@89: }); paulo@89: paulo@89: // boxes paulo@89: boxEnter.append('rect') paulo@89: .attr('class', 'nv-boxplot-box') paulo@89: // tooltip events paulo@89: .on('mouseover', function(d,i) { paulo@89: d3.select(this).classed('hover', true); paulo@89: dispatch.elementMouseover({ paulo@89: key: d.label, paulo@89: value: d.label, paulo@89: series: [ paulo@89: { key: 'Q3', value: d.values.Q3, color: d.color || color(d,i) }, paulo@89: { key: 'Q2', value: d.values.Q2, color: d.color || color(d,i) }, paulo@89: { key: 'Q1', value: d.values.Q1, color: d.color || color(d,i) } paulo@89: ], paulo@89: data: d, paulo@89: index: i, paulo@89: e: d3.event paulo@89: }); paulo@89: }) paulo@89: .on('mouseout', function(d,i) { paulo@89: d3.select(this).classed('hover', false); paulo@89: dispatch.elementMouseout({ paulo@89: key: d.label, paulo@89: value: d.label, paulo@89: series: [ paulo@89: { key: 'Q3', value: d.values.Q3, color: d.color || color(d,i) }, paulo@89: { key: 'Q2', value: d.values.Q2, color: d.color || color(d,i) }, paulo@89: { key: 'Q1', value: d.values.Q1, color: d.color || color(d,i) } paulo@89: ], paulo@89: data: d, paulo@89: index: i, paulo@89: e: d3.event paulo@89: }); paulo@89: }) paulo@89: .on('mousemove', function(d,i) { paulo@89: dispatch.elementMousemove({e: d3.event}); paulo@89: }); paulo@89: paulo@89: // box transitions paulo@89: boxplots.select('rect.nv-boxplot-box') paulo@89: .watchTransition(renderWatch, 'nv-boxplot: boxes') paulo@89: .attr('y', function(d,i) { return y(d.values.Q3); }) paulo@89: .attr('width', box_width) paulo@89: .attr('x', box_left ) paulo@89: paulo@89: .attr('height', function(d,i) { return Math.abs(y(d.values.Q3) - y(d.values.Q1)) || 1 }) paulo@89: .style('fill', function(d,i) { return d.color || color(d,i) }) paulo@89: .style('stroke', function(d,i) { return d.color || color(d,i) }); paulo@89: paulo@89: // median line paulo@89: boxEnter.append('line').attr('class', 'nv-boxplot-median'); paulo@89: paulo@89: boxplots.select('line.nv-boxplot-median') paulo@89: .watchTransition(renderWatch, 'nv-boxplot: boxplots line') paulo@89: .attr('x1', box_left) paulo@89: .attr('y1', function(d,i) { return y(d.values.Q2); }) paulo@89: .attr('x2', box_right) paulo@89: .attr('y2', function(d,i) { return y(d.values.Q2); }); paulo@89: paulo@89: //store old scales for use in transitions on update paulo@89: x0 = x.copy(); paulo@89: y0 = y.copy(); paulo@89: }); paulo@89: paulo@89: renderWatch.renderEnd('nv-boxplot immediate'); paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: chart.dispatch = dispatch; paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: maxBoxWidth: {get: function(){return maxBoxWidth;}, set: function(_){maxBoxWidth=_;}}, paulo@89: x: {get: function(){return getX;}, set: function(_){getX=_;}}, paulo@89: y: {get: function(){return getY;}, set: function(_){getY=_;}}, paulo@89: xScale: {get: function(){return x;}, set: function(_){x=_;}}, paulo@89: yScale: {get: function(){return y;}, set: function(_){y=_;}}, paulo@89: xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}}, paulo@89: yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}}, paulo@89: xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}}, paulo@89: yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}}, paulo@89: id: {get: function(){return id;}, set: function(_){id=_;}}, paulo@89: // rectClass: {get: function(){return rectClass;}, set: function(_){rectClass=_;}}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: }}, paulo@89: duration: {get: function(){return duration;}, set: function(_){ paulo@89: duration = _; paulo@89: renderWatch.reset(duration); paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.initOptions(chart); paulo@89: paulo@89: return chart; paulo@89: }; paulo@89: nv.models.boxPlotChart = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var boxplot = nv.models.boxPlot() paulo@89: , xAxis = nv.models.axis() paulo@89: , yAxis = nv.models.axis() paulo@89: ; paulo@89: paulo@89: var margin = {top: 15, right: 10, bottom: 50, left: 60} paulo@89: , width = null paulo@89: , height = null paulo@89: , color = nv.utils.getColor() paulo@89: , showXAxis = true paulo@89: , showYAxis = true paulo@89: , rightAlignYAxis = false paulo@89: , staggerLabels = false paulo@89: , tooltip = nv.models.tooltip() paulo@89: , x paulo@89: , y paulo@89: , noData = "No Data Available." paulo@89: , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'beforeUpdate', 'renderEnd') paulo@89: , duration = 250 paulo@89: ; paulo@89: paulo@89: xAxis paulo@89: .orient('bottom') paulo@89: .showMaxMin(false) paulo@89: .tickFormat(function(d) { return d }) paulo@89: ; paulo@89: yAxis paulo@89: .orient((rightAlignYAxis) ? 'right' : 'left') paulo@89: .tickFormat(d3.format(',.1f')) paulo@89: ; paulo@89: paulo@89: tooltip.duration(0); paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var renderWatch = nv.utils.renderWatch(dispatch, duration); paulo@89: paulo@89: function chart(selection) { paulo@89: renderWatch.reset(); paulo@89: renderWatch.models(boxplot); paulo@89: if (showXAxis) renderWatch.models(xAxis); paulo@89: if (showYAxis) renderWatch.models(yAxis); paulo@89: paulo@89: selection.each(function(data) { paulo@89: var container = d3.select(this), paulo@89: that = this; paulo@89: nv.utils.initSVG(container); paulo@89: var availableWidth = (width || parseInt(container.style('width')) || 960) paulo@89: - margin.left - margin.right, paulo@89: availableHeight = (height || parseInt(container.style('height')) || 400) paulo@89: - margin.top - margin.bottom; paulo@89: paulo@89: chart.update = function() { paulo@89: dispatch.beforeUpdate(); paulo@89: container.transition().duration(duration).call(chart); paulo@89: }; paulo@89: chart.container = this; paulo@89: paulo@89: // Display No Data message if there's nothing to show. (quartiles required at minimum) paulo@89: if (!data || !data.length || paulo@89: !data.filter(function(d) { return d.values.hasOwnProperty("Q1") && d.values.hasOwnProperty("Q2") && d.values.hasOwnProperty("Q3"); }).length) { paulo@89: var noDataText = container.selectAll('.nv-noData').data([noData]); paulo@89: paulo@89: noDataText.enter().append('text') paulo@89: .attr('class', 'nvd3 nv-noData') paulo@89: .attr('dy', '-.7em') paulo@89: .style('text-anchor', 'middle'); paulo@89: paulo@89: noDataText paulo@89: .attr('x', margin.left + availableWidth / 2) paulo@89: .attr('y', margin.top + availableHeight / 2) paulo@89: .text(function(d) { return d }); paulo@89: paulo@89: return chart; paulo@89: } else { paulo@89: container.selectAll('.nv-noData').remove(); paulo@89: } paulo@89: paulo@89: // Setup Scales paulo@89: x = boxplot.xScale(); paulo@89: y = boxplot.yScale().clamp(true); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-boxPlotWithAxes').data([data]); paulo@89: var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-boxPlotWithAxes').append('g'); paulo@89: var defsEnter = gEnter.append('defs'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-x nv-axis'); paulo@89: gEnter.append('g').attr('class', 'nv-y nv-axis') paulo@89: .append('g').attr('class', 'nv-zeroLine') paulo@89: .append('line'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-barsWrap'); paulo@89: paulo@89: g.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: if (rightAlignYAxis) { paulo@89: g.select(".nv-y.nv-axis") paulo@89: .attr("transform", "translate(" + availableWidth + ",0)"); paulo@89: } paulo@89: paulo@89: // Main Chart Component(s) paulo@89: boxplot paulo@89: .width(availableWidth) paulo@89: .height(availableHeight); paulo@89: paulo@89: var barsWrap = g.select('.nv-barsWrap') paulo@89: .datum(data.filter(function(d) { return !d.disabled })) paulo@89: paulo@89: barsWrap.transition().call(boxplot); paulo@89: paulo@89: paulo@89: defsEnter.append('clipPath') paulo@89: .attr('id', 'nv-x-label-clip-' + boxplot.id()) paulo@89: .append('rect'); paulo@89: paulo@89: g.select('#nv-x-label-clip-' + boxplot.id() + ' rect') paulo@89: .attr('width', x.rangeBand() * (staggerLabels ? 2 : 1)) paulo@89: .attr('height', 16) paulo@89: .attr('x', -x.rangeBand() / (staggerLabels ? 1 : 2 )); paulo@89: paulo@89: // Setup Axes paulo@89: if (showXAxis) { paulo@89: xAxis paulo@89: .scale(x) paulo@89: .ticks( nv.utils.calcTicksX(availableWidth/100, data) ) paulo@89: .tickSize(-availableHeight, 0); paulo@89: paulo@89: g.select('.nv-x.nv-axis').attr('transform', 'translate(0,' + y.range()[0] + ')'); paulo@89: g.select('.nv-x.nv-axis').call(xAxis); paulo@89: paulo@89: var xTicks = g.select('.nv-x.nv-axis').selectAll('g'); paulo@89: if (staggerLabels) { paulo@89: xTicks paulo@89: .selectAll('text') paulo@89: .attr('transform', function(d,i,j) { return 'translate(0,' + (j % 2 == 0 ? '5' : '17') + ')' }) paulo@89: } paulo@89: } paulo@89: paulo@89: if (showYAxis) { paulo@89: yAxis paulo@89: .scale(y) paulo@89: .ticks( Math.floor(availableHeight/36) ) // can't use nv.utils.calcTicksY with Object data paulo@89: .tickSize( -availableWidth, 0); paulo@89: paulo@89: g.select('.nv-y.nv-axis').call(yAxis); paulo@89: } paulo@89: paulo@89: // Zero line paulo@89: g.select(".nv-zeroLine line") paulo@89: .attr("x1",0) paulo@89: .attr("x2",availableWidth) paulo@89: .attr("y1", y(0)) paulo@89: .attr("y2", y(0)) paulo@89: ; paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (in chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: }); paulo@89: paulo@89: renderWatch.renderEnd('nv-boxplot chart immediate'); paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (out of chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: boxplot.dispatch.on('elementMouseover.tooltip', function(evt) { paulo@89: tooltip.data(evt).hidden(false); paulo@89: }); paulo@89: paulo@89: boxplot.dispatch.on('elementMouseout.tooltip', function(evt) { paulo@89: tooltip.data(evt).hidden(true); paulo@89: }); paulo@89: paulo@89: boxplot.dispatch.on('elementMousemove.tooltip', function(evt) { paulo@89: tooltip.position({top: d3.event.pageY, left: d3.event.pageX})(); paulo@89: }); paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: chart.dispatch = dispatch; paulo@89: chart.boxplot = boxplot; paulo@89: chart.xAxis = xAxis; paulo@89: chart.yAxis = yAxis; paulo@89: chart.tooltip = tooltip; paulo@89: paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: staggerLabels: {get: function(){return staggerLabels;}, set: function(_){staggerLabels=_;}}, paulo@89: showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}}, paulo@89: showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}}, paulo@89: tooltips: {get: function(){return tooltips;}, set: function(_){tooltips=_;}}, paulo@89: tooltipContent: {get: function(){return tooltip;}, set: function(_){tooltip=_;}}, paulo@89: noData: {get: function(){return noData;}, set: function(_){noData=_;}}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: duration: {get: function(){return duration;}, set: function(_){ paulo@89: duration = _; paulo@89: renderWatch.reset(duration); paulo@89: boxplot.duration(duration); paulo@89: xAxis.duration(duration); paulo@89: yAxis.duration(duration); paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: boxplot.color(color); paulo@89: }}, paulo@89: rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){ paulo@89: rightAlignYAxis = _; paulo@89: yAxis.orient( (_) ? 'right' : 'left'); paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.inheritOptions(chart, boxplot); paulo@89: nv.utils.initOptions(chart); paulo@89: paulo@89: return chart; paulo@89: } paulo@89: // Chart design based on the recommendations of Stephen Few. Implementation paulo@89: // based on the work of Clint Ivy, Jamie Love, and Jason Davies. paulo@89: // http://projects.instantcognition.com/protovis/bulletchart/ paulo@89: paulo@89: nv.models.bullet = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var margin = {top: 0, right: 0, bottom: 0, left: 0} paulo@89: , orient = 'left' // TODO top & bottom paulo@89: , reverse = false paulo@89: , ranges = function(d) { return d.ranges } paulo@89: , markers = function(d) { return d.markers ? d.markers : [0] } paulo@89: , measures = function(d) { return d.measures } paulo@89: , rangeLabels = function(d) { return d.rangeLabels ? d.rangeLabels : [] } paulo@89: , markerLabels = function(d) { return d.markerLabels ? d.markerLabels : [] } paulo@89: , measureLabels = function(d) { return d.measureLabels ? d.measureLabels : [] } paulo@89: , forceX = [0] // List of numbers to Force into the X scale (ie. 0, or a max / min, etc.) paulo@89: , width = 380 paulo@89: , height = 30 paulo@89: , container = null paulo@89: , tickFormat = null paulo@89: , color = nv.utils.getColor(['#1f77b4']) paulo@89: , dispatch = d3.dispatch('elementMouseover', 'elementMouseout', 'elementMousemove') paulo@89: ; paulo@89: paulo@89: function chart(selection) { paulo@89: selection.each(function(d, i) { paulo@89: var availableWidth = width - margin.left - margin.right, paulo@89: availableHeight = height - margin.top - margin.bottom; paulo@89: paulo@89: container = d3.select(this); paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: var rangez = ranges.call(this, d, i).slice().sort(d3.descending), paulo@89: markerz = markers.call(this, d, i).slice().sort(d3.descending), paulo@89: measurez = measures.call(this, d, i).slice().sort(d3.descending), paulo@89: rangeLabelz = rangeLabels.call(this, d, i).slice(), paulo@89: markerLabelz = markerLabels.call(this, d, i).slice(), paulo@89: measureLabelz = measureLabels.call(this, d, i).slice(); paulo@89: paulo@89: // Setup Scales paulo@89: // Compute the new x-scale. paulo@89: var x1 = d3.scale.linear() paulo@89: .domain( d3.extent(d3.merge([forceX, rangez])) ) paulo@89: .range(reverse ? [availableWidth, 0] : [0, availableWidth]); paulo@89: paulo@89: // Retrieve the old x-scale, if this is an update. paulo@89: var x0 = this.__chart__ || d3.scale.linear() paulo@89: .domain([0, Infinity]) paulo@89: .range(x1.range()); paulo@89: paulo@89: // Stash the new scale. paulo@89: this.__chart__ = x1; paulo@89: paulo@89: var rangeMin = d3.min(rangez), //rangez[2] paulo@89: rangeMax = d3.max(rangez), //rangez[0] paulo@89: rangeAvg = rangez[1]; paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-bullet').data([d]); paulo@89: var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-bullet'); paulo@89: var gEnter = wrapEnter.append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append('rect').attr('class', 'nv-range nv-rangeMax'); paulo@89: gEnter.append('rect').attr('class', 'nv-range nv-rangeAvg'); paulo@89: gEnter.append('rect').attr('class', 'nv-range nv-rangeMin'); paulo@89: gEnter.append('rect').attr('class', 'nv-measure'); paulo@89: paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: var w0 = function(d) { return Math.abs(x0(d) - x0(0)) }, // TODO: could optimize by precalculating x0(0) and x1(0) paulo@89: w1 = function(d) { return Math.abs(x1(d) - x1(0)) }; paulo@89: var xp0 = function(d) { return d < 0 ? x0(d) : x0(0) }, paulo@89: xp1 = function(d) { return d < 0 ? x1(d) : x1(0) }; paulo@89: paulo@89: g.select('rect.nv-rangeMax') paulo@89: .attr('height', availableHeight) paulo@89: .attr('width', w1(rangeMax > 0 ? rangeMax : rangeMin)) paulo@89: .attr('x', xp1(rangeMax > 0 ? rangeMax : rangeMin)) paulo@89: .datum(rangeMax > 0 ? rangeMax : rangeMin) paulo@89: paulo@89: g.select('rect.nv-rangeAvg') paulo@89: .attr('height', availableHeight) paulo@89: .attr('width', w1(rangeAvg)) paulo@89: .attr('x', xp1(rangeAvg)) paulo@89: .datum(rangeAvg) paulo@89: paulo@89: g.select('rect.nv-rangeMin') paulo@89: .attr('height', availableHeight) paulo@89: .attr('width', w1(rangeMax)) paulo@89: .attr('x', xp1(rangeMax)) paulo@89: .attr('width', w1(rangeMax > 0 ? rangeMin : rangeMax)) paulo@89: .attr('x', xp1(rangeMax > 0 ? rangeMin : rangeMax)) paulo@89: .datum(rangeMax > 0 ? rangeMin : rangeMax) paulo@89: paulo@89: g.select('rect.nv-measure') paulo@89: .style('fill', color) paulo@89: .attr('height', availableHeight / 3) paulo@89: .attr('y', availableHeight / 3) paulo@89: .attr('width', measurez < 0 ? paulo@89: x1(0) - x1(measurez[0]) paulo@89: : x1(measurez[0]) - x1(0)) paulo@89: .attr('x', xp1(measurez)) paulo@89: .on('mouseover', function() { paulo@89: dispatch.elementMouseover({ paulo@89: value: measurez[0], paulo@89: label: measureLabelz[0] || 'Current', paulo@89: color: d3.select(this).style("fill") paulo@89: }) paulo@89: }) paulo@89: .on('mousemove', function() { paulo@89: dispatch.elementMousemove({ paulo@89: value: measurez[0], paulo@89: label: measureLabelz[0] || 'Current', paulo@89: color: d3.select(this).style("fill") paulo@89: }) paulo@89: }) paulo@89: .on('mouseout', function() { paulo@89: dispatch.elementMouseout({ paulo@89: value: measurez[0], paulo@89: label: measureLabelz[0] || 'Current', paulo@89: color: d3.select(this).style("fill") paulo@89: }) paulo@89: }); paulo@89: paulo@89: var h3 = availableHeight / 6; paulo@89: paulo@89: var markerData = markerz.map( function(marker, index) { paulo@89: return {value: marker, label: markerLabelz[index]} paulo@89: }); paulo@89: gEnter paulo@89: .selectAll("path.nv-markerTriangle") paulo@89: .data(markerData) paulo@89: .enter() paulo@89: .append('path') paulo@89: .attr('class', 'nv-markerTriangle') paulo@89: .attr('transform', function(d) { return 'translate(' + x1(d.value) + ',' + (availableHeight / 2) + ')' }) paulo@89: .attr('d', 'M0,' + h3 + 'L' + h3 + ',' + (-h3) + ' ' + (-h3) + ',' + (-h3) + 'Z') paulo@89: .on('mouseover', function(d) { paulo@89: dispatch.elementMouseover({ paulo@89: value: d.value, paulo@89: label: d.label || 'Previous', paulo@89: color: d3.select(this).style("fill"), paulo@89: pos: [x1(d.value), availableHeight/2] paulo@89: }) paulo@89: paulo@89: }) paulo@89: .on('mousemove', function(d) { paulo@89: dispatch.elementMousemove({ paulo@89: value: d.value, paulo@89: label: d.label || 'Previous', paulo@89: color: d3.select(this).style("fill") paulo@89: }) paulo@89: }) paulo@89: .on('mouseout', function(d, i) { paulo@89: dispatch.elementMouseout({ paulo@89: value: d.value, paulo@89: label: d.label || 'Previous', paulo@89: color: d3.select(this).style("fill") paulo@89: }) paulo@89: }); paulo@89: paulo@89: wrap.selectAll('.nv-range') paulo@89: .on('mouseover', function(d,i) { paulo@89: var label = rangeLabelz[i] || (!i ? "Maximum" : i == 1 ? "Mean" : "Minimum"); paulo@89: dispatch.elementMouseover({ paulo@89: value: d, paulo@89: label: label, paulo@89: color: d3.select(this).style("fill") paulo@89: }) paulo@89: }) paulo@89: .on('mousemove', function() { paulo@89: dispatch.elementMousemove({ paulo@89: value: measurez[0], paulo@89: label: measureLabelz[0] || 'Previous', paulo@89: color: d3.select(this).style("fill") paulo@89: }) paulo@89: }) paulo@89: .on('mouseout', function(d,i) { paulo@89: var label = rangeLabelz[i] || (!i ? "Maximum" : i == 1 ? "Mean" : "Minimum"); paulo@89: dispatch.elementMouseout({ paulo@89: value: d, paulo@89: label: label, paulo@89: color: d3.select(this).style("fill") paulo@89: }) paulo@89: }); paulo@89: }); paulo@89: paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: chart.dispatch = dispatch; paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: ranges: {get: function(){return ranges;}, set: function(_){ranges=_;}}, // ranges (bad, satisfactory, good) paulo@89: markers: {get: function(){return markers;}, set: function(_){markers=_;}}, // markers (previous, goal) paulo@89: measures: {get: function(){return measures;}, set: function(_){measures=_;}}, // measures (actual, forecast) paulo@89: forceX: {get: function(){return forceX;}, set: function(_){forceX=_;}}, paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: tickFormat: {get: function(){return tickFormat;}, set: function(_){tickFormat=_;}}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: orient: {get: function(){return orient;}, set: function(_){ // left, right, top, bottom paulo@89: orient = _; paulo@89: reverse = orient == 'right' || orient == 'bottom'; paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.initOptions(chart); paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: paulo@89: paulo@89: // Chart design based on the recommendations of Stephen Few. Implementation paulo@89: // based on the work of Clint Ivy, Jamie Love, and Jason Davies. paulo@89: // http://projects.instantcognition.com/protovis/bulletchart/ paulo@89: nv.models.bulletChart = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var bullet = nv.models.bullet(); paulo@89: var tooltip = nv.models.tooltip(); paulo@89: paulo@89: var orient = 'left' // TODO top & bottom paulo@89: , reverse = false paulo@89: , margin = {top: 5, right: 40, bottom: 20, left: 120} paulo@89: , ranges = function(d) { return d.ranges } paulo@89: , markers = function(d) { return d.markers ? d.markers : [0] } paulo@89: , measures = function(d) { return d.measures } paulo@89: , width = null paulo@89: , height = 55 paulo@89: , tickFormat = null paulo@89: , ticks = null paulo@89: , noData = null paulo@89: , dispatch = d3.dispatch('tooltipShow', 'tooltipHide') paulo@89: ; paulo@89: paulo@89: tooltip.duration(0).headerEnabled(false); paulo@89: paulo@89: function chart(selection) { paulo@89: selection.each(function(d, i) { paulo@89: var container = d3.select(this); paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: var availableWidth = nv.utils.availableWidth(width, container, margin), paulo@89: availableHeight = height - margin.top - margin.bottom, paulo@89: that = this; paulo@89: paulo@89: chart.update = function() { chart(selection) }; paulo@89: chart.container = this; paulo@89: paulo@89: // Display No Data message if there's nothing to show. paulo@89: if (!d || !ranges.call(this, d, i)) { paulo@89: nv.utils.noData(chart, container) paulo@89: return chart; paulo@89: } else { paulo@89: container.selectAll('.nv-noData').remove(); paulo@89: } paulo@89: paulo@89: var rangez = ranges.call(this, d, i).slice().sort(d3.descending), paulo@89: markerz = markers.call(this, d, i).slice().sort(d3.descending), paulo@89: measurez = measures.call(this, d, i).slice().sort(d3.descending); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-bulletChart').data([d]); paulo@89: var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-bulletChart'); paulo@89: var gEnter = wrapEnter.append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-bulletWrap'); paulo@89: gEnter.append('g').attr('class', 'nv-titles'); paulo@89: paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: // Compute the new x-scale. paulo@89: var x1 = d3.scale.linear() paulo@89: .domain([0, Math.max(rangez[0], markerz[0], measurez[0])]) // TODO: need to allow forceX and forceY, and xDomain, yDomain paulo@89: .range(reverse ? [availableWidth, 0] : [0, availableWidth]); paulo@89: paulo@89: // Retrieve the old x-scale, if this is an update. paulo@89: var x0 = this.__chart__ || d3.scale.linear() paulo@89: .domain([0, Infinity]) paulo@89: .range(x1.range()); paulo@89: paulo@89: // Stash the new scale. paulo@89: this.__chart__ = x1; paulo@89: paulo@89: var w0 = function(d) { return Math.abs(x0(d) - x0(0)) }, // TODO: could optimize by precalculating x0(0) and x1(0) paulo@89: w1 = function(d) { return Math.abs(x1(d) - x1(0)) }; paulo@89: paulo@89: var title = gEnter.select('.nv-titles').append('g') paulo@89: .attr('text-anchor', 'end') paulo@89: .attr('transform', 'translate(-6,' + (height - margin.top - margin.bottom) / 2 + ')'); paulo@89: title.append('text') paulo@89: .attr('class', 'nv-title') paulo@89: .text(function(d) { return d.title; }); paulo@89: paulo@89: title.append('text') paulo@89: .attr('class', 'nv-subtitle') paulo@89: .attr('dy', '1em') paulo@89: .text(function(d) { return d.subtitle; }); paulo@89: paulo@89: bullet paulo@89: .width(availableWidth) paulo@89: .height(availableHeight) paulo@89: paulo@89: var bulletWrap = g.select('.nv-bulletWrap'); paulo@89: d3.transition(bulletWrap).call(bullet); paulo@89: paulo@89: // Compute the tick format. paulo@89: var format = tickFormat || x1.tickFormat( availableWidth / 100 ); paulo@89: paulo@89: // Update the tick groups. paulo@89: var tick = g.selectAll('g.nv-tick') paulo@89: .data(x1.ticks( ticks ? ticks : (availableWidth / 50) ), function(d) { paulo@89: return this.textContent || format(d); paulo@89: }); paulo@89: paulo@89: // Initialize the ticks with the old scale, x0. paulo@89: var tickEnter = tick.enter().append('g') paulo@89: .attr('class', 'nv-tick') paulo@89: .attr('transform', function(d) { return 'translate(' + x0(d) + ',0)' }) paulo@89: .style('opacity', 1e-6); paulo@89: paulo@89: tickEnter.append('line') paulo@89: .attr('y1', availableHeight) paulo@89: .attr('y2', availableHeight * 7 / 6); paulo@89: paulo@89: tickEnter.append('text') paulo@89: .attr('text-anchor', 'middle') paulo@89: .attr('dy', '1em') paulo@89: .attr('y', availableHeight * 7 / 6) paulo@89: .text(format); paulo@89: paulo@89: // Transition the updating ticks to the new scale, x1. paulo@89: var tickUpdate = d3.transition(tick) paulo@89: .attr('transform', function(d) { return 'translate(' + x1(d) + ',0)' }) paulo@89: .style('opacity', 1); paulo@89: paulo@89: tickUpdate.select('line') paulo@89: .attr('y1', availableHeight) paulo@89: .attr('y2', availableHeight * 7 / 6); paulo@89: paulo@89: tickUpdate.select('text') paulo@89: .attr('y', availableHeight * 7 / 6); paulo@89: paulo@89: // Transition the exiting ticks to the new scale, x1. paulo@89: d3.transition(tick.exit()) paulo@89: .attr('transform', function(d) { return 'translate(' + x1(d) + ',0)' }) paulo@89: .style('opacity', 1e-6) paulo@89: .remove(); paulo@89: }); paulo@89: paulo@89: d3.timer.flush(); paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (out of chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: bullet.dispatch.on('elementMouseover.tooltip', function(evt) { paulo@89: evt['series'] = { paulo@89: key: evt.label, paulo@89: value: evt.value, paulo@89: color: evt.color paulo@89: }; paulo@89: tooltip.data(evt).hidden(false); paulo@89: }); paulo@89: paulo@89: bullet.dispatch.on('elementMouseout.tooltip', function(evt) { paulo@89: tooltip.hidden(true); paulo@89: }); paulo@89: paulo@89: bullet.dispatch.on('elementMousemove.tooltip', function(evt) { paulo@89: tooltip.position({top: d3.event.pageY, left: d3.event.pageX})(); paulo@89: }); paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: chart.bullet = bullet; paulo@89: chart.dispatch = dispatch; paulo@89: chart.tooltip = tooltip; paulo@89: paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: ranges: {get: function(){return ranges;}, set: function(_){ranges=_;}}, // ranges (bad, satisfactory, good) paulo@89: markers: {get: function(){return markers;}, set: function(_){markers=_;}}, // markers (previous, goal) paulo@89: measures: {get: function(){return measures;}, set: function(_){measures=_;}}, // measures (actual, forecast) paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: tickFormat: {get: function(){return tickFormat;}, set: function(_){tickFormat=_;}}, paulo@89: ticks: {get: function(){return ticks;}, set: function(_){ticks=_;}}, paulo@89: noData: {get: function(){return noData;}, set: function(_){noData=_;}}, paulo@89: paulo@89: // deprecated options paulo@89: tooltips: {get: function(){return tooltip.enabled();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead'); paulo@89: tooltip.enabled(!!_); paulo@89: }}, paulo@89: tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead'); paulo@89: tooltip.contentGenerator(_); paulo@89: }}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: orient: {get: function(){return orient;}, set: function(_){ // left, right, top, bottom paulo@89: orient = _; paulo@89: reverse = orient == 'right' || orient == 'bottom'; paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.inheritOptions(chart, bullet); paulo@89: nv.utils.initOptions(chart); paulo@89: paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: paulo@89: paulo@89: nv.models.candlestickBar = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var margin = {top: 0, right: 0, bottom: 0, left: 0} paulo@89: , width = null paulo@89: , height = null paulo@89: , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one paulo@89: , container paulo@89: , x = d3.scale.linear() paulo@89: , y = d3.scale.linear() paulo@89: , getX = function(d) { return d.x } paulo@89: , getY = function(d) { return d.y } paulo@89: , getOpen = function(d) { return d.open } paulo@89: , getClose = function(d) { return d.close } paulo@89: , getHigh = function(d) { return d.high } paulo@89: , getLow = function(d) { return d.low } paulo@89: , forceX = [] paulo@89: , forceY = [] paulo@89: , padData = false // If true, adds half a data points width to front and back, for lining up a line chart with a bar chart paulo@89: , clipEdge = true paulo@89: , color = nv.utils.defaultColor() paulo@89: , interactive = false paulo@89: , xDomain paulo@89: , yDomain paulo@89: , xRange paulo@89: , yRange paulo@89: , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'stateChange', 'changeState', 'renderEnd', 'chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'elementMousemove') paulo@89: ; paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: function chart(selection) { paulo@89: selection.each(function(data) { paulo@89: container = d3.select(this); paulo@89: var availableWidth = nv.utils.availableWidth(width, container, margin), paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: // Width of the candlestick bars. paulo@89: var barWidth = (availableWidth / data[0].values.length) * .45; paulo@89: paulo@89: // Setup Scales paulo@89: x.domain(xDomain || d3.extent(data[0].values.map(getX).concat(forceX) )); paulo@89: paulo@89: if (padData) paulo@89: x.range(xRange || [availableWidth * .5 / data[0].values.length, availableWidth * (data[0].values.length - .5) / data[0].values.length ]); paulo@89: else paulo@89: x.range(xRange || [5 + barWidth / 2, availableWidth - barWidth / 2 - 5]); paulo@89: paulo@89: y.domain(yDomain || [ paulo@89: d3.min(data[0].values.map(getLow).concat(forceY)), paulo@89: d3.max(data[0].values.map(getHigh).concat(forceY)) paulo@89: ] paulo@89: ).range(yRange || [availableHeight, 0]); paulo@89: paulo@89: // If scale's domain don't have a range, slightly adjust to make one... so a chart can show a single data point paulo@89: if (x.domain()[0] === x.domain()[1]) paulo@89: x.domain()[0] ? paulo@89: x.domain([x.domain()[0] - x.domain()[0] * 0.01, x.domain()[1] + x.domain()[1] * 0.01]) paulo@89: : x.domain([-1,1]); paulo@89: paulo@89: if (y.domain()[0] === y.domain()[1]) paulo@89: y.domain()[0] ? paulo@89: y.domain([y.domain()[0] + y.domain()[0] * 0.01, y.domain()[1] - y.domain()[1] * 0.01]) paulo@89: : y.domain([-1,1]); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = d3.select(this).selectAll('g.nv-wrap.nv-candlestickBar').data([data[0].values]); paulo@89: var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-candlestickBar'); paulo@89: var defsEnter = wrapEnter.append('defs'); paulo@89: var gEnter = wrapEnter.append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-ticks'); paulo@89: paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: container paulo@89: .on('click', function(d,i) { paulo@89: dispatch.chartClick({ paulo@89: data: d, paulo@89: index: i, paulo@89: pos: d3.event, paulo@89: id: id paulo@89: }); paulo@89: }); paulo@89: paulo@89: defsEnter.append('clipPath') paulo@89: .attr('id', 'nv-chart-clip-path-' + id) paulo@89: .append('rect'); paulo@89: paulo@89: wrap.select('#nv-chart-clip-path-' + id + ' rect') paulo@89: .attr('width', availableWidth) paulo@89: .attr('height', availableHeight); paulo@89: paulo@89: g .attr('clip-path', clipEdge ? 'url(#nv-chart-clip-path-' + id + ')' : ''); paulo@89: paulo@89: var ticks = wrap.select('.nv-ticks').selectAll('.nv-tick') paulo@89: .data(function(d) { return d }); paulo@89: ticks.exit().remove(); paulo@89: paulo@89: // The colors are currently controlled by CSS. paulo@89: var tickGroups = ticks.enter().append('g') paulo@89: .attr('class', function(d, i, j) { return (getOpen(d, i) > getClose(d, i) ? 'nv-tick negative' : 'nv-tick positive') + ' nv-tick-' + j + '-' + i}); paulo@89: paulo@89: var lines = tickGroups.append('line') paulo@89: .attr('class', 'nv-candlestick-lines') paulo@89: .attr('transform', function(d, i) { return 'translate(' + x(getX(d, i)) + ',0)'; }) paulo@89: .attr('x1', 0) paulo@89: .attr('y1', function(d, i) { return y(getHigh(d, i)); }) paulo@89: .attr('x2', 0) paulo@89: .attr('y2', function(d, i) { return y(getLow(d, i)); }); paulo@89: paulo@89: var rects = tickGroups.append('rect') paulo@89: .attr('class', 'nv-candlestick-rects nv-bars') paulo@89: .attr('transform', function(d, i) { paulo@89: return 'translate(' + (x(getX(d, i)) - barWidth/2) + ',' paulo@89: + (y(getY(d, i)) - (getOpen(d, i) > getClose(d, i) ? (y(getClose(d, i)) - y(getOpen(d, i))) : 0)) paulo@89: + ')'; paulo@89: }) paulo@89: .attr('x', 0) paulo@89: .attr('y', 0) paulo@89: .attr('width', barWidth) paulo@89: .attr('height', function(d, i) { paulo@89: var open = getOpen(d, i); paulo@89: var close = getClose(d, i); paulo@89: return open > close ? y(close) - y(open) : y(open) - y(close); paulo@89: }); paulo@89: paulo@89: container.selectAll('.nv-candlestick-lines').transition() paulo@89: .attr('transform', function(d, i) { return 'translate(' + x(getX(d, i)) + ',0)'; }) paulo@89: .attr('x1', 0) paulo@89: .attr('y1', function(d, i) { return y(getHigh(d, i)); }) paulo@89: .attr('x2', 0) paulo@89: .attr('y2', function(d, i) { return y(getLow(d, i)); }); paulo@89: paulo@89: container.selectAll('.nv-candlestick-rects').transition() paulo@89: .attr('transform', function(d, i) { paulo@89: return 'translate(' + (x(getX(d, i)) - barWidth/2) + ',' paulo@89: + (y(getY(d, i)) - (getOpen(d, i) > getClose(d, i) ? (y(getClose(d, i)) - y(getOpen(d, i))) : 0)) paulo@89: + ')'; paulo@89: }) paulo@89: .attr('x', 0) paulo@89: .attr('y', 0) paulo@89: .attr('width', barWidth) paulo@89: .attr('height', function(d, i) { paulo@89: var open = getOpen(d, i); paulo@89: var close = getClose(d, i); paulo@89: return open > close ? y(close) - y(open) : y(open) - y(close); paulo@89: }); paulo@89: }); paulo@89: paulo@89: return chart; paulo@89: } paulo@89: paulo@89: paulo@89: //Create methods to allow outside functions to highlight a specific bar. paulo@89: chart.highlightPoint = function(pointIndex, isHoverOver) { paulo@89: chart.clearHighlights(); paulo@89: container.select(".nv-candlestickBar .nv-tick-0-" + pointIndex) paulo@89: .classed("hover", isHoverOver) paulo@89: ; paulo@89: }; paulo@89: paulo@89: chart.clearHighlights = function() { paulo@89: container.select(".nv-candlestickBar .nv-tick.hover") paulo@89: .classed("hover", false) paulo@89: ; paulo@89: }; paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: chart.dispatch = dispatch; paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: xScale: {get: function(){return x;}, set: function(_){x=_;}}, paulo@89: yScale: {get: function(){return y;}, set: function(_){y=_;}}, paulo@89: xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}}, paulo@89: yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}}, paulo@89: xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}}, paulo@89: yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}}, paulo@89: forceX: {get: function(){return forceX;}, set: function(_){forceX=_;}}, paulo@89: forceY: {get: function(){return forceY;}, set: function(_){forceY=_;}}, paulo@89: padData: {get: function(){return padData;}, set: function(_){padData=_;}}, paulo@89: clipEdge: {get: function(){return clipEdge;}, set: function(_){clipEdge=_;}}, paulo@89: id: {get: function(){return id;}, set: function(_){id=_;}}, paulo@89: interactive: {get: function(){return interactive;}, set: function(_){interactive=_;}}, paulo@89: paulo@89: x: {get: function(){return getX;}, set: function(_){getX=_;}}, paulo@89: y: {get: function(){return getY;}, set: function(_){getY=_;}}, paulo@89: open: {get: function(){return getOpen();}, set: function(_){getOpen=_;}}, paulo@89: close: {get: function(){return getClose();}, set: function(_){getClose=_;}}, paulo@89: high: {get: function(){return getHigh;}, set: function(_){getHigh=_;}}, paulo@89: low: {get: function(){return getLow;}, set: function(_){getLow=_;}}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top != undefined ? _.top : margin.top; paulo@89: margin.right = _.right != undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom != undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left != undefined ? _.left : margin.left; paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.initOptions(chart); paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: nv.models.cumulativeLineChart = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var lines = nv.models.line() paulo@89: , xAxis = nv.models.axis() paulo@89: , yAxis = nv.models.axis() paulo@89: , legend = nv.models.legend() paulo@89: , controls = nv.models.legend() paulo@89: , interactiveLayer = nv.interactiveGuideline() paulo@89: , tooltip = nv.models.tooltip() paulo@89: ; paulo@89: paulo@89: var margin = {top: 30, right: 30, bottom: 50, left: 60} paulo@89: , color = nv.utils.defaultColor() paulo@89: , width = null paulo@89: , height = null paulo@89: , showLegend = true paulo@89: , showXAxis = true paulo@89: , showYAxis = true paulo@89: , rightAlignYAxis = false paulo@89: , showControls = true paulo@89: , useInteractiveGuideline = false paulo@89: , rescaleY = true paulo@89: , x //can be accessed via chart.xScale() paulo@89: , y //can be accessed via chart.yScale() paulo@89: , id = lines.id() paulo@89: , state = nv.utils.state() paulo@89: , defaultState = null paulo@89: , noData = null paulo@89: , average = function(d) { return d.average } paulo@89: , dispatch = d3.dispatch('stateChange', 'changeState', 'renderEnd') paulo@89: , transitionDuration = 250 paulo@89: , duration = 250 paulo@89: , noErrorCheck = false //if set to TRUE, will bypass an error check in the indexify function. paulo@89: ; paulo@89: paulo@89: state.index = 0; paulo@89: state.rescaleY = rescaleY; paulo@89: paulo@89: xAxis.orient('bottom').tickPadding(7); paulo@89: yAxis.orient((rightAlignYAxis) ? 'right' : 'left'); paulo@89: paulo@89: tooltip.valueFormatter(function(d, i) { paulo@89: return yAxis.tickFormat()(d, i); paulo@89: }).headerFormatter(function(d, i) { paulo@89: return xAxis.tickFormat()(d, i); paulo@89: }); paulo@89: paulo@89: controls.updateState(false); paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var dx = d3.scale.linear() paulo@89: , index = {i: 0, x: 0} paulo@89: , renderWatch = nv.utils.renderWatch(dispatch, duration) paulo@89: ; paulo@89: paulo@89: var stateGetter = function(data) { paulo@89: return function(){ paulo@89: return { paulo@89: active: data.map(function(d) { return !d.disabled }), paulo@89: index: index.i, paulo@89: rescaleY: rescaleY paulo@89: }; paulo@89: } paulo@89: }; paulo@89: paulo@89: var stateSetter = function(data) { paulo@89: return function(state) { paulo@89: if (state.index !== undefined) paulo@89: index.i = state.index; paulo@89: if (state.rescaleY !== undefined) paulo@89: rescaleY = state.rescaleY; paulo@89: if (state.active !== undefined) paulo@89: data.forEach(function(series,i) { paulo@89: series.disabled = !state.active[i]; paulo@89: }); paulo@89: } paulo@89: }; paulo@89: paulo@89: function chart(selection) { paulo@89: renderWatch.reset(); paulo@89: renderWatch.models(lines); paulo@89: if (showXAxis) renderWatch.models(xAxis); paulo@89: if (showYAxis) renderWatch.models(yAxis); paulo@89: selection.each(function(data) { paulo@89: var container = d3.select(this); paulo@89: nv.utils.initSVG(container); paulo@89: container.classed('nv-chart-' + id, true); paulo@89: var that = this; paulo@89: paulo@89: var availableWidth = nv.utils.availableWidth(width, container, margin), paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: paulo@89: chart.update = function() { paulo@89: if (duration === 0) paulo@89: container.call(chart); paulo@89: else paulo@89: container.transition().duration(duration).call(chart) paulo@89: }; paulo@89: chart.container = this; paulo@89: paulo@89: state paulo@89: .setter(stateSetter(data), chart.update) paulo@89: .getter(stateGetter(data)) paulo@89: .update(); paulo@89: paulo@89: // DEPRECATED set state.disableddisabled paulo@89: state.disabled = data.map(function(d) { return !!d.disabled }); paulo@89: paulo@89: if (!defaultState) { paulo@89: var key; paulo@89: defaultState = {}; paulo@89: for (key in state) { paulo@89: if (state[key] instanceof Array) paulo@89: defaultState[key] = state[key].slice(0); paulo@89: else paulo@89: defaultState[key] = state[key]; paulo@89: } paulo@89: } paulo@89: paulo@89: var indexDrag = d3.behavior.drag() paulo@89: .on('dragstart', dragStart) paulo@89: .on('drag', dragMove) paulo@89: .on('dragend', dragEnd); paulo@89: paulo@89: paulo@89: function dragStart(d,i) { paulo@89: d3.select(chart.container) paulo@89: .style('cursor', 'ew-resize'); paulo@89: } paulo@89: paulo@89: function dragMove(d,i) { paulo@89: index.x = d3.event.x; paulo@89: index.i = Math.round(dx.invert(index.x)); paulo@89: updateZero(); paulo@89: } paulo@89: paulo@89: function dragEnd(d,i) { paulo@89: d3.select(chart.container) paulo@89: .style('cursor', 'auto'); paulo@89: paulo@89: // update state and send stateChange with new index paulo@89: state.index = index.i; paulo@89: dispatch.stateChange(state); paulo@89: } paulo@89: paulo@89: // Display No Data message if there's nothing to show. paulo@89: if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) { paulo@89: nv.utils.noData(chart, container) paulo@89: return chart; paulo@89: } else { paulo@89: container.selectAll('.nv-noData').remove(); paulo@89: } paulo@89: paulo@89: // Setup Scales paulo@89: x = lines.xScale(); paulo@89: y = lines.yScale(); paulo@89: paulo@89: if (!rescaleY) { paulo@89: var seriesDomains = data paulo@89: .filter(function(series) { return !series.disabled }) paulo@89: .map(function(series,i) { paulo@89: var initialDomain = d3.extent(series.values, lines.y()); paulo@89: paulo@89: //account for series being disabled when losing 95% or more paulo@89: if (initialDomain[0] < -.95) initialDomain[0] = -.95; paulo@89: paulo@89: return [ paulo@89: (initialDomain[0] - initialDomain[1]) / (1 + initialDomain[1]), paulo@89: (initialDomain[1] - initialDomain[0]) / (1 + initialDomain[0]) paulo@89: ]; paulo@89: }); paulo@89: paulo@89: var completeDomain = [ paulo@89: d3.min(seriesDomains, function(d) { return d[0] }), paulo@89: d3.max(seriesDomains, function(d) { return d[1] }) paulo@89: ]; paulo@89: paulo@89: lines.yDomain(completeDomain); paulo@89: } else { paulo@89: lines.yDomain(null); paulo@89: } paulo@89: paulo@89: dx.domain([0, data[0].values.length - 1]) //Assumes all series have same length paulo@89: .range([0, availableWidth]) paulo@89: .clamp(true); paulo@89: paulo@89: var data = indexify(index.i, data); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var interactivePointerEvents = (useInteractiveGuideline) ? "none" : "all"; paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-cumulativeLine').data([data]); paulo@89: var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-cumulativeLine').append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-interactive'); paulo@89: gEnter.append('g').attr('class', 'nv-x nv-axis').style("pointer-events","none"); paulo@89: gEnter.append('g').attr('class', 'nv-y nv-axis'); paulo@89: gEnter.append('g').attr('class', 'nv-background'); paulo@89: gEnter.append('g').attr('class', 'nv-linesWrap').style("pointer-events",interactivePointerEvents); paulo@89: gEnter.append('g').attr('class', 'nv-avgLinesWrap').style("pointer-events","none"); paulo@89: gEnter.append('g').attr('class', 'nv-legendWrap'); paulo@89: gEnter.append('g').attr('class', 'nv-controlsWrap'); paulo@89: paulo@89: // Legend paulo@89: if (showLegend) { paulo@89: legend.width(availableWidth); paulo@89: paulo@89: g.select('.nv-legendWrap') paulo@89: .datum(data) paulo@89: .call(legend); paulo@89: paulo@89: if ( margin.top != legend.height()) { paulo@89: margin.top = legend.height(); paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: } paulo@89: paulo@89: g.select('.nv-legendWrap') paulo@89: .attr('transform', 'translate(0,' + (-margin.top) +')') paulo@89: } paulo@89: paulo@89: // Controls paulo@89: if (showControls) { paulo@89: var controlsData = [ paulo@89: { key: 'Re-scale y-axis', disabled: !rescaleY } paulo@89: ]; paulo@89: paulo@89: controls paulo@89: .width(140) paulo@89: .color(['#444', '#444', '#444']) paulo@89: .rightAlign(false) paulo@89: .margin({top: 5, right: 0, bottom: 5, left: 20}) paulo@89: ; paulo@89: paulo@89: g.select('.nv-controlsWrap') paulo@89: .datum(controlsData) paulo@89: .attr('transform', 'translate(0,' + (-margin.top) +')') paulo@89: .call(controls); paulo@89: } paulo@89: paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: if (rightAlignYAxis) { paulo@89: g.select(".nv-y.nv-axis") paulo@89: .attr("transform", "translate(" + availableWidth + ",0)"); paulo@89: } paulo@89: paulo@89: // Show error if series goes below 100% paulo@89: var tempDisabled = data.filter(function(d) { return d.tempDisabled }); paulo@89: paulo@89: wrap.select('.tempDisabled').remove(); //clean-up and prevent duplicates paulo@89: if (tempDisabled.length) { paulo@89: wrap.append('text').attr('class', 'tempDisabled') paulo@89: .attr('x', availableWidth / 2) paulo@89: .attr('y', '-.71em') paulo@89: .style('text-anchor', 'end') paulo@89: .text(tempDisabled.map(function(d) { return d.key }).join(', ') + ' values cannot be calculated for this time period.'); paulo@89: } paulo@89: paulo@89: //Set up interactive layer paulo@89: if (useInteractiveGuideline) { paulo@89: interactiveLayer paulo@89: .width(availableWidth) paulo@89: .height(availableHeight) paulo@89: .margin({left:margin.left,top:margin.top}) paulo@89: .svgContainer(container) paulo@89: .xScale(x); paulo@89: wrap.select(".nv-interactive").call(interactiveLayer); paulo@89: } paulo@89: paulo@89: gEnter.select('.nv-background') paulo@89: .append('rect'); paulo@89: paulo@89: g.select('.nv-background rect') paulo@89: .attr('width', availableWidth) paulo@89: .attr('height', availableHeight); paulo@89: paulo@89: lines paulo@89: //.x(function(d) { return d.x }) paulo@89: .y(function(d) { return d.display.y }) paulo@89: .width(availableWidth) paulo@89: .height(availableHeight) paulo@89: .color(data.map(function(d,i) { paulo@89: return d.color || color(d, i); paulo@89: }).filter(function(d,i) { return !data[i].disabled && !data[i].tempDisabled; })); paulo@89: paulo@89: var linesWrap = g.select('.nv-linesWrap') paulo@89: .datum(data.filter(function(d) { return !d.disabled && !d.tempDisabled })); paulo@89: paulo@89: linesWrap.call(lines); paulo@89: paulo@89: //Store a series index number in the data array. paulo@89: data.forEach(function(d,i) { paulo@89: d.seriesIndex = i; paulo@89: }); paulo@89: paulo@89: var avgLineData = data.filter(function(d) { paulo@89: return !d.disabled && !!average(d); paulo@89: }); paulo@89: paulo@89: var avgLines = g.select(".nv-avgLinesWrap").selectAll("line") paulo@89: .data(avgLineData, function(d) { return d.key; }); paulo@89: paulo@89: var getAvgLineY = function(d) { paulo@89: //If average lines go off the svg element, clamp them to the svg bounds. paulo@89: var yVal = y(average(d)); paulo@89: if (yVal < 0) return 0; paulo@89: if (yVal > availableHeight) return availableHeight; paulo@89: return yVal; paulo@89: }; paulo@89: paulo@89: avgLines.enter() paulo@89: .append('line') paulo@89: .style('stroke-width',2) paulo@89: .style('stroke-dasharray','10,10') paulo@89: .style('stroke',function (d,i) { paulo@89: return lines.color()(d,d.seriesIndex); paulo@89: }) paulo@89: .attr('x1',0) paulo@89: .attr('x2',availableWidth) paulo@89: .attr('y1', getAvgLineY) paulo@89: .attr('y2', getAvgLineY); paulo@89: paulo@89: avgLines paulo@89: .style('stroke-opacity',function(d){ paulo@89: //If average lines go offscreen, make them transparent paulo@89: var yVal = y(average(d)); paulo@89: if (yVal < 0 || yVal > availableHeight) return 0; paulo@89: return 1; paulo@89: }) paulo@89: .attr('x1',0) paulo@89: .attr('x2',availableWidth) paulo@89: .attr('y1', getAvgLineY) paulo@89: .attr('y2', getAvgLineY); paulo@89: paulo@89: avgLines.exit().remove(); paulo@89: paulo@89: //Create index line paulo@89: var indexLine = linesWrap.selectAll('.nv-indexLine') paulo@89: .data([index]); paulo@89: indexLine.enter().append('rect').attr('class', 'nv-indexLine') paulo@89: .attr('width', 3) paulo@89: .attr('x', -2) paulo@89: .attr('fill', 'red') paulo@89: .attr('fill-opacity', .5) paulo@89: .style("pointer-events","all") paulo@89: .call(indexDrag); paulo@89: paulo@89: indexLine paulo@89: .attr('transform', function(d) { return 'translate(' + dx(d.i) + ',0)' }) paulo@89: .attr('height', availableHeight); paulo@89: paulo@89: // Setup Axes paulo@89: if (showXAxis) { paulo@89: xAxis paulo@89: .scale(x) paulo@89: ._ticks( nv.utils.calcTicksX(availableWidth/70, data) ) paulo@89: .tickSize(-availableHeight, 0); paulo@89: paulo@89: g.select('.nv-x.nv-axis') paulo@89: .attr('transform', 'translate(0,' + y.range()[0] + ')'); paulo@89: g.select('.nv-x.nv-axis') paulo@89: .call(xAxis); paulo@89: } paulo@89: paulo@89: if (showYAxis) { paulo@89: yAxis paulo@89: .scale(y) paulo@89: ._ticks( nv.utils.calcTicksY(availableHeight/36, data) ) paulo@89: .tickSize( -availableWidth, 0); paulo@89: paulo@89: g.select('.nv-y.nv-axis') paulo@89: .call(yAxis); paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (in chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: function updateZero() { paulo@89: indexLine paulo@89: .data([index]); paulo@89: paulo@89: //When dragging the index line, turn off line transitions. paulo@89: // Then turn them back on when done dragging. paulo@89: var oldDuration = chart.duration(); paulo@89: chart.duration(0); paulo@89: chart.update(); paulo@89: chart.duration(oldDuration); paulo@89: } paulo@89: paulo@89: g.select('.nv-background rect') paulo@89: .on('click', function() { paulo@89: index.x = d3.mouse(this)[0]; paulo@89: index.i = Math.round(dx.invert(index.x)); paulo@89: paulo@89: // update state and send stateChange with new index paulo@89: state.index = index.i; paulo@89: dispatch.stateChange(state); paulo@89: paulo@89: updateZero(); paulo@89: }); paulo@89: paulo@89: lines.dispatch.on('elementClick', function(e) { paulo@89: index.i = e.pointIndex; paulo@89: index.x = dx(index.i); paulo@89: paulo@89: // update state and send stateChange with new index paulo@89: state.index = index.i; paulo@89: dispatch.stateChange(state); paulo@89: paulo@89: updateZero(); paulo@89: }); paulo@89: paulo@89: controls.dispatch.on('legendClick', function(d,i) { paulo@89: d.disabled = !d.disabled; paulo@89: rescaleY = !d.disabled; paulo@89: paulo@89: state.rescaleY = rescaleY; paulo@89: dispatch.stateChange(state); paulo@89: chart.update(); paulo@89: }); paulo@89: paulo@89: legend.dispatch.on('stateChange', function(newState) { paulo@89: for (var key in newState) paulo@89: state[key] = newState[key]; paulo@89: dispatch.stateChange(state); paulo@89: chart.update(); paulo@89: }); paulo@89: paulo@89: interactiveLayer.dispatch.on('elementMousemove', function(e) { paulo@89: lines.clearHighlights(); paulo@89: var singlePoint, pointIndex, pointXLocation, allData = []; paulo@89: paulo@89: data paulo@89: .filter(function(series, i) { paulo@89: series.seriesIndex = i; paulo@89: return !series.disabled; paulo@89: }) paulo@89: .forEach(function(series,i) { paulo@89: pointIndex = nv.interactiveBisect(series.values, e.pointXValue, chart.x()); paulo@89: lines.highlightPoint(i, pointIndex, true); paulo@89: var point = series.values[pointIndex]; paulo@89: if (typeof point === 'undefined') return; paulo@89: if (typeof singlePoint === 'undefined') singlePoint = point; paulo@89: if (typeof pointXLocation === 'undefined') pointXLocation = chart.xScale()(chart.x()(point,pointIndex)); paulo@89: allData.push({ paulo@89: key: series.key, paulo@89: value: chart.y()(point, pointIndex), paulo@89: color: color(series,series.seriesIndex) paulo@89: }); paulo@89: }); paulo@89: paulo@89: //Highlight the tooltip entry based on which point the mouse is closest to. paulo@89: if (allData.length > 2) { paulo@89: var yValue = chart.yScale().invert(e.mouseY); paulo@89: var domainExtent = Math.abs(chart.yScale().domain()[0] - chart.yScale().domain()[1]); paulo@89: var threshold = 0.03 * domainExtent; paulo@89: var indexToHighlight = nv.nearestValueIndex(allData.map(function(d){return d.value}),yValue,threshold); paulo@89: if (indexToHighlight !== null) paulo@89: allData[indexToHighlight].highlight = true; paulo@89: } paulo@89: paulo@89: var xValue = xAxis.tickFormat()(chart.x()(singlePoint,pointIndex), pointIndex); paulo@89: interactiveLayer.tooltip paulo@89: .position({left: pointXLocation + margin.left, top: e.mouseY + margin.top}) paulo@89: .chartContainer(that.parentNode) paulo@89: .valueFormatter(function(d,i) { paulo@89: return yAxis.tickFormat()(d); paulo@89: }) paulo@89: .data( paulo@89: { paulo@89: value: xValue, paulo@89: series: allData paulo@89: } paulo@89: )(); paulo@89: paulo@89: interactiveLayer.renderGuideLine(pointXLocation); paulo@89: }); paulo@89: paulo@89: interactiveLayer.dispatch.on("elementMouseout",function(e) { paulo@89: lines.clearHighlights(); paulo@89: }); paulo@89: paulo@89: // Update chart from a state object passed to event handler paulo@89: dispatch.on('changeState', function(e) { paulo@89: if (typeof e.disabled !== 'undefined') { paulo@89: data.forEach(function(series,i) { paulo@89: series.disabled = e.disabled[i]; paulo@89: }); paulo@89: paulo@89: state.disabled = e.disabled; paulo@89: } paulo@89: paulo@89: if (typeof e.index !== 'undefined') { paulo@89: index.i = e.index; paulo@89: index.x = dx(index.i); paulo@89: paulo@89: state.index = e.index; paulo@89: paulo@89: indexLine paulo@89: .data([index]); paulo@89: } paulo@89: paulo@89: if (typeof e.rescaleY !== 'undefined') { paulo@89: rescaleY = e.rescaleY; paulo@89: } paulo@89: paulo@89: chart.update(); paulo@89: }); paulo@89: paulo@89: }); paulo@89: paulo@89: renderWatch.renderEnd('cumulativeLineChart immediate'); paulo@89: paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (out of chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: lines.dispatch.on('elementMouseover.tooltip', function(evt) { paulo@89: var point = { paulo@89: x: chart.x()(evt.point), paulo@89: y: chart.y()(evt.point), paulo@89: color: evt.point.color paulo@89: }; paulo@89: evt.point = point; paulo@89: tooltip.data(evt).position(evt.pos).hidden(false); paulo@89: }); paulo@89: paulo@89: lines.dispatch.on('elementMouseout.tooltip', function(evt) { paulo@89: tooltip.hidden(true) paulo@89: }); paulo@89: paulo@89: //============================================================ paulo@89: // Functions paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var indexifyYGetter = null; paulo@89: /* Normalize the data according to an index point. */ paulo@89: function indexify(idx, data) { paulo@89: if (!indexifyYGetter) indexifyYGetter = lines.y(); paulo@89: return data.map(function(line, i) { paulo@89: if (!line.values) { paulo@89: return line; paulo@89: } paulo@89: var indexValue = line.values[idx]; paulo@89: if (indexValue == null) { paulo@89: return line; paulo@89: } paulo@89: var v = indexifyYGetter(indexValue, idx); paulo@89: paulo@89: //TODO: implement check below, and disable series if series loses 100% or more cause divide by 0 issue paulo@89: if (v < -.95 && !noErrorCheck) { paulo@89: //if a series loses more than 100%, calculations fail.. anything close can cause major distortion (but is mathematically correct till it hits 100) paulo@89: paulo@89: line.tempDisabled = true; paulo@89: return line; paulo@89: } paulo@89: paulo@89: line.tempDisabled = false; paulo@89: paulo@89: line.values = line.values.map(function(point, pointIndex) { paulo@89: point.display = {'y': (indexifyYGetter(point, pointIndex) - v) / (1 + v) }; paulo@89: return point; paulo@89: }); paulo@89: paulo@89: return line; paulo@89: }) paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: // expose chart's sub-components paulo@89: chart.dispatch = dispatch; paulo@89: chart.lines = lines; paulo@89: chart.legend = legend; paulo@89: chart.controls = controls; paulo@89: chart.xAxis = xAxis; paulo@89: chart.yAxis = yAxis; paulo@89: chart.interactiveLayer = interactiveLayer; paulo@89: chart.state = state; paulo@89: chart.tooltip = tooltip; paulo@89: paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: rescaleY: {get: function(){return rescaleY;}, set: function(_){rescaleY=_;}}, paulo@89: showControls: {get: function(){return showControls;}, set: function(_){showControls=_;}}, paulo@89: showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}}, paulo@89: average: {get: function(){return average;}, set: function(_){average=_;}}, paulo@89: defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}}, paulo@89: noData: {get: function(){return noData;}, set: function(_){noData=_;}}, paulo@89: showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}}, paulo@89: showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}}, paulo@89: noErrorCheck: {get: function(){return noErrorCheck;}, set: function(_){noErrorCheck=_;}}, paulo@89: paulo@89: // deprecated options paulo@89: tooltips: {get: function(){return tooltip.enabled();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead'); paulo@89: tooltip.enabled(!!_); paulo@89: }}, paulo@89: tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead'); paulo@89: tooltip.contentGenerator(_); paulo@89: }}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: legend.color(color); paulo@89: }}, paulo@89: useInteractiveGuideline: {get: function(){return useInteractiveGuideline;}, set: function(_){ paulo@89: useInteractiveGuideline = _; paulo@89: if (_ === true) { paulo@89: chart.interactive(false); paulo@89: chart.useVoronoi(false); paulo@89: } paulo@89: }}, paulo@89: rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){ paulo@89: rightAlignYAxis = _; paulo@89: yAxis.orient( (_) ? 'right' : 'left'); paulo@89: }}, paulo@89: duration: {get: function(){return duration;}, set: function(_){ paulo@89: duration = _; paulo@89: lines.duration(duration); paulo@89: xAxis.duration(duration); paulo@89: yAxis.duration(duration); paulo@89: renderWatch.reset(duration); paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.inheritOptions(chart, lines); paulo@89: nv.utils.initOptions(chart); paulo@89: paulo@89: return chart; paulo@89: }; paulo@89: //TODO: consider deprecating by adding necessary features to multiBar model paulo@89: nv.models.discreteBar = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var margin = {top: 0, right: 0, bottom: 0, left: 0} paulo@89: , width = 960 paulo@89: , height = 500 paulo@89: , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one paulo@89: , container paulo@89: , x = d3.scale.ordinal() paulo@89: , y = d3.scale.linear() paulo@89: , getX = function(d) { return d.x } paulo@89: , getY = function(d) { return d.y } paulo@89: , forceY = [0] // 0 is forced by default.. this makes sense for the majority of bar graphs... user can always do chart.forceY([]) to remove paulo@89: , color = nv.utils.defaultColor() paulo@89: , showValues = false paulo@89: , valueFormat = d3.format(',.2f') paulo@89: , xDomain paulo@89: , yDomain paulo@89: , xRange paulo@89: , yRange paulo@89: , dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd') paulo@89: , rectClass = 'discreteBar' paulo@89: , duration = 250 paulo@89: ; paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var x0, y0; paulo@89: var renderWatch = nv.utils.renderWatch(dispatch, duration); paulo@89: paulo@89: function chart(selection) { paulo@89: renderWatch.reset(); paulo@89: selection.each(function(data) { paulo@89: var availableWidth = width - margin.left - margin.right, paulo@89: availableHeight = height - margin.top - margin.bottom; paulo@89: paulo@89: container = d3.select(this); paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: //add series index to each data point for reference paulo@89: data.forEach(function(series, i) { paulo@89: series.values.forEach(function(point) { paulo@89: point.series = i; paulo@89: }); paulo@89: }); paulo@89: paulo@89: // Setup Scales paulo@89: // remap and flatten the data for use in calculating the scales' domains paulo@89: var seriesData = (xDomain && yDomain) ? [] : // if we know xDomain and yDomain, no need to calculate paulo@89: data.map(function(d) { paulo@89: return d.values.map(function(d,i) { paulo@89: return { x: getX(d,i), y: getY(d,i), y0: d.y0 } paulo@89: }) paulo@89: }); paulo@89: paulo@89: x .domain(xDomain || d3.merge(seriesData).map(function(d) { return d.x })) paulo@89: .rangeBands(xRange || [0, availableWidth], .1); paulo@89: y .domain(yDomain || d3.extent(d3.merge(seriesData).map(function(d) { return d.y }).concat(forceY))); paulo@89: paulo@89: // If showValues, pad the Y axis range to account for label height paulo@89: if (showValues) y.range(yRange || [availableHeight - (y.domain()[0] < 0 ? 12 : 0), y.domain()[1] > 0 ? 12 : 0]); paulo@89: else y.range(yRange || [availableHeight, 0]); paulo@89: paulo@89: //store old scales if they exist paulo@89: x0 = x0 || x; paulo@89: y0 = y0 || y.copy().range([y(0),y(0)]); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-discretebar').data([data]); paulo@89: var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-discretebar'); paulo@89: var gEnter = wrapEnter.append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-groups'); paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: //TODO: by definition, the discrete bar should not have multiple groups, will modify/remove later paulo@89: var groups = wrap.select('.nv-groups').selectAll('.nv-group') paulo@89: .data(function(d) { return d }, function(d) { return d.key }); paulo@89: groups.enter().append('g') paulo@89: .style('stroke-opacity', 1e-6) paulo@89: .style('fill-opacity', 1e-6); paulo@89: groups.exit() paulo@89: .watchTransition(renderWatch, 'discreteBar: exit groups') paulo@89: .style('stroke-opacity', 1e-6) paulo@89: .style('fill-opacity', 1e-6) paulo@89: .remove(); paulo@89: groups paulo@89: .attr('class', function(d,i) { return 'nv-group nv-series-' + i }) paulo@89: .classed('hover', function(d) { return d.hover }); paulo@89: groups paulo@89: .watchTransition(renderWatch, 'discreteBar: groups') paulo@89: .style('stroke-opacity', 1) paulo@89: .style('fill-opacity', .75); paulo@89: paulo@89: var bars = groups.selectAll('g.nv-bar') paulo@89: .data(function(d) { return d.values }); paulo@89: bars.exit().remove(); paulo@89: paulo@89: var barsEnter = bars.enter().append('g') paulo@89: .attr('transform', function(d,i,j) { paulo@89: return 'translate(' + (x(getX(d,i)) + x.rangeBand() * .05 ) + ', ' + y(0) + ')' paulo@89: }) paulo@89: .on('mouseover', function(d,i) { //TODO: figure out why j works above, but not here paulo@89: d3.select(this).classed('hover', true); paulo@89: dispatch.elementMouseover({ paulo@89: data: d, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: }) paulo@89: .on('mouseout', function(d,i) { paulo@89: d3.select(this).classed('hover', false); paulo@89: dispatch.elementMouseout({ paulo@89: data: d, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: }) paulo@89: .on('mousemove', function(d,i) { paulo@89: dispatch.elementMousemove({ paulo@89: data: d, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: }) paulo@89: .on('click', function(d,i) { paulo@89: dispatch.elementClick({ paulo@89: data: d, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: d3.event.stopPropagation(); paulo@89: }) paulo@89: .on('dblclick', function(d,i) { paulo@89: dispatch.elementDblClick({ paulo@89: data: d, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: d3.event.stopPropagation(); paulo@89: }); paulo@89: paulo@89: barsEnter.append('rect') paulo@89: .attr('height', 0) paulo@89: .attr('width', x.rangeBand() * .9 / data.length ) paulo@89: paulo@89: if (showValues) { paulo@89: barsEnter.append('text') paulo@89: .attr('text-anchor', 'middle') paulo@89: ; paulo@89: paulo@89: bars.select('text') paulo@89: .text(function(d,i) { return valueFormat(getY(d,i)) }) paulo@89: .watchTransition(renderWatch, 'discreteBar: bars text') paulo@89: .attr('x', x.rangeBand() * .9 / 2) paulo@89: .attr('y', function(d,i) { return getY(d,i) < 0 ? y(getY(d,i)) - y(0) + 12 : -4 }) paulo@89: paulo@89: ; paulo@89: } else { paulo@89: bars.selectAll('text').remove(); paulo@89: } paulo@89: paulo@89: bars paulo@89: .attr('class', function(d,i) { return getY(d,i) < 0 ? 'nv-bar negative' : 'nv-bar positive' }) paulo@89: .style('fill', function(d,i) { return d.color || color(d,i) }) paulo@89: .style('stroke', function(d,i) { return d.color || color(d,i) }) paulo@89: .select('rect') paulo@89: .attr('class', rectClass) paulo@89: .watchTransition(renderWatch, 'discreteBar: bars rect') paulo@89: .attr('width', x.rangeBand() * .9 / data.length); paulo@89: bars.watchTransition(renderWatch, 'discreteBar: bars') paulo@89: //.delay(function(d,i) { return i * 1200 / data[0].values.length }) paulo@89: .attr('transform', function(d,i) { paulo@89: var left = x(getX(d,i)) + x.rangeBand() * .05, paulo@89: top = getY(d,i) < 0 ? paulo@89: y(0) : paulo@89: y(0) - y(getY(d,i)) < 1 ? paulo@89: y(0) - 1 : //make 1 px positive bars show up above y=0 paulo@89: y(getY(d,i)); paulo@89: paulo@89: return 'translate(' + left + ', ' + top + ')' paulo@89: }) paulo@89: .select('rect') paulo@89: .attr('height', function(d,i) { paulo@89: return Math.max(Math.abs(y(getY(d,i)) - y((yDomain && yDomain[0]) || 0)) || 1) paulo@89: }); paulo@89: paulo@89: paulo@89: //store old scales for use in transitions on update paulo@89: x0 = x.copy(); paulo@89: y0 = y.copy(); paulo@89: paulo@89: }); paulo@89: paulo@89: renderWatch.renderEnd('discreteBar immediate'); paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: chart.dispatch = dispatch; paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: forceY: {get: function(){return forceY;}, set: function(_){forceY=_;}}, paulo@89: showValues: {get: function(){return showValues;}, set: function(_){showValues=_;}}, paulo@89: x: {get: function(){return getX;}, set: function(_){getX=_;}}, paulo@89: y: {get: function(){return getY;}, set: function(_){getY=_;}}, paulo@89: xScale: {get: function(){return x;}, set: function(_){x=_;}}, paulo@89: yScale: {get: function(){return y;}, set: function(_){y=_;}}, paulo@89: xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}}, paulo@89: yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}}, paulo@89: xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}}, paulo@89: yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}}, paulo@89: valueFormat: {get: function(){return valueFormat;}, set: function(_){valueFormat=_;}}, paulo@89: id: {get: function(){return id;}, set: function(_){id=_;}}, paulo@89: rectClass: {get: function(){return rectClass;}, set: function(_){rectClass=_;}}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: }}, paulo@89: duration: {get: function(){return duration;}, set: function(_){ paulo@89: duration = _; paulo@89: renderWatch.reset(duration); paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.initOptions(chart); paulo@89: paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: nv.models.discreteBarChart = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var discretebar = nv.models.discreteBar() paulo@89: , xAxis = nv.models.axis() paulo@89: , yAxis = nv.models.axis() paulo@89: , tooltip = nv.models.tooltip() paulo@89: ; paulo@89: paulo@89: var margin = {top: 15, right: 10, bottom: 50, left: 60} paulo@89: , width = null paulo@89: , height = null paulo@89: , color = nv.utils.getColor() paulo@89: , showXAxis = true paulo@89: , showYAxis = true paulo@89: , rightAlignYAxis = false paulo@89: , staggerLabels = false paulo@89: , x paulo@89: , y paulo@89: , noData = null paulo@89: , dispatch = d3.dispatch('beforeUpdate','renderEnd') paulo@89: , duration = 250 paulo@89: ; paulo@89: paulo@89: xAxis paulo@89: .orient('bottom') paulo@89: .showMaxMin(false) paulo@89: .tickFormat(function(d) { return d }) paulo@89: ; paulo@89: yAxis paulo@89: .orient((rightAlignYAxis) ? 'right' : 'left') paulo@89: .tickFormat(d3.format(',.1f')) paulo@89: ; paulo@89: paulo@89: tooltip paulo@89: .duration(0) paulo@89: .headerEnabled(false) paulo@89: .valueFormatter(function(d, i) { paulo@89: return yAxis.tickFormat()(d, i); paulo@89: }) paulo@89: .keyFormatter(function(d, i) { paulo@89: return xAxis.tickFormat()(d, i); paulo@89: }); paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var renderWatch = nv.utils.renderWatch(dispatch, duration); paulo@89: paulo@89: function chart(selection) { paulo@89: renderWatch.reset(); paulo@89: renderWatch.models(discretebar); paulo@89: if (showXAxis) renderWatch.models(xAxis); paulo@89: if (showYAxis) renderWatch.models(yAxis); paulo@89: paulo@89: selection.each(function(data) { paulo@89: var container = d3.select(this), paulo@89: that = this; paulo@89: nv.utils.initSVG(container); paulo@89: var availableWidth = nv.utils.availableWidth(width, container, margin), paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: paulo@89: chart.update = function() { paulo@89: dispatch.beforeUpdate(); paulo@89: container.transition().duration(duration).call(chart); paulo@89: }; paulo@89: chart.container = this; paulo@89: paulo@89: // Display No Data message if there's nothing to show. paulo@89: if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) { paulo@89: nv.utils.noData(chart, container); paulo@89: return chart; paulo@89: } else { paulo@89: container.selectAll('.nv-noData').remove(); paulo@89: } paulo@89: paulo@89: // Setup Scales paulo@89: x = discretebar.xScale(); paulo@89: y = discretebar.yScale().clamp(true); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-discreteBarWithAxes').data([data]); paulo@89: var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-discreteBarWithAxes').append('g'); paulo@89: var defsEnter = gEnter.append('defs'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-x nv-axis'); paulo@89: gEnter.append('g').attr('class', 'nv-y nv-axis') paulo@89: .append('g').attr('class', 'nv-zeroLine') paulo@89: .append('line'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-barsWrap'); paulo@89: paulo@89: g.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: if (rightAlignYAxis) { paulo@89: g.select(".nv-y.nv-axis") paulo@89: .attr("transform", "translate(" + availableWidth + ",0)"); paulo@89: } paulo@89: paulo@89: // Main Chart Component(s) paulo@89: discretebar paulo@89: .width(availableWidth) paulo@89: .height(availableHeight); paulo@89: paulo@89: var barsWrap = g.select('.nv-barsWrap') paulo@89: .datum(data.filter(function(d) { return !d.disabled })); paulo@89: paulo@89: barsWrap.transition().call(discretebar); paulo@89: paulo@89: paulo@89: defsEnter.append('clipPath') paulo@89: .attr('id', 'nv-x-label-clip-' + discretebar.id()) paulo@89: .append('rect'); paulo@89: paulo@89: g.select('#nv-x-label-clip-' + discretebar.id() + ' rect') paulo@89: .attr('width', x.rangeBand() * (staggerLabels ? 2 : 1)) paulo@89: .attr('height', 16) paulo@89: .attr('x', -x.rangeBand() / (staggerLabels ? 1 : 2 )); paulo@89: paulo@89: // Setup Axes paulo@89: if (showXAxis) { paulo@89: xAxis paulo@89: .scale(x) paulo@89: ._ticks( nv.utils.calcTicksX(availableWidth/100, data) ) paulo@89: .tickSize(-availableHeight, 0); paulo@89: paulo@89: g.select('.nv-x.nv-axis') paulo@89: .attr('transform', 'translate(0,' + (y.range()[0] + ((discretebar.showValues() && y.domain()[0] < 0) ? 16 : 0)) + ')'); paulo@89: g.select('.nv-x.nv-axis').call(xAxis); paulo@89: paulo@89: var xTicks = g.select('.nv-x.nv-axis').selectAll('g'); paulo@89: if (staggerLabels) { paulo@89: xTicks paulo@89: .selectAll('text') paulo@89: .attr('transform', function(d,i,j) { return 'translate(0,' + (j % 2 == 0 ? '5' : '17') + ')' }) paulo@89: } paulo@89: } paulo@89: paulo@89: if (showYAxis) { paulo@89: yAxis paulo@89: .scale(y) paulo@89: ._ticks( nv.utils.calcTicksY(availableHeight/36, data) ) paulo@89: .tickSize( -availableWidth, 0); paulo@89: paulo@89: g.select('.nv-y.nv-axis').call(yAxis); paulo@89: } paulo@89: paulo@89: // Zero line paulo@89: g.select(".nv-zeroLine line") paulo@89: .attr("x1",0) paulo@89: .attr("x2",availableWidth) paulo@89: .attr("y1", y(0)) paulo@89: .attr("y2", y(0)) paulo@89: ; paulo@89: }); paulo@89: paulo@89: renderWatch.renderEnd('discreteBar chart immediate'); paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (out of chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: discretebar.dispatch.on('elementMouseover.tooltip', function(evt) { paulo@89: evt['series'] = { paulo@89: key: chart.x()(evt.data), paulo@89: value: chart.y()(evt.data), paulo@89: color: evt.color paulo@89: }; paulo@89: tooltip.data(evt).hidden(false); paulo@89: }); paulo@89: paulo@89: discretebar.dispatch.on('elementMouseout.tooltip', function(evt) { paulo@89: tooltip.hidden(true); paulo@89: }); paulo@89: paulo@89: discretebar.dispatch.on('elementMousemove.tooltip', function(evt) { paulo@89: tooltip.position({top: d3.event.pageY, left: d3.event.pageX})(); paulo@89: }); paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: chart.dispatch = dispatch; paulo@89: chart.discretebar = discretebar; paulo@89: chart.xAxis = xAxis; paulo@89: chart.yAxis = yAxis; paulo@89: chart.tooltip = tooltip; paulo@89: paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: staggerLabels: {get: function(){return staggerLabels;}, set: function(_){staggerLabels=_;}}, paulo@89: showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}}, paulo@89: showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}}, paulo@89: noData: {get: function(){return noData;}, set: function(_){noData=_;}}, paulo@89: paulo@89: // deprecated options paulo@89: tooltips: {get: function(){return tooltip.enabled();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead'); paulo@89: tooltip.enabled(!!_); paulo@89: }}, paulo@89: tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead'); paulo@89: tooltip.contentGenerator(_); paulo@89: }}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: duration: {get: function(){return duration;}, set: function(_){ paulo@89: duration = _; paulo@89: renderWatch.reset(duration); paulo@89: discretebar.duration(duration); paulo@89: xAxis.duration(duration); paulo@89: yAxis.duration(duration); paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: discretebar.color(color); paulo@89: }}, paulo@89: rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){ paulo@89: rightAlignYAxis = _; paulo@89: yAxis.orient( (_) ? 'right' : 'left'); paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.inheritOptions(chart, discretebar); paulo@89: nv.utils.initOptions(chart); paulo@89: paulo@89: return chart; paulo@89: } paulo@89: paulo@89: nv.models.distribution = function() { paulo@89: "use strict"; paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var margin = {top: 0, right: 0, bottom: 0, left: 0} paulo@89: , width = 400 //technically width or height depending on x or y.... paulo@89: , size = 8 paulo@89: , axis = 'x' // 'x' or 'y'... horizontal or vertical paulo@89: , getData = function(d) { return d[axis] } // defaults d.x or d.y paulo@89: , color = nv.utils.defaultColor() paulo@89: , scale = d3.scale.linear() paulo@89: , domain paulo@89: , duration = 250 paulo@89: , dispatch = d3.dispatch('renderEnd') paulo@89: ; paulo@89: paulo@89: //============================================================ paulo@89: paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var scale0; paulo@89: var renderWatch = nv.utils.renderWatch(dispatch, duration); paulo@89: paulo@89: //============================================================ paulo@89: paulo@89: paulo@89: function chart(selection) { paulo@89: renderWatch.reset(); paulo@89: selection.each(function(data) { paulo@89: var availableLength = width - (axis === 'x' ? margin.left + margin.right : margin.top + margin.bottom), paulo@89: naxis = axis == 'x' ? 'y' : 'x', paulo@89: container = d3.select(this); paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: //------------------------------------------------------------ paulo@89: // Setup Scales paulo@89: paulo@89: scale0 = scale0 || scale; paulo@89: paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: paulo@89: //------------------------------------------------------------ paulo@89: // Setup containers and skeleton of chart paulo@89: paulo@89: var wrap = container.selectAll('g.nv-distribution').data([data]); paulo@89: var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-distribution'); paulo@89: var gEnter = wrapEnter.append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') paulo@89: paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: paulo@89: var distWrap = g.selectAll('g.nv-dist') paulo@89: .data(function(d) { return d }, function(d) { return d.key }); paulo@89: paulo@89: distWrap.enter().append('g'); paulo@89: distWrap paulo@89: .attr('class', function(d,i) { return 'nv-dist nv-series-' + i }) paulo@89: .style('stroke', function(d,i) { return color(d, i) }); paulo@89: paulo@89: var dist = distWrap.selectAll('line.nv-dist' + axis) paulo@89: .data(function(d) { return d.values }) paulo@89: dist.enter().append('line') paulo@89: .attr(axis + '1', function(d,i) { return scale0(getData(d,i)) }) paulo@89: .attr(axis + '2', function(d,i) { return scale0(getData(d,i)) }) paulo@89: renderWatch.transition(distWrap.exit().selectAll('line.nv-dist' + axis), 'dist exit') paulo@89: // .transition() paulo@89: .attr(axis + '1', function(d,i) { return scale(getData(d,i)) }) paulo@89: .attr(axis + '2', function(d,i) { return scale(getData(d,i)) }) paulo@89: .style('stroke-opacity', 0) paulo@89: .remove(); paulo@89: dist paulo@89: .attr('class', function(d,i) { return 'nv-dist' + axis + ' nv-dist' + axis + '-' + i }) paulo@89: .attr(naxis + '1', 0) paulo@89: .attr(naxis + '2', size); paulo@89: renderWatch.transition(dist, 'dist') paulo@89: // .transition() paulo@89: .attr(axis + '1', function(d,i) { return scale(getData(d,i)) }) paulo@89: .attr(axis + '2', function(d,i) { return scale(getData(d,i)) }) paulo@89: paulo@89: paulo@89: scale0 = scale.copy(); paulo@89: paulo@89: }); paulo@89: renderWatch.renderEnd('distribution immediate'); paulo@89: return chart; paulo@89: } paulo@89: paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: chart.dispatch = dispatch; paulo@89: paulo@89: chart.margin = function(_) { paulo@89: if (!arguments.length) return margin; paulo@89: margin.top = typeof _.top != 'undefined' ? _.top : margin.top; paulo@89: margin.right = typeof _.right != 'undefined' ? _.right : margin.right; paulo@89: margin.bottom = typeof _.bottom != 'undefined' ? _.bottom : margin.bottom; paulo@89: margin.left = typeof _.left != 'undefined' ? _.left : margin.left; paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: chart.width = function(_) { paulo@89: if (!arguments.length) return width; paulo@89: width = _; paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: chart.axis = function(_) { paulo@89: if (!arguments.length) return axis; paulo@89: axis = _; paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: chart.size = function(_) { paulo@89: if (!arguments.length) return size; paulo@89: size = _; paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: chart.getData = function(_) { paulo@89: if (!arguments.length) return getData; paulo@89: getData = d3.functor(_); paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: chart.scale = function(_) { paulo@89: if (!arguments.length) return scale; paulo@89: scale = _; paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: chart.color = function(_) { paulo@89: if (!arguments.length) return color; paulo@89: color = nv.utils.getColor(_); paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: chart.duration = function(_) { paulo@89: if (!arguments.length) return duration; paulo@89: duration = _; paulo@89: renderWatch.reset(duration); paulo@89: return chart; paulo@89: }; paulo@89: //============================================================ paulo@89: paulo@89: paulo@89: return chart; paulo@89: } paulo@89: nv.models.furiousLegend = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var margin = {top: 5, right: 0, bottom: 5, left: 0} paulo@89: , width = 400 paulo@89: , height = 20 paulo@89: , getKey = function(d) { return d.key } paulo@89: , color = nv.utils.getColor() paulo@89: , align = true paulo@89: , padding = 28 //define how much space between legend items. - recommend 32 for furious version paulo@89: , rightAlign = true paulo@89: , updateState = true //If true, legend will update data.disabled and trigger a 'stateChange' dispatch. paulo@89: , radioButtonMode = false //If true, clicking legend items will cause it to behave like a radio button. (only one can be selected at a time) paulo@89: , expanded = false paulo@89: , dispatch = d3.dispatch('legendClick', 'legendDblclick', 'legendMouseover', 'legendMouseout', 'stateChange') paulo@89: , vers = 'classic' //Options are "classic" and "furious" paulo@89: ; paulo@89: paulo@89: function chart(selection) { paulo@89: selection.each(function(data) { paulo@89: var availableWidth = width - margin.left - margin.right, paulo@89: container = d3.select(this); paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-legend').data([data]); paulo@89: var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-legend').append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: var series = g.selectAll('.nv-series') paulo@89: .data(function(d) { paulo@89: if(vers != 'furious') return d; paulo@89: paulo@89: return d.filter(function(n) { paulo@89: return expanded ? true : !n.disengaged; paulo@89: }); paulo@89: }); paulo@89: var seriesEnter = series.enter().append('g').attr('class', 'nv-series') paulo@89: paulo@89: var seriesShape; paulo@89: paulo@89: if(vers == 'classic') { paulo@89: seriesEnter.append('circle') paulo@89: .style('stroke-width', 2) paulo@89: .attr('class','nv-legend-symbol') paulo@89: .attr('r', 5); paulo@89: paulo@89: seriesShape = series.select('circle'); paulo@89: } else if (vers == 'furious') { paulo@89: seriesEnter.append('rect') paulo@89: .style('stroke-width', 2) paulo@89: .attr('class','nv-legend-symbol') paulo@89: .attr('rx', 3) paulo@89: .attr('ry', 3); paulo@89: paulo@89: seriesShape = series.select('rect'); paulo@89: paulo@89: seriesEnter.append('g') paulo@89: .attr('class', 'nv-check-box') paulo@89: .property('innerHTML','') paulo@89: .attr('transform', 'translate(-10,-8)scale(0.5)'); paulo@89: paulo@89: var seriesCheckbox = series.select('.nv-check-box'); paulo@89: paulo@89: seriesCheckbox.each(function(d,i) { paulo@89: d3.select(this).selectAll('path') paulo@89: .attr('stroke', setTextColor(d,i)); paulo@89: }); paulo@89: } paulo@89: paulo@89: seriesEnter.append('text') paulo@89: .attr('text-anchor', 'start') paulo@89: .attr('class','nv-legend-text') paulo@89: .attr('dy', '.32em') paulo@89: .attr('dx', '8'); paulo@89: paulo@89: var seriesText = series.select('text.nv-legend-text'); paulo@89: paulo@89: series paulo@89: .on('mouseover', function(d,i) { paulo@89: dispatch.legendMouseover(d,i); //TODO: Make consistent with other event objects paulo@89: }) paulo@89: .on('mouseout', function(d,i) { paulo@89: dispatch.legendMouseout(d,i); paulo@89: }) paulo@89: .on('click', function(d,i) { paulo@89: dispatch.legendClick(d,i); paulo@89: // make sure we re-get data in case it was modified paulo@89: var data = series.data(); paulo@89: if (updateState) { paulo@89: if(vers =='classic') { paulo@89: if (radioButtonMode) { paulo@89: //Radio button mode: set every series to disabled, paulo@89: // and enable the clicked series. paulo@89: data.forEach(function(series) { series.disabled = true}); paulo@89: d.disabled = false; paulo@89: } paulo@89: else { paulo@89: d.disabled = !d.disabled; paulo@89: if (data.every(function(series) { return series.disabled})) { paulo@89: //the default behavior of NVD3 legends is, if every single series paulo@89: // is disabled, turn all series' back on. paulo@89: data.forEach(function(series) { series.disabled = false}); paulo@89: } paulo@89: } paulo@89: } else if(vers == 'furious') { paulo@89: if(expanded) { paulo@89: d.disengaged = !d.disengaged; paulo@89: d.userDisabled = d.userDisabled == undefined ? !!d.disabled : d.userDisabled; paulo@89: d.disabled = d.disengaged || d.userDisabled; paulo@89: } else if (!expanded) { paulo@89: d.disabled = !d.disabled; paulo@89: d.userDisabled = d.disabled; paulo@89: var engaged = data.filter(function(d) { return !d.disengaged; }); paulo@89: if (engaged.every(function(series) { return series.userDisabled })) { paulo@89: //the default behavior of NVD3 legends is, if every single series paulo@89: // is disabled, turn all series' back on. paulo@89: data.forEach(function(series) { paulo@89: series.disabled = series.userDisabled = false; paulo@89: }); paulo@89: } paulo@89: } paulo@89: } paulo@89: dispatch.stateChange({ paulo@89: disabled: data.map(function(d) { return !!d.disabled }), paulo@89: disengaged: data.map(function(d) { return !!d.disengaged }) paulo@89: }); paulo@89: paulo@89: } paulo@89: }) paulo@89: .on('dblclick', function(d,i) { paulo@89: if(vers == 'furious' && expanded) return; paulo@89: dispatch.legendDblclick(d,i); paulo@89: if (updateState) { paulo@89: // make sure we re-get data in case it was modified paulo@89: var data = series.data(); paulo@89: //the default behavior of NVD3 legends, when double clicking one, paulo@89: // is to set all other series' to false, and make the double clicked series enabled. paulo@89: data.forEach(function(series) { paulo@89: series.disabled = true; paulo@89: if(vers == 'furious') series.userDisabled = series.disabled; paulo@89: }); paulo@89: d.disabled = false; paulo@89: if(vers == 'furious') d.userDisabled = d.disabled; paulo@89: dispatch.stateChange({ paulo@89: disabled: data.map(function(d) { return !!d.disabled }) paulo@89: }); paulo@89: } paulo@89: }); paulo@89: paulo@89: series.classed('nv-disabled', function(d) { return d.userDisabled }); paulo@89: series.exit().remove(); paulo@89: paulo@89: seriesText paulo@89: .attr('fill', setTextColor) paulo@89: .text(getKey); paulo@89: paulo@89: //TODO: implement fixed-width and max-width options (max-width is especially useful with the align option) paulo@89: // NEW ALIGNING CODE, TODO: clean up paulo@89: paulo@89: var versPadding; paulo@89: switch(vers) { paulo@89: case 'furious' : paulo@89: versPadding = 23; paulo@89: break; paulo@89: case 'classic' : paulo@89: versPadding = 20; paulo@89: } paulo@89: paulo@89: if (align) { paulo@89: paulo@89: var seriesWidths = []; paulo@89: series.each(function(d,i) { paulo@89: var legendText = d3.select(this).select('text'); paulo@89: var nodeTextLength; paulo@89: try { paulo@89: nodeTextLength = legendText.node().getComputedTextLength(); paulo@89: // If the legendText is display:none'd (nodeTextLength == 0), simulate an error so we approximate, instead paulo@89: if(nodeTextLength <= 0) throw Error(); paulo@89: } paulo@89: catch(e) { paulo@89: nodeTextLength = nv.utils.calcApproxTextWidth(legendText); paulo@89: } paulo@89: paulo@89: seriesWidths.push(nodeTextLength + padding); paulo@89: }); paulo@89: paulo@89: var seriesPerRow = 0; paulo@89: var legendWidth = 0; paulo@89: var columnWidths = []; paulo@89: paulo@89: while ( legendWidth < availableWidth && seriesPerRow < seriesWidths.length) { paulo@89: columnWidths[seriesPerRow] = seriesWidths[seriesPerRow]; paulo@89: legendWidth += seriesWidths[seriesPerRow++]; paulo@89: } paulo@89: if (seriesPerRow === 0) seriesPerRow = 1; //minimum of one series per row paulo@89: paulo@89: while ( legendWidth > availableWidth && seriesPerRow > 1 ) { paulo@89: columnWidths = []; paulo@89: seriesPerRow--; paulo@89: paulo@89: for (var k = 0; k < seriesWidths.length; k++) { paulo@89: if (seriesWidths[k] > (columnWidths[k % seriesPerRow] || 0) ) paulo@89: columnWidths[k % seriesPerRow] = seriesWidths[k]; paulo@89: } paulo@89: paulo@89: legendWidth = columnWidths.reduce(function(prev, cur, index, array) { paulo@89: return prev + cur; paulo@89: }); paulo@89: } paulo@89: paulo@89: var xPositions = []; paulo@89: for (var i = 0, curX = 0; i < seriesPerRow; i++) { paulo@89: xPositions[i] = curX; paulo@89: curX += columnWidths[i]; paulo@89: } paulo@89: paulo@89: series paulo@89: .attr('transform', function(d, i) { paulo@89: return 'translate(' + xPositions[i % seriesPerRow] + ',' + (5 + Math.floor(i / seriesPerRow) * versPadding) + ')'; paulo@89: }); paulo@89: paulo@89: //position legend as far right as possible within the total width paulo@89: if (rightAlign) { paulo@89: g.attr('transform', 'translate(' + (width - margin.right - legendWidth) + ',' + margin.top + ')'); paulo@89: } paulo@89: else { paulo@89: g.attr('transform', 'translate(0' + ',' + margin.top + ')'); paulo@89: } paulo@89: paulo@89: height = margin.top + margin.bottom + (Math.ceil(seriesWidths.length / seriesPerRow) * versPadding); paulo@89: paulo@89: } else { paulo@89: paulo@89: var ypos = 5, paulo@89: newxpos = 5, paulo@89: maxwidth = 0, paulo@89: xpos; paulo@89: series paulo@89: .attr('transform', function(d, i) { paulo@89: var length = d3.select(this).select('text').node().getComputedTextLength() + padding; paulo@89: xpos = newxpos; paulo@89: paulo@89: if (width < margin.left + margin.right + xpos + length) { paulo@89: newxpos = xpos = 5; paulo@89: ypos += versPadding; paulo@89: } paulo@89: paulo@89: newxpos += length; paulo@89: if (newxpos > maxwidth) maxwidth = newxpos; paulo@89: paulo@89: return 'translate(' + xpos + ',' + ypos + ')'; paulo@89: }); paulo@89: paulo@89: //position legend as far right as possible within the total width paulo@89: g.attr('transform', 'translate(' + (width - margin.right - maxwidth) + ',' + margin.top + ')'); paulo@89: paulo@89: height = margin.top + margin.bottom + ypos + 15; paulo@89: } paulo@89: paulo@89: if(vers == 'furious') { paulo@89: // Size rectangles after text is placed paulo@89: seriesShape paulo@89: .attr('width', function(d,i) { paulo@89: return seriesText[0][i].getComputedTextLength() + 27; paulo@89: }) paulo@89: .attr('height', 18) paulo@89: .attr('y', -9) paulo@89: .attr('x', -15) paulo@89: } paulo@89: paulo@89: seriesShape paulo@89: .style('fill', setBGColor) paulo@89: .style('stroke', function(d,i) { return d.color || color(d, i) }); paulo@89: }); paulo@89: paulo@89: function setTextColor(d,i) { paulo@89: if(vers != 'furious') return '#000'; paulo@89: if(expanded) { paulo@89: return d.disengaged ? color(d,i) : '#fff'; paulo@89: } else if (!expanded) { paulo@89: return !!d.disabled ? color(d,i) : '#fff'; paulo@89: } paulo@89: } paulo@89: paulo@89: function setBGColor(d,i) { paulo@89: if(expanded && vers == 'furious') { paulo@89: return d.disengaged ? '#fff' : color(d,i); paulo@89: } else { paulo@89: return !!d.disabled ? '#fff' : color(d,i); paulo@89: } paulo@89: } paulo@89: paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: chart.dispatch = dispatch; paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: key: {get: function(){return getKey;}, set: function(_){getKey=_;}}, paulo@89: align: {get: function(){return align;}, set: function(_){align=_;}}, paulo@89: rightAlign: {get: function(){return rightAlign;}, set: function(_){rightAlign=_;}}, paulo@89: padding: {get: function(){return padding;}, set: function(_){padding=_;}}, paulo@89: updateState: {get: function(){return updateState;}, set: function(_){updateState=_;}}, paulo@89: radioButtonMode: {get: function(){return radioButtonMode;}, set: function(_){radioButtonMode=_;}}, paulo@89: expanded: {get: function(){return expanded;}, set: function(_){expanded=_;}}, paulo@89: vers: {get: function(){return vers;}, set: function(_){vers=_;}}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.initOptions(chart); paulo@89: paulo@89: return chart; paulo@89: }; paulo@89: //TODO: consider deprecating and using multibar with single series for this paulo@89: nv.models.historicalBar = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var margin = {top: 0, right: 0, bottom: 0, left: 0} paulo@89: , width = null paulo@89: , height = null paulo@89: , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one paulo@89: , container = null paulo@89: , x = d3.scale.linear() paulo@89: , y = d3.scale.linear() paulo@89: , getX = function(d) { return d.x } paulo@89: , getY = function(d) { return d.y } paulo@89: , forceX = [] paulo@89: , forceY = [0] paulo@89: , padData = false paulo@89: , clipEdge = true paulo@89: , color = nv.utils.defaultColor() paulo@89: , xDomain paulo@89: , yDomain paulo@89: , xRange paulo@89: , yRange paulo@89: , dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd') paulo@89: , interactive = true paulo@89: ; paulo@89: paulo@89: var renderWatch = nv.utils.renderWatch(dispatch, 0); paulo@89: paulo@89: function chart(selection) { paulo@89: selection.each(function(data) { paulo@89: renderWatch.reset(); paulo@89: paulo@89: container = d3.select(this); paulo@89: var availableWidth = nv.utils.availableWidth(width, container, margin), paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: // Setup Scales paulo@89: x.domain(xDomain || d3.extent(data[0].values.map(getX).concat(forceX) )); paulo@89: paulo@89: if (padData) paulo@89: x.range(xRange || [availableWidth * .5 / data[0].values.length, availableWidth * (data[0].values.length - .5) / data[0].values.length ]); paulo@89: else paulo@89: x.range(xRange || [0, availableWidth]); paulo@89: paulo@89: y.domain(yDomain || d3.extent(data[0].values.map(getY).concat(forceY) )) paulo@89: .range(yRange || [availableHeight, 0]); paulo@89: paulo@89: // If scale's domain don't have a range, slightly adjust to make one... so a chart can show a single data point paulo@89: if (x.domain()[0] === x.domain()[1]) paulo@89: x.domain()[0] ? paulo@89: x.domain([x.domain()[0] - x.domain()[0] * 0.01, x.domain()[1] + x.domain()[1] * 0.01]) paulo@89: : x.domain([-1,1]); paulo@89: paulo@89: if (y.domain()[0] === y.domain()[1]) paulo@89: y.domain()[0] ? paulo@89: y.domain([y.domain()[0] + y.domain()[0] * 0.01, y.domain()[1] - y.domain()[1] * 0.01]) paulo@89: : y.domain([-1,1]); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-historicalBar-' + id).data([data[0].values]); paulo@89: var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-historicalBar-' + id); paulo@89: var defsEnter = wrapEnter.append('defs'); paulo@89: var gEnter = wrapEnter.append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-bars'); paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: container paulo@89: .on('click', function(d,i) { paulo@89: dispatch.chartClick({ paulo@89: data: d, paulo@89: index: i, paulo@89: pos: d3.event, paulo@89: id: id paulo@89: }); paulo@89: }); paulo@89: paulo@89: defsEnter.append('clipPath') paulo@89: .attr('id', 'nv-chart-clip-path-' + id) paulo@89: .append('rect'); paulo@89: paulo@89: wrap.select('#nv-chart-clip-path-' + id + ' rect') paulo@89: .attr('width', availableWidth) paulo@89: .attr('height', availableHeight); paulo@89: paulo@89: g.attr('clip-path', clipEdge ? 'url(#nv-chart-clip-path-' + id + ')' : ''); paulo@89: paulo@89: var bars = wrap.select('.nv-bars').selectAll('.nv-bar') paulo@89: .data(function(d) { return d }, function(d,i) {return getX(d,i)}); paulo@89: bars.exit().remove(); paulo@89: paulo@89: bars.enter().append('rect') paulo@89: .attr('x', 0 ) paulo@89: .attr('y', function(d,i) { return nv.utils.NaNtoZero(y(Math.max(0, getY(d,i)))) }) paulo@89: .attr('height', function(d,i) { return nv.utils.NaNtoZero(Math.abs(y(getY(d,i)) - y(0))) }) paulo@89: .attr('transform', function(d,i) { return 'translate(' + (x(getX(d,i)) - availableWidth / data[0].values.length * .45) + ',0)'; }) paulo@89: .on('mouseover', function(d,i) { paulo@89: if (!interactive) return; paulo@89: d3.select(this).classed('hover', true); paulo@89: dispatch.elementMouseover({ paulo@89: data: d, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: paulo@89: }) paulo@89: .on('mouseout', function(d,i) { paulo@89: if (!interactive) return; paulo@89: d3.select(this).classed('hover', false); paulo@89: dispatch.elementMouseout({ paulo@89: data: d, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: }) paulo@89: .on('mousemove', function(d,i) { paulo@89: if (!interactive) return; paulo@89: dispatch.elementMousemove({ paulo@89: data: d, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: }) paulo@89: .on('click', function(d,i) { paulo@89: if (!interactive) return; paulo@89: dispatch.elementClick({ paulo@89: data: d, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: d3.event.stopPropagation(); paulo@89: }) paulo@89: .on('dblclick', function(d,i) { paulo@89: if (!interactive) return; paulo@89: dispatch.elementDblClick({ paulo@89: data: d, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: d3.event.stopPropagation(); paulo@89: }); paulo@89: paulo@89: bars paulo@89: .attr('fill', function(d,i) { return color(d, i); }) paulo@89: .attr('class', function(d,i,j) { return (getY(d,i) < 0 ? 'nv-bar negative' : 'nv-bar positive') + ' nv-bar-' + j + '-' + i }) paulo@89: .watchTransition(renderWatch, 'bars') paulo@89: .attr('transform', function(d,i) { return 'translate(' + (x(getX(d,i)) - availableWidth / data[0].values.length * .45) + ',0)'; }) paulo@89: //TODO: better width calculations that don't assume always uniform data spacing;w paulo@89: .attr('width', (availableWidth / data[0].values.length) * .9 ); paulo@89: paulo@89: bars.watchTransition(renderWatch, 'bars') paulo@89: .attr('y', function(d,i) { paulo@89: var rval = getY(d,i) < 0 ? paulo@89: y(0) : paulo@89: y(0) - y(getY(d,i)) < 1 ? paulo@89: y(0) - 1 : paulo@89: y(getY(d,i)); paulo@89: return nv.utils.NaNtoZero(rval); paulo@89: }) paulo@89: .attr('height', function(d,i) { return nv.utils.NaNtoZero(Math.max(Math.abs(y(getY(d,i)) - y(0)),1)) }); paulo@89: paulo@89: }); paulo@89: paulo@89: renderWatch.renderEnd('historicalBar immediate'); paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //Create methods to allow outside functions to highlight a specific bar. paulo@89: chart.highlightPoint = function(pointIndex, isHoverOver) { paulo@89: container paulo@89: .select(".nv-bars .nv-bar-0-" + pointIndex) paulo@89: .classed("hover", isHoverOver) paulo@89: ; paulo@89: }; paulo@89: paulo@89: chart.clearHighlights = function() { paulo@89: container paulo@89: .select(".nv-bars .nv-bar.hover") paulo@89: .classed("hover", false) paulo@89: ; paulo@89: }; paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: chart.dispatch = dispatch; paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: forceX: {get: function(){return forceX;}, set: function(_){forceX=_;}}, paulo@89: forceY: {get: function(){return forceY;}, set: function(_){forceY=_;}}, paulo@89: padData: {get: function(){return padData;}, set: function(_){padData=_;}}, paulo@89: x: {get: function(){return getX;}, set: function(_){getX=_;}}, paulo@89: y: {get: function(){return getY;}, set: function(_){getY=_;}}, paulo@89: xScale: {get: function(){return x;}, set: function(_){x=_;}}, paulo@89: yScale: {get: function(){return y;}, set: function(_){y=_;}}, paulo@89: xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}}, paulo@89: yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}}, paulo@89: xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}}, paulo@89: yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}}, paulo@89: clipEdge: {get: function(){return clipEdge;}, set: function(_){clipEdge=_;}}, paulo@89: id: {get: function(){return id;}, set: function(_){id=_;}}, paulo@89: interactive: {get: function(){return interactive;}, set: function(_){interactive=_;}}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.initOptions(chart); paulo@89: paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: nv.models.historicalBarChart = function(bar_model) { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var bars = bar_model || nv.models.historicalBar() paulo@89: , xAxis = nv.models.axis() paulo@89: , yAxis = nv.models.axis() paulo@89: , legend = nv.models.legend() paulo@89: , interactiveLayer = nv.interactiveGuideline() paulo@89: , tooltip = nv.models.tooltip() paulo@89: ; paulo@89: paulo@89: paulo@89: var margin = {top: 30, right: 90, bottom: 50, left: 90} paulo@89: , color = nv.utils.defaultColor() paulo@89: , width = null paulo@89: , height = null paulo@89: , showLegend = false paulo@89: , showXAxis = true paulo@89: , showYAxis = true paulo@89: , rightAlignYAxis = false paulo@89: , useInteractiveGuideline = false paulo@89: , x paulo@89: , y paulo@89: , state = {} paulo@89: , defaultState = null paulo@89: , noData = null paulo@89: , dispatch = d3.dispatch('tooltipHide', 'stateChange', 'changeState', 'renderEnd') paulo@89: , transitionDuration = 250 paulo@89: ; paulo@89: paulo@89: xAxis.orient('bottom').tickPadding(7); paulo@89: yAxis.orient( (rightAlignYAxis) ? 'right' : 'left'); paulo@89: tooltip paulo@89: .duration(0) paulo@89: .headerEnabled(false) paulo@89: .valueFormatter(function(d, i) { paulo@89: return yAxis.tickFormat()(d, i); paulo@89: }) paulo@89: .headerFormatter(function(d, i) { paulo@89: return xAxis.tickFormat()(d, i); paulo@89: }); paulo@89: paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var renderWatch = nv.utils.renderWatch(dispatch, 0); paulo@89: paulo@89: function chart(selection) { paulo@89: selection.each(function(data) { paulo@89: renderWatch.reset(); paulo@89: renderWatch.models(bars); paulo@89: if (showXAxis) renderWatch.models(xAxis); paulo@89: if (showYAxis) renderWatch.models(yAxis); paulo@89: paulo@89: var container = d3.select(this), paulo@89: that = this; paulo@89: nv.utils.initSVG(container); paulo@89: var availableWidth = nv.utils.availableWidth(width, container, margin), paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: paulo@89: chart.update = function() { container.transition().duration(transitionDuration).call(chart) }; paulo@89: chart.container = this; paulo@89: paulo@89: //set state.disabled paulo@89: state.disabled = data.map(function(d) { return !!d.disabled }); paulo@89: paulo@89: if (!defaultState) { paulo@89: var key; paulo@89: defaultState = {}; paulo@89: for (key in state) { paulo@89: if (state[key] instanceof Array) paulo@89: defaultState[key] = state[key].slice(0); paulo@89: else paulo@89: defaultState[key] = state[key]; paulo@89: } paulo@89: } paulo@89: paulo@89: // Display noData message if there's nothing to show. paulo@89: if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) { paulo@89: nv.utils.noData(chart, container) paulo@89: return chart; paulo@89: } else { paulo@89: container.selectAll('.nv-noData').remove(); paulo@89: } paulo@89: paulo@89: // Setup Scales paulo@89: x = bars.xScale(); paulo@89: y = bars.yScale(); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-historicalBarChart').data([data]); paulo@89: var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-historicalBarChart').append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-x nv-axis'); paulo@89: gEnter.append('g').attr('class', 'nv-y nv-axis'); paulo@89: gEnter.append('g').attr('class', 'nv-barsWrap'); paulo@89: gEnter.append('g').attr('class', 'nv-legendWrap'); paulo@89: gEnter.append('g').attr('class', 'nv-interactive'); paulo@89: paulo@89: // Legend paulo@89: if (showLegend) { paulo@89: legend.width(availableWidth); paulo@89: paulo@89: g.select('.nv-legendWrap') paulo@89: .datum(data) paulo@89: .call(legend); paulo@89: paulo@89: if ( margin.top != legend.height()) { paulo@89: margin.top = legend.height(); paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: } paulo@89: paulo@89: wrap.select('.nv-legendWrap') paulo@89: .attr('transform', 'translate(0,' + (-margin.top) +')') paulo@89: } paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: if (rightAlignYAxis) { paulo@89: g.select(".nv-y.nv-axis") paulo@89: .attr("transform", "translate(" + availableWidth + ",0)"); paulo@89: } paulo@89: paulo@89: //Set up interactive layer paulo@89: if (useInteractiveGuideline) { paulo@89: interactiveLayer paulo@89: .width(availableWidth) paulo@89: .height(availableHeight) paulo@89: .margin({left:margin.left, top:margin.top}) paulo@89: .svgContainer(container) paulo@89: .xScale(x); paulo@89: wrap.select(".nv-interactive").call(interactiveLayer); paulo@89: } paulo@89: bars paulo@89: .width(availableWidth) paulo@89: .height(availableHeight) paulo@89: .color(data.map(function(d,i) { paulo@89: return d.color || color(d, i); paulo@89: }).filter(function(d,i) { return !data[i].disabled })); paulo@89: paulo@89: var barsWrap = g.select('.nv-barsWrap') paulo@89: .datum(data.filter(function(d) { return !d.disabled })); paulo@89: barsWrap.transition().call(bars); paulo@89: paulo@89: // Setup Axes paulo@89: if (showXAxis) { paulo@89: xAxis paulo@89: .scale(x) paulo@89: ._ticks( nv.utils.calcTicksX(availableWidth/100, data) ) paulo@89: .tickSize(-availableHeight, 0); paulo@89: paulo@89: g.select('.nv-x.nv-axis') paulo@89: .attr('transform', 'translate(0,' + y.range()[0] + ')'); paulo@89: g.select('.nv-x.nv-axis') paulo@89: .transition() paulo@89: .call(xAxis); paulo@89: } paulo@89: paulo@89: if (showYAxis) { paulo@89: yAxis paulo@89: .scale(y) paulo@89: ._ticks( nv.utils.calcTicksY(availableHeight/36, data) ) paulo@89: .tickSize( -availableWidth, 0); paulo@89: paulo@89: g.select('.nv-y.nv-axis') paulo@89: .transition() paulo@89: .call(yAxis); paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (in chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: interactiveLayer.dispatch.on('elementMousemove', function(e) { paulo@89: bars.clearHighlights(); paulo@89: paulo@89: var singlePoint, pointIndex, pointXLocation, allData = []; paulo@89: data paulo@89: .filter(function(series, i) { paulo@89: series.seriesIndex = i; paulo@89: return !series.disabled; paulo@89: }) paulo@89: .forEach(function(series,i) { paulo@89: pointIndex = nv.interactiveBisect(series.values, e.pointXValue, chart.x()); paulo@89: bars.highlightPoint(pointIndex,true); paulo@89: var point = series.values[pointIndex]; paulo@89: if (point === undefined) return; paulo@89: if (singlePoint === undefined) singlePoint = point; paulo@89: if (pointXLocation === undefined) pointXLocation = chart.xScale()(chart.x()(point,pointIndex)); paulo@89: allData.push({ paulo@89: key: series.key, paulo@89: value: chart.y()(point, pointIndex), paulo@89: color: color(series,series.seriesIndex), paulo@89: data: series.values[pointIndex] paulo@89: }); paulo@89: }); paulo@89: paulo@89: var xValue = xAxis.tickFormat()(chart.x()(singlePoint,pointIndex)); paulo@89: interactiveLayer.tooltip paulo@89: .position({left: pointXLocation + margin.left, top: e.mouseY + margin.top}) paulo@89: .chartContainer(that.parentNode) paulo@89: .valueFormatter(function(d,i) { paulo@89: return yAxis.tickFormat()(d); paulo@89: }) paulo@89: .data({ paulo@89: value: xValue, paulo@89: index: pointIndex, paulo@89: series: allData paulo@89: })(); paulo@89: paulo@89: interactiveLayer.renderGuideLine(pointXLocation); paulo@89: paulo@89: }); paulo@89: paulo@89: interactiveLayer.dispatch.on("elementMouseout",function(e) { paulo@89: dispatch.tooltipHide(); paulo@89: bars.clearHighlights(); paulo@89: }); paulo@89: paulo@89: legend.dispatch.on('legendClick', function(d,i) { paulo@89: d.disabled = !d.disabled; paulo@89: paulo@89: if (!data.filter(function(d) { return !d.disabled }).length) { paulo@89: data.map(function(d) { paulo@89: d.disabled = false; paulo@89: wrap.selectAll('.nv-series').classed('disabled', false); paulo@89: return d; paulo@89: }); paulo@89: } paulo@89: paulo@89: state.disabled = data.map(function(d) { return !!d.disabled }); paulo@89: dispatch.stateChange(state); paulo@89: paulo@89: selection.transition().call(chart); paulo@89: }); paulo@89: paulo@89: legend.dispatch.on('legendDblclick', function(d) { paulo@89: //Double clicking should always enable current series, and disabled all others. paulo@89: data.forEach(function(d) { paulo@89: d.disabled = true; paulo@89: }); paulo@89: d.disabled = false; paulo@89: paulo@89: state.disabled = data.map(function(d) { return !!d.disabled }); paulo@89: dispatch.stateChange(state); paulo@89: chart.update(); paulo@89: }); paulo@89: paulo@89: dispatch.on('changeState', function(e) { paulo@89: if (typeof e.disabled !== 'undefined') { paulo@89: data.forEach(function(series,i) { paulo@89: series.disabled = e.disabled[i]; paulo@89: }); paulo@89: paulo@89: state.disabled = e.disabled; paulo@89: } paulo@89: paulo@89: chart.update(); paulo@89: }); paulo@89: }); paulo@89: paulo@89: renderWatch.renderEnd('historicalBarChart immediate'); paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (out of chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: bars.dispatch.on('elementMouseover.tooltip', function(evt) { paulo@89: evt['series'] = { paulo@89: key: chart.x()(evt.data), paulo@89: value: chart.y()(evt.data), paulo@89: color: evt.color paulo@89: }; paulo@89: tooltip.data(evt).hidden(false); paulo@89: }); paulo@89: paulo@89: bars.dispatch.on('elementMouseout.tooltip', function(evt) { paulo@89: tooltip.hidden(true); paulo@89: }); paulo@89: paulo@89: bars.dispatch.on('elementMousemove.tooltip', function(evt) { paulo@89: tooltip.position({top: d3.event.pageY, left: d3.event.pageX})(); paulo@89: }); paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: // expose chart's sub-components paulo@89: chart.dispatch = dispatch; paulo@89: chart.bars = bars; paulo@89: chart.legend = legend; paulo@89: chart.xAxis = xAxis; paulo@89: chart.yAxis = yAxis; paulo@89: chart.interactiveLayer = interactiveLayer; paulo@89: chart.tooltip = tooltip; paulo@89: paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}}, paulo@89: showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}}, paulo@89: showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}}, paulo@89: defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}}, paulo@89: noData: {get: function(){return noData;}, set: function(_){noData=_;}}, paulo@89: paulo@89: // deprecated options paulo@89: tooltips: {get: function(){return tooltip.enabled();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead'); paulo@89: tooltip.enabled(!!_); paulo@89: }}, paulo@89: tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead'); paulo@89: tooltip.contentGenerator(_); paulo@89: }}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: legend.color(color); paulo@89: bars.color(color); paulo@89: }}, paulo@89: duration: {get: function(){return transitionDuration;}, set: function(_){ paulo@89: transitionDuration=_; paulo@89: renderWatch.reset(transitionDuration); paulo@89: yAxis.duration(transitionDuration); paulo@89: xAxis.duration(transitionDuration); paulo@89: }}, paulo@89: rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){ paulo@89: rightAlignYAxis = _; paulo@89: yAxis.orient( (_) ? 'right' : 'left'); paulo@89: }}, paulo@89: useInteractiveGuideline: {get: function(){return useInteractiveGuideline;}, set: function(_){ paulo@89: useInteractiveGuideline = _; paulo@89: if (_ === true) { paulo@89: chart.interactive(false); paulo@89: } paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.inheritOptions(chart, bars); paulo@89: nv.utils.initOptions(chart); paulo@89: paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: paulo@89: // ohlcChart is just a historical chart with ohlc bars and some tweaks paulo@89: nv.models.ohlcBarChart = function() { paulo@89: var chart = nv.models.historicalBarChart(nv.models.ohlcBar()); paulo@89: paulo@89: // special default tooltip since we show multiple values per x paulo@89: chart.useInteractiveGuideline(true); paulo@89: chart.interactiveLayer.tooltip.contentGenerator(function(data) { paulo@89: // we assume only one series exists for this chart paulo@89: var d = data.series[0].data; paulo@89: // match line colors as defined in nv.d3.css paulo@89: var color = d.open < d.close ? "2ca02c" : "d62728"; paulo@89: return '' + paulo@89: '

' + data.value + '

' + paulo@89: '' + paulo@89: '' + paulo@89: '' + paulo@89: '' + paulo@89: '' + paulo@89: '
open:' + chart.yAxis.tickFormat()(d.open) + '
close:' + chart.yAxis.tickFormat()(d.close) + '
high' + chart.yAxis.tickFormat()(d.high) + '
low:' + chart.yAxis.tickFormat()(d.low) + '
'; paulo@89: }); paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: // candlestickChart is just a historical chart with candlestick bars and some tweaks paulo@89: nv.models.candlestickBarChart = function() { paulo@89: var chart = nv.models.historicalBarChart(nv.models.candlestickBar()); paulo@89: paulo@89: // special default tooltip since we show multiple values per x paulo@89: chart.useInteractiveGuideline(true); paulo@89: chart.interactiveLayer.tooltip.contentGenerator(function(data) { paulo@89: // we assume only one series exists for this chart paulo@89: var d = data.series[0].data; paulo@89: // match line colors as defined in nv.d3.css paulo@89: var color = d.open < d.close ? "2ca02c" : "d62728"; paulo@89: return '' + paulo@89: '

' + data.value + '

' + paulo@89: '' + paulo@89: '' + paulo@89: '' + paulo@89: '' + paulo@89: '' + paulo@89: '
open:' + chart.yAxis.tickFormat()(d.open) + '
close:' + chart.yAxis.tickFormat()(d.close) + '
high' + chart.yAxis.tickFormat()(d.high) + '
low:' + chart.yAxis.tickFormat()(d.low) + '
'; paulo@89: }); paulo@89: return chart; paulo@89: }; paulo@89: nv.models.legend = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var margin = {top: 5, right: 0, bottom: 5, left: 0} paulo@89: , width = 400 paulo@89: , height = 20 paulo@89: , getKey = function(d) { return d.key } paulo@89: , color = nv.utils.getColor() paulo@89: , align = true paulo@89: , padding = 32 //define how much space between legend items. - recommend 32 for furious version paulo@89: , rightAlign = true paulo@89: , updateState = true //If true, legend will update data.disabled and trigger a 'stateChange' dispatch. paulo@89: , radioButtonMode = false //If true, clicking legend items will cause it to behave like a radio button. (only one can be selected at a time) paulo@89: , expanded = false paulo@89: , dispatch = d3.dispatch('legendClick', 'legendDblclick', 'legendMouseover', 'legendMouseout', 'stateChange') paulo@89: , vers = 'classic' //Options are "classic" and "furious" paulo@89: ; paulo@89: paulo@89: function chart(selection) { paulo@89: selection.each(function(data) { paulo@89: var availableWidth = width - margin.left - margin.right, paulo@89: container = d3.select(this); paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-legend').data([data]); paulo@89: var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-legend').append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: var series = g.selectAll('.nv-series') paulo@89: .data(function(d) { paulo@89: if(vers != 'furious') return d; paulo@89: paulo@89: return d.filter(function(n) { paulo@89: return expanded ? true : !n.disengaged; paulo@89: }); paulo@89: }); paulo@89: paulo@89: var seriesEnter = series.enter().append('g').attr('class', 'nv-series'); paulo@89: var seriesShape; paulo@89: paulo@89: var versPadding; paulo@89: switch(vers) { paulo@89: case 'furious' : paulo@89: versPadding = 23; paulo@89: break; paulo@89: case 'classic' : paulo@89: versPadding = 20; paulo@89: } paulo@89: paulo@89: if(vers == 'classic') { paulo@89: seriesEnter.append('circle') paulo@89: .style('stroke-width', 2) paulo@89: .attr('class','nv-legend-symbol') paulo@89: .attr('r', 5); paulo@89: paulo@89: seriesShape = series.select('circle'); paulo@89: } else if (vers == 'furious') { paulo@89: seriesEnter.append('rect') paulo@89: .style('stroke-width', 2) paulo@89: .attr('class','nv-legend-symbol') paulo@89: .attr('rx', 3) paulo@89: .attr('ry', 3); paulo@89: paulo@89: seriesShape = series.select('.nv-legend-symbol'); paulo@89: paulo@89: seriesEnter.append('g') paulo@89: .attr('class', 'nv-check-box') paulo@89: .property('innerHTML','') paulo@89: .attr('transform', 'translate(-10,-8)scale(0.5)'); paulo@89: paulo@89: var seriesCheckbox = series.select('.nv-check-box'); paulo@89: paulo@89: seriesCheckbox.each(function(d,i) { paulo@89: d3.select(this).selectAll('path') paulo@89: .attr('stroke', setTextColor(d,i)); paulo@89: }); paulo@89: } paulo@89: paulo@89: seriesEnter.append('text') paulo@89: .attr('text-anchor', 'start') paulo@89: .attr('class','nv-legend-text') paulo@89: .attr('dy', '.32em') paulo@89: .attr('dx', '8'); paulo@89: paulo@89: var seriesText = series.select('text.nv-legend-text'); paulo@89: paulo@89: series paulo@89: .on('mouseover', function(d,i) { paulo@89: dispatch.legendMouseover(d,i); //TODO: Make consistent with other event objects paulo@89: }) paulo@89: .on('mouseout', function(d,i) { paulo@89: dispatch.legendMouseout(d,i); paulo@89: }) paulo@89: .on('click', function(d,i) { paulo@89: dispatch.legendClick(d,i); paulo@89: // make sure we re-get data in case it was modified paulo@89: var data = series.data(); paulo@89: if (updateState) { paulo@89: if(vers =='classic') { paulo@89: if (radioButtonMode) { paulo@89: //Radio button mode: set every series to disabled, paulo@89: // and enable the clicked series. paulo@89: data.forEach(function(series) { series.disabled = true}); paulo@89: d.disabled = false; paulo@89: } paulo@89: else { paulo@89: d.disabled = !d.disabled; paulo@89: if (data.every(function(series) { return series.disabled})) { paulo@89: //the default behavior of NVD3 legends is, if every single series paulo@89: // is disabled, turn all series' back on. paulo@89: data.forEach(function(series) { series.disabled = false}); paulo@89: } paulo@89: } paulo@89: } else if(vers == 'furious') { paulo@89: if(expanded) { paulo@89: d.disengaged = !d.disengaged; paulo@89: d.userDisabled = d.userDisabled == undefined ? !!d.disabled : d.userDisabled; paulo@89: d.disabled = d.disengaged || d.userDisabled; paulo@89: } else if (!expanded) { paulo@89: d.disabled = !d.disabled; paulo@89: d.userDisabled = d.disabled; paulo@89: var engaged = data.filter(function(d) { return !d.disengaged; }); paulo@89: if (engaged.every(function(series) { return series.userDisabled })) { paulo@89: //the default behavior of NVD3 legends is, if every single series paulo@89: // is disabled, turn all series' back on. paulo@89: data.forEach(function(series) { paulo@89: series.disabled = series.userDisabled = false; paulo@89: }); paulo@89: } paulo@89: } paulo@89: } paulo@89: dispatch.stateChange({ paulo@89: disabled: data.map(function(d) { return !!d.disabled }), paulo@89: disengaged: data.map(function(d) { return !!d.disengaged }) paulo@89: }); paulo@89: paulo@89: } paulo@89: }) paulo@89: .on('dblclick', function(d,i) { paulo@89: if(vers == 'furious' && expanded) return; paulo@89: dispatch.legendDblclick(d,i); paulo@89: if (updateState) { paulo@89: // make sure we re-get data in case it was modified paulo@89: var data = series.data(); paulo@89: //the default behavior of NVD3 legends, when double clicking one, paulo@89: // is to set all other series' to false, and make the double clicked series enabled. paulo@89: data.forEach(function(series) { paulo@89: series.disabled = true; paulo@89: if(vers == 'furious') series.userDisabled = series.disabled; paulo@89: }); paulo@89: d.disabled = false; paulo@89: if(vers == 'furious') d.userDisabled = d.disabled; paulo@89: dispatch.stateChange({ paulo@89: disabled: data.map(function(d) { return !!d.disabled }) paulo@89: }); paulo@89: } paulo@89: }); paulo@89: paulo@89: series.classed('nv-disabled', function(d) { return d.userDisabled }); paulo@89: series.exit().remove(); paulo@89: paulo@89: seriesText paulo@89: .attr('fill', setTextColor) paulo@89: .text(getKey); paulo@89: paulo@89: //TODO: implement fixed-width and max-width options (max-width is especially useful with the align option) paulo@89: // NEW ALIGNING CODE, TODO: clean up paulo@89: var legendWidth = 0; paulo@89: if (align) { paulo@89: paulo@89: var seriesWidths = []; paulo@89: series.each(function(d,i) { paulo@89: var legendText = d3.select(this).select('text'); paulo@89: var nodeTextLength; paulo@89: try { paulo@89: nodeTextLength = legendText.node().getComputedTextLength(); paulo@89: // If the legendText is display:none'd (nodeTextLength == 0), simulate an error so we approximate, instead paulo@89: if(nodeTextLength <= 0) throw Error(); paulo@89: } paulo@89: catch(e) { paulo@89: nodeTextLength = nv.utils.calcApproxTextWidth(legendText); paulo@89: } paulo@89: paulo@89: seriesWidths.push(nodeTextLength + padding); paulo@89: }); paulo@89: paulo@89: var seriesPerRow = 0; paulo@89: var columnWidths = []; paulo@89: legendWidth = 0; paulo@89: paulo@89: while ( legendWidth < availableWidth && seriesPerRow < seriesWidths.length) { paulo@89: columnWidths[seriesPerRow] = seriesWidths[seriesPerRow]; paulo@89: legendWidth += seriesWidths[seriesPerRow++]; paulo@89: } paulo@89: if (seriesPerRow === 0) seriesPerRow = 1; //minimum of one series per row paulo@89: paulo@89: while ( legendWidth > availableWidth && seriesPerRow > 1 ) { paulo@89: columnWidths = []; paulo@89: seriesPerRow--; paulo@89: paulo@89: for (var k = 0; k < seriesWidths.length; k++) { paulo@89: if (seriesWidths[k] > (columnWidths[k % seriesPerRow] || 0) ) paulo@89: columnWidths[k % seriesPerRow] = seriesWidths[k]; paulo@89: } paulo@89: paulo@89: legendWidth = columnWidths.reduce(function(prev, cur, index, array) { paulo@89: return prev + cur; paulo@89: }); paulo@89: } paulo@89: paulo@89: var xPositions = []; paulo@89: for (var i = 0, curX = 0; i < seriesPerRow; i++) { paulo@89: xPositions[i] = curX; paulo@89: curX += columnWidths[i]; paulo@89: } paulo@89: paulo@89: series paulo@89: .attr('transform', function(d, i) { paulo@89: return 'translate(' + xPositions[i % seriesPerRow] + ',' + (5 + Math.floor(i / seriesPerRow) * versPadding) + ')'; paulo@89: }); paulo@89: paulo@89: //position legend as far right as possible within the total width paulo@89: if (rightAlign) { paulo@89: g.attr('transform', 'translate(' + (width - margin.right - legendWidth) + ',' + margin.top + ')'); paulo@89: } paulo@89: else { paulo@89: g.attr('transform', 'translate(0' + ',' + margin.top + ')'); paulo@89: } paulo@89: paulo@89: height = margin.top + margin.bottom + (Math.ceil(seriesWidths.length / seriesPerRow) * versPadding); paulo@89: paulo@89: } else { paulo@89: paulo@89: var ypos = 5, paulo@89: newxpos = 5, paulo@89: maxwidth = 0, paulo@89: xpos; paulo@89: series paulo@89: .attr('transform', function(d, i) { paulo@89: var length = d3.select(this).select('text').node().getComputedTextLength() + padding; paulo@89: xpos = newxpos; paulo@89: paulo@89: if (width < margin.left + margin.right + xpos + length) { paulo@89: newxpos = xpos = 5; paulo@89: ypos += versPadding; paulo@89: } paulo@89: paulo@89: newxpos += length; paulo@89: if (newxpos > maxwidth) maxwidth = newxpos; paulo@89: paulo@89: if(legendWidth < xpos + maxwidth) { paulo@89: legendWidth = xpos + maxwidth; paulo@89: } paulo@89: return 'translate(' + xpos + ',' + ypos + ')'; paulo@89: }); paulo@89: paulo@89: //position legend as far right as possible within the total width paulo@89: g.attr('transform', 'translate(' + (width - margin.right - maxwidth) + ',' + margin.top + ')'); paulo@89: paulo@89: height = margin.top + margin.bottom + ypos + 15; paulo@89: } paulo@89: paulo@89: if(vers == 'furious') { paulo@89: // Size rectangles after text is placed paulo@89: seriesShape paulo@89: .attr('width', function(d,i) { paulo@89: return seriesText[0][i].getComputedTextLength() + 27; paulo@89: }) paulo@89: .attr('height', 18) paulo@89: .attr('y', -9) paulo@89: .attr('x', -15); paulo@89: paulo@89: // The background for the expanded legend (UI) paulo@89: gEnter.insert('rect',':first-child') paulo@89: .attr('class', 'nv-legend-bg') paulo@89: .attr('fill', '#eee') paulo@89: // .attr('stroke', '#444') paulo@89: .attr('opacity',0); paulo@89: paulo@89: var seriesBG = g.select('.nv-legend-bg'); paulo@89: paulo@89: seriesBG paulo@89: .transition().duration(300) paulo@89: .attr('x', -versPadding ) paulo@89: .attr('width', legendWidth + versPadding - 12) paulo@89: .attr('height', height + 10) paulo@89: .attr('y', -margin.top - 10) paulo@89: .attr('opacity', expanded ? 1 : 0); paulo@89: paulo@89: paulo@89: } paulo@89: paulo@89: seriesShape paulo@89: .style('fill', setBGColor) paulo@89: .style('fill-opacity', setBGOpacity) paulo@89: .style('stroke', setBGColor); paulo@89: }); paulo@89: paulo@89: function setTextColor(d,i) { paulo@89: if(vers != 'furious') return '#000'; paulo@89: if(expanded) { paulo@89: return d.disengaged ? '#000' : '#fff'; paulo@89: } else if (!expanded) { paulo@89: if(!d.color) d.color = color(d,i); paulo@89: return !!d.disabled ? d.color : '#fff'; paulo@89: } paulo@89: } paulo@89: paulo@89: function setBGColor(d,i) { paulo@89: if(expanded && vers == 'furious') { paulo@89: return d.disengaged ? '#eee' : d.color || color(d,i); paulo@89: } else { paulo@89: return d.color || color(d,i); paulo@89: } paulo@89: } paulo@89: paulo@89: paulo@89: function setBGOpacity(d,i) { paulo@89: if(expanded && vers == 'furious') { paulo@89: return 1; paulo@89: } else { paulo@89: return !!d.disabled ? 0 : 1; paulo@89: } paulo@89: } paulo@89: paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: chart.dispatch = dispatch; paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: key: {get: function(){return getKey;}, set: function(_){getKey=_;}}, paulo@89: align: {get: function(){return align;}, set: function(_){align=_;}}, paulo@89: rightAlign: {get: function(){return rightAlign;}, set: function(_){rightAlign=_;}}, paulo@89: padding: {get: function(){return padding;}, set: function(_){padding=_;}}, paulo@89: updateState: {get: function(){return updateState;}, set: function(_){updateState=_;}}, paulo@89: radioButtonMode: {get: function(){return radioButtonMode;}, set: function(_){radioButtonMode=_;}}, paulo@89: expanded: {get: function(){return expanded;}, set: function(_){expanded=_;}}, paulo@89: vers: {get: function(){return vers;}, set: function(_){vers=_;}}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.initOptions(chart); paulo@89: paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: nv.models.line = function() { paulo@89: "use strict"; paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var scatter = nv.models.scatter() paulo@89: ; paulo@89: paulo@89: var margin = {top: 0, right: 0, bottom: 0, left: 0} paulo@89: , width = 960 paulo@89: , height = 500 paulo@89: , container = null paulo@89: , strokeWidth = 1.5 paulo@89: , color = nv.utils.defaultColor() // a function that returns a color paulo@89: , getX = function(d) { return d.x } // accessor to get the x value from a data point paulo@89: , getY = function(d) { return d.y } // accessor to get the y value from a data point paulo@89: , 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 paulo@89: , isArea = function(d) { return d.area } // decides if a line is an area or just a line paulo@89: , clipEdge = false // if true, masks lines within x and y scale paulo@89: , x //can be accessed via chart.xScale() paulo@89: , y //can be accessed via chart.yScale() paulo@89: , interpolate = "linear" // controls the line interpolation paulo@89: , duration = 250 paulo@89: , dispatch = d3.dispatch('elementClick', 'elementMouseover', 'elementMouseout', 'renderEnd') paulo@89: ; paulo@89: paulo@89: scatter paulo@89: .pointSize(16) // default size paulo@89: .pointDomain([16,256]) //set to speed up calculation, needs to be unset if there is a custom size accessor paulo@89: ; paulo@89: paulo@89: //============================================================ paulo@89: paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var x0, y0 //used to store previous scales paulo@89: , renderWatch = nv.utils.renderWatch(dispatch, duration) paulo@89: ; paulo@89: paulo@89: //============================================================ paulo@89: paulo@89: paulo@89: function chart(selection) { paulo@89: renderWatch.reset(); paulo@89: renderWatch.models(scatter); paulo@89: selection.each(function(data) { paulo@89: container = d3.select(this); paulo@89: var availableWidth = nv.utils.availableWidth(width, container, margin), paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: // Setup Scales paulo@89: x = scatter.xScale(); paulo@89: y = scatter.yScale(); paulo@89: paulo@89: x0 = x0 || x; paulo@89: y0 = y0 || y; paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-line').data([data]); paulo@89: var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-line'); paulo@89: var defsEnter = wrapEnter.append('defs'); paulo@89: var gEnter = wrapEnter.append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-groups'); paulo@89: gEnter.append('g').attr('class', 'nv-scatterWrap'); paulo@89: paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: scatter paulo@89: .width(availableWidth) paulo@89: .height(availableHeight); paulo@89: paulo@89: var scatterWrap = wrap.select('.nv-scatterWrap'); paulo@89: scatterWrap.call(scatter); paulo@89: paulo@89: defsEnter.append('clipPath') paulo@89: .attr('id', 'nv-edge-clip-' + scatter.id()) paulo@89: .append('rect'); paulo@89: paulo@89: wrap.select('#nv-edge-clip-' + scatter.id() + ' rect') paulo@89: .attr('width', availableWidth) paulo@89: .attr('height', (availableHeight > 0) ? availableHeight : 0); paulo@89: paulo@89: g .attr('clip-path', clipEdge ? 'url(#nv-edge-clip-' + scatter.id() + ')' : ''); paulo@89: scatterWrap paulo@89: .attr('clip-path', clipEdge ? 'url(#nv-edge-clip-' + scatter.id() + ')' : ''); paulo@89: paulo@89: var groups = wrap.select('.nv-groups').selectAll('.nv-group') paulo@89: .data(function(d) { return d }, function(d) { return d.key }); paulo@89: groups.enter().append('g') paulo@89: .style('stroke-opacity', 1e-6) paulo@89: .style('stroke-width', function(d) { return d.strokeWidth || strokeWidth }) paulo@89: .style('fill-opacity', 1e-6); paulo@89: paulo@89: groups.exit().remove(); paulo@89: paulo@89: groups paulo@89: .attr('class', function(d,i) { paulo@89: return (d.classed || '') + ' nv-group nv-series-' + i; paulo@89: }) paulo@89: .classed('hover', function(d) { return d.hover }) paulo@89: .style('fill', function(d,i){ return color(d, i) }) paulo@89: .style('stroke', function(d,i){ return color(d, i)}); paulo@89: groups.watchTransition(renderWatch, 'line: groups') paulo@89: .style('stroke-opacity', 1) paulo@89: .style('fill-opacity', function(d) { return d.fillOpacity || .5}); paulo@89: paulo@89: var areaPaths = groups.selectAll('path.nv-area') paulo@89: .data(function(d) { return isArea(d) ? [d] : [] }); // this is done differently than lines because I need to check if series is an area paulo@89: areaPaths.enter().append('path') paulo@89: .attr('class', 'nv-area') paulo@89: .attr('d', function(d) { paulo@89: return d3.svg.area() paulo@89: .interpolate(interpolate) paulo@89: .defined(defined) paulo@89: .x(function(d,i) { return nv.utils.NaNtoZero(x0(getX(d,i))) }) paulo@89: .y0(function(d,i) { return nv.utils.NaNtoZero(y0(getY(d,i))) }) paulo@89: .y1(function(d,i) { return y0( y.domain()[0] <= 0 ? y.domain()[1] >= 0 ? 0 : y.domain()[1] : y.domain()[0] ) }) paulo@89: //.y1(function(d,i) { return y0(0) }) //assuming 0 is within y domain.. may need to tweak this paulo@89: .apply(this, [d.values]) paulo@89: }); paulo@89: groups.exit().selectAll('path.nv-area') paulo@89: .remove(); paulo@89: paulo@89: areaPaths.watchTransition(renderWatch, 'line: areaPaths') paulo@89: .attr('d', function(d) { paulo@89: return d3.svg.area() paulo@89: .interpolate(interpolate) paulo@89: .defined(defined) paulo@89: .x(function(d,i) { return nv.utils.NaNtoZero(x(getX(d,i))) }) paulo@89: .y0(function(d,i) { return nv.utils.NaNtoZero(y(getY(d,i))) }) paulo@89: .y1(function(d,i) { return y( y.domain()[0] <= 0 ? y.domain()[1] >= 0 ? 0 : y.domain()[1] : y.domain()[0] ) }) paulo@89: //.y1(function(d,i) { return y0(0) }) //assuming 0 is within y domain.. may need to tweak this paulo@89: .apply(this, [d.values]) paulo@89: }); paulo@89: paulo@89: var linePaths = groups.selectAll('path.nv-line') paulo@89: .data(function(d) { return [d.values] }); paulo@89: paulo@89: linePaths.enter().append('path') paulo@89: .attr('class', 'nv-line') paulo@89: .attr('d', paulo@89: d3.svg.line() paulo@89: .interpolate(interpolate) paulo@89: .defined(defined) paulo@89: .x(function(d,i) { return nv.utils.NaNtoZero(x0(getX(d,i))) }) paulo@89: .y(function(d,i) { return nv.utils.NaNtoZero(y0(getY(d,i))) }) paulo@89: ); paulo@89: paulo@89: linePaths.watchTransition(renderWatch, 'line: linePaths') paulo@89: .attr('d', paulo@89: d3.svg.line() paulo@89: .interpolate(interpolate) paulo@89: .defined(defined) paulo@89: .x(function(d,i) { return nv.utils.NaNtoZero(x(getX(d,i))) }) paulo@89: .y(function(d,i) { return nv.utils.NaNtoZero(y(getY(d,i))) }) paulo@89: ); paulo@89: paulo@89: //store old scales for use in transitions on update paulo@89: x0 = x.copy(); paulo@89: y0 = y.copy(); paulo@89: }); paulo@89: renderWatch.renderEnd('line immediate'); paulo@89: return chart; paulo@89: } paulo@89: paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: chart.dispatch = dispatch; paulo@89: chart.scatter = scatter; paulo@89: // Pass through events paulo@89: scatter.dispatch.on('elementClick', function(){ dispatch.elementClick.apply(this, arguments); }); paulo@89: scatter.dispatch.on('elementMouseover', function(){ dispatch.elementMouseover.apply(this, arguments); }); paulo@89: scatter.dispatch.on('elementMouseout', function(){ dispatch.elementMouseout.apply(this, arguments); }); paulo@89: paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: defined: {get: function(){return defined;}, set: function(_){defined=_;}}, paulo@89: interpolate: {get: function(){return interpolate;}, set: function(_){interpolate=_;}}, paulo@89: clipEdge: {get: function(){return clipEdge;}, set: function(_){clipEdge=_;}}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: duration: {get: function(){return duration;}, set: function(_){ paulo@89: duration = _; paulo@89: renderWatch.reset(duration); paulo@89: scatter.duration(duration); paulo@89: }}, paulo@89: isArea: {get: function(){return isArea;}, set: function(_){ paulo@89: isArea = d3.functor(_); paulo@89: }}, paulo@89: x: {get: function(){return getX;}, set: function(_){ paulo@89: getX = _; paulo@89: scatter.x(_); paulo@89: }}, paulo@89: y: {get: function(){return getY;}, set: function(_){ paulo@89: getY = _; paulo@89: scatter.y(_); paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: scatter.color(color); paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.inheritOptions(chart, scatter); paulo@89: nv.utils.initOptions(chart); paulo@89: paulo@89: return chart; paulo@89: }; paulo@89: nv.models.lineChart = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var lines = nv.models.line() paulo@89: , xAxis = nv.models.axis() paulo@89: , yAxis = nv.models.axis() paulo@89: , legend = nv.models.legend() paulo@89: , interactiveLayer = nv.interactiveGuideline() paulo@89: , tooltip = nv.models.tooltip() paulo@89: ; paulo@89: paulo@89: var margin = {top: 30, right: 20, bottom: 50, left: 60} paulo@89: , color = nv.utils.defaultColor() paulo@89: , width = null paulo@89: , height = null paulo@89: , showLegend = true paulo@89: , showXAxis = true paulo@89: , showYAxis = true paulo@89: , rightAlignYAxis = false paulo@89: , useInteractiveGuideline = false paulo@89: , x paulo@89: , y paulo@89: , state = nv.utils.state() paulo@89: , defaultState = null paulo@89: , noData = null paulo@89: , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'stateChange', 'changeState', 'renderEnd') paulo@89: , duration = 250 paulo@89: ; paulo@89: paulo@89: // set options on sub-objects for this chart paulo@89: xAxis.orient('bottom').tickPadding(7); paulo@89: yAxis.orient(rightAlignYAxis ? 'right' : 'left'); paulo@89: tooltip.valueFormatter(function(d, i) { paulo@89: return yAxis.tickFormat()(d, i); paulo@89: }).headerFormatter(function(d, i) { paulo@89: return xAxis.tickFormat()(d, i); paulo@89: }); paulo@89: paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var renderWatch = nv.utils.renderWatch(dispatch, duration); paulo@89: paulo@89: var stateGetter = function(data) { paulo@89: return function(){ paulo@89: return { paulo@89: active: data.map(function(d) { return !d.disabled }) paulo@89: }; paulo@89: } paulo@89: }; paulo@89: paulo@89: var stateSetter = function(data) { paulo@89: return function(state) { paulo@89: if (state.active !== undefined) paulo@89: data.forEach(function(series,i) { paulo@89: series.disabled = !state.active[i]; paulo@89: }); paulo@89: } paulo@89: }; paulo@89: paulo@89: function chart(selection) { paulo@89: renderWatch.reset(); paulo@89: renderWatch.models(lines); paulo@89: if (showXAxis) renderWatch.models(xAxis); paulo@89: if (showYAxis) renderWatch.models(yAxis); paulo@89: paulo@89: selection.each(function(data) { paulo@89: var container = d3.select(this), paulo@89: that = this; paulo@89: nv.utils.initSVG(container); paulo@89: var availableWidth = nv.utils.availableWidth(width, container, margin), paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: paulo@89: chart.update = function() { paulo@89: if (duration === 0) paulo@89: container.call(chart); paulo@89: else paulo@89: container.transition().duration(duration).call(chart) paulo@89: }; paulo@89: chart.container = this; paulo@89: paulo@89: state paulo@89: .setter(stateSetter(data), chart.update) paulo@89: .getter(stateGetter(data)) paulo@89: .update(); paulo@89: paulo@89: // DEPRECATED set state.disableddisabled paulo@89: state.disabled = data.map(function(d) { return !!d.disabled }); paulo@89: paulo@89: if (!defaultState) { paulo@89: var key; paulo@89: defaultState = {}; paulo@89: for (key in state) { paulo@89: if (state[key] instanceof Array) paulo@89: defaultState[key] = state[key].slice(0); paulo@89: else paulo@89: defaultState[key] = state[key]; paulo@89: } paulo@89: } paulo@89: paulo@89: // Display noData message if there's nothing to show. paulo@89: if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) { paulo@89: nv.utils.noData(chart, container) paulo@89: return chart; paulo@89: } else { paulo@89: container.selectAll('.nv-noData').remove(); paulo@89: } paulo@89: paulo@89: paulo@89: // Setup Scales paulo@89: x = lines.xScale(); paulo@89: y = lines.yScale(); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-lineChart').data([data]); paulo@89: var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-lineChart').append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append("rect").style("opacity",0); paulo@89: gEnter.append('g').attr('class', 'nv-x nv-axis'); paulo@89: gEnter.append('g').attr('class', 'nv-y nv-axis'); paulo@89: gEnter.append('g').attr('class', 'nv-linesWrap'); paulo@89: gEnter.append('g').attr('class', 'nv-legendWrap'); paulo@89: gEnter.append('g').attr('class', 'nv-interactive'); paulo@89: paulo@89: g.select("rect") paulo@89: .attr("width",availableWidth) paulo@89: .attr("height",(availableHeight > 0) ? availableHeight : 0); paulo@89: paulo@89: // Legend paulo@89: if (showLegend) { paulo@89: legend.width(availableWidth); paulo@89: paulo@89: g.select('.nv-legendWrap') paulo@89: .datum(data) paulo@89: .call(legend); paulo@89: paulo@89: if ( margin.top != legend.height()) { paulo@89: margin.top = legend.height(); paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: } paulo@89: paulo@89: wrap.select('.nv-legendWrap') paulo@89: .attr('transform', 'translate(0,' + (-margin.top) +')') paulo@89: } paulo@89: paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: if (rightAlignYAxis) { paulo@89: g.select(".nv-y.nv-axis") paulo@89: .attr("transform", "translate(" + availableWidth + ",0)"); paulo@89: } paulo@89: paulo@89: //Set up interactive layer paulo@89: if (useInteractiveGuideline) { paulo@89: interactiveLayer paulo@89: .width(availableWidth) paulo@89: .height(availableHeight) paulo@89: .margin({left:margin.left, top:margin.top}) paulo@89: .svgContainer(container) paulo@89: .xScale(x); paulo@89: wrap.select(".nv-interactive").call(interactiveLayer); paulo@89: } paulo@89: paulo@89: lines paulo@89: .width(availableWidth) paulo@89: .height(availableHeight) paulo@89: .color(data.map(function(d,i) { paulo@89: return d.color || color(d, i); paulo@89: }).filter(function(d,i) { return !data[i].disabled })); paulo@89: paulo@89: paulo@89: var linesWrap = g.select('.nv-linesWrap') paulo@89: .datum(data.filter(function(d) { return !d.disabled })); paulo@89: paulo@89: linesWrap.call(lines); paulo@89: paulo@89: // Setup Axes paulo@89: if (showXAxis) { paulo@89: xAxis paulo@89: .scale(x) paulo@89: ._ticks(nv.utils.calcTicksX(availableWidth/100, data) ) paulo@89: .tickSize(-availableHeight, 0); paulo@89: paulo@89: g.select('.nv-x.nv-axis') paulo@89: .attr('transform', 'translate(0,' + y.range()[0] + ')'); paulo@89: g.select('.nv-x.nv-axis') paulo@89: .call(xAxis); paulo@89: } paulo@89: paulo@89: if (showYAxis) { paulo@89: yAxis paulo@89: .scale(y) paulo@89: ._ticks(nv.utils.calcTicksY(availableHeight/36, data) ) paulo@89: .tickSize( -availableWidth, 0); paulo@89: paulo@89: g.select('.nv-y.nv-axis') paulo@89: .call(yAxis); paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (in chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: legend.dispatch.on('stateChange', function(newState) { paulo@89: for (var key in newState) paulo@89: state[key] = newState[key]; paulo@89: dispatch.stateChange(state); paulo@89: chart.update(); paulo@89: }); paulo@89: paulo@89: interactiveLayer.dispatch.on('elementMousemove', function(e) { paulo@89: lines.clearHighlights(); paulo@89: var singlePoint, pointIndex, pointXLocation, allData = []; paulo@89: data paulo@89: .filter(function(series, i) { paulo@89: series.seriesIndex = i; paulo@89: return !series.disabled; paulo@89: }) paulo@89: .forEach(function(series,i) { paulo@89: pointIndex = nv.interactiveBisect(series.values, e.pointXValue, chart.x()); paulo@89: var point = series.values[pointIndex]; paulo@89: var pointYValue = chart.y()(point, pointIndex); paulo@89: if (pointYValue != null) { paulo@89: lines.highlightPoint(i, pointIndex, true); paulo@89: } paulo@89: if (point === undefined) return; paulo@89: if (singlePoint === undefined) singlePoint = point; paulo@89: if (pointXLocation === undefined) pointXLocation = chart.xScale()(chart.x()(point,pointIndex)); paulo@89: allData.push({ paulo@89: key: series.key, paulo@89: value: pointYValue, paulo@89: color: color(series,series.seriesIndex) paulo@89: }); paulo@89: }); paulo@89: //Highlight the tooltip entry based on which point the mouse is closest to. paulo@89: if (allData.length > 2) { paulo@89: var yValue = chart.yScale().invert(e.mouseY); paulo@89: var domainExtent = Math.abs(chart.yScale().domain()[0] - chart.yScale().domain()[1]); paulo@89: var threshold = 0.03 * domainExtent; paulo@89: var indexToHighlight = nv.nearestValueIndex(allData.map(function(d){return d.value}),yValue,threshold); paulo@89: if (indexToHighlight !== null) paulo@89: allData[indexToHighlight].highlight = true; paulo@89: } paulo@89: paulo@89: var xValue = xAxis.tickFormat()(chart.x()(singlePoint,pointIndex)); paulo@89: interactiveLayer.tooltip paulo@89: .position({left: e.mouseX + margin.left, top: e.mouseY + margin.top}) paulo@89: .chartContainer(that.parentNode) paulo@89: .valueFormatter(function(d,i) { paulo@89: return d == null ? "N/A" : yAxis.tickFormat()(d); paulo@89: }) paulo@89: .data({ paulo@89: value: xValue, paulo@89: index: pointIndex, paulo@89: series: allData paulo@89: })(); paulo@89: paulo@89: interactiveLayer.renderGuideLine(pointXLocation); paulo@89: paulo@89: }); paulo@89: paulo@89: interactiveLayer.dispatch.on('elementClick', function(e) { paulo@89: var pointXLocation, allData = []; paulo@89: paulo@89: data.filter(function(series, i) { paulo@89: series.seriesIndex = i; paulo@89: return !series.disabled; paulo@89: }).forEach(function(series) { paulo@89: var pointIndex = nv.interactiveBisect(series.values, e.pointXValue, chart.x()); paulo@89: var point = series.values[pointIndex]; paulo@89: if (typeof point === 'undefined') return; paulo@89: if (typeof pointXLocation === 'undefined') pointXLocation = chart.xScale()(chart.x()(point,pointIndex)); paulo@89: var yPos = chart.yScale()(chart.y()(point,pointIndex)); paulo@89: allData.push({ paulo@89: point: point, paulo@89: pointIndex: pointIndex, paulo@89: pos: [pointXLocation, yPos], paulo@89: seriesIndex: series.seriesIndex, paulo@89: series: series paulo@89: }); paulo@89: }); paulo@89: paulo@89: lines.dispatch.elementClick(allData); paulo@89: }); paulo@89: paulo@89: interactiveLayer.dispatch.on("elementMouseout",function(e) { paulo@89: lines.clearHighlights(); paulo@89: }); paulo@89: paulo@89: dispatch.on('changeState', function(e) { paulo@89: if (typeof e.disabled !== 'undefined' && data.length === e.disabled.length) { paulo@89: data.forEach(function(series,i) { paulo@89: series.disabled = e.disabled[i]; paulo@89: }); paulo@89: paulo@89: state.disabled = e.disabled; paulo@89: } paulo@89: paulo@89: chart.update(); paulo@89: }); paulo@89: paulo@89: }); paulo@89: paulo@89: renderWatch.renderEnd('lineChart immediate'); paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (out of chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: lines.dispatch.on('elementMouseover.tooltip', function(evt) { paulo@89: tooltip.data(evt).position(evt.pos).hidden(false); paulo@89: }); paulo@89: paulo@89: lines.dispatch.on('elementMouseout.tooltip', function(evt) { paulo@89: tooltip.hidden(true) paulo@89: }); paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: // expose chart's sub-components paulo@89: chart.dispatch = dispatch; paulo@89: chart.lines = lines; paulo@89: chart.legend = legend; paulo@89: chart.xAxis = xAxis; paulo@89: chart.yAxis = yAxis; paulo@89: chart.interactiveLayer = interactiveLayer; paulo@89: chart.tooltip = tooltip; paulo@89: paulo@89: chart.dispatch = dispatch; paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}}, paulo@89: showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}}, paulo@89: showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}}, paulo@89: defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}}, paulo@89: noData: {get: function(){return noData;}, set: function(_){noData=_;}}, paulo@89: paulo@89: // deprecated options paulo@89: tooltips: {get: function(){return tooltip.enabled();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead'); paulo@89: tooltip.enabled(!!_); paulo@89: }}, paulo@89: tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead'); paulo@89: tooltip.contentGenerator(_); paulo@89: }}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: duration: {get: function(){return duration;}, set: function(_){ paulo@89: duration = _; paulo@89: renderWatch.reset(duration); paulo@89: lines.duration(duration); paulo@89: xAxis.duration(duration); paulo@89: yAxis.duration(duration); paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: legend.color(color); paulo@89: lines.color(color); paulo@89: }}, paulo@89: rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){ paulo@89: rightAlignYAxis = _; paulo@89: yAxis.orient( rightAlignYAxis ? 'right' : 'left'); paulo@89: }}, paulo@89: useInteractiveGuideline: {get: function(){return useInteractiveGuideline;}, set: function(_){ paulo@89: useInteractiveGuideline = _; paulo@89: if (useInteractiveGuideline) { paulo@89: lines.interactive(false); paulo@89: lines.useVoronoi(false); paulo@89: } paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.inheritOptions(chart, lines); paulo@89: nv.utils.initOptions(chart); paulo@89: paulo@89: return chart; paulo@89: }; paulo@89: nv.models.linePlusBarChart = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var lines = nv.models.line() paulo@89: , lines2 = nv.models.line() paulo@89: , bars = nv.models.historicalBar() paulo@89: , bars2 = nv.models.historicalBar() paulo@89: , xAxis = nv.models.axis() paulo@89: , x2Axis = nv.models.axis() paulo@89: , y1Axis = nv.models.axis() paulo@89: , y2Axis = nv.models.axis() paulo@89: , y3Axis = nv.models.axis() paulo@89: , y4Axis = nv.models.axis() paulo@89: , legend = nv.models.legend() paulo@89: , brush = d3.svg.brush() paulo@89: , tooltip = nv.models.tooltip() paulo@89: ; paulo@89: paulo@89: var margin = {top: 30, right: 30, bottom: 30, left: 60} paulo@89: , margin2 = {top: 0, right: 30, bottom: 20, left: 60} paulo@89: , width = null paulo@89: , height = null paulo@89: , getX = function(d) { return d.x } paulo@89: , getY = function(d) { return d.y } paulo@89: , color = nv.utils.defaultColor() paulo@89: , showLegend = true paulo@89: , focusEnable = true paulo@89: , focusShowAxisY = false paulo@89: , focusShowAxisX = true paulo@89: , focusHeight = 50 paulo@89: , extent paulo@89: , brushExtent = null paulo@89: , x paulo@89: , x2 paulo@89: , y1 paulo@89: , y2 paulo@89: , y3 paulo@89: , y4 paulo@89: , noData = null paulo@89: , dispatch = d3.dispatch('brush', 'stateChange', 'changeState') paulo@89: , transitionDuration = 0 paulo@89: , state = nv.utils.state() paulo@89: , defaultState = null paulo@89: , legendLeftAxisHint = ' (left axis)' paulo@89: , legendRightAxisHint = ' (right axis)' paulo@89: ; paulo@89: paulo@89: lines.clipEdge(true); paulo@89: lines2.interactive(false); paulo@89: xAxis.orient('bottom').tickPadding(5); paulo@89: y1Axis.orient('left'); paulo@89: y2Axis.orient('right'); paulo@89: x2Axis.orient('bottom').tickPadding(5); paulo@89: y3Axis.orient('left'); paulo@89: y4Axis.orient('right'); paulo@89: paulo@89: tooltip.headerEnabled(true).headerFormatter(function(d, i) { paulo@89: return xAxis.tickFormat()(d, i); paulo@89: }); paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var stateGetter = function(data) { paulo@89: return function(){ paulo@89: return { paulo@89: active: data.map(function(d) { return !d.disabled }) paulo@89: }; paulo@89: } paulo@89: }; paulo@89: paulo@89: var stateSetter = function(data) { paulo@89: return function(state) { paulo@89: if (state.active !== undefined) paulo@89: data.forEach(function(series,i) { paulo@89: series.disabled = !state.active[i]; paulo@89: }); paulo@89: } paulo@89: }; paulo@89: paulo@89: function chart(selection) { paulo@89: selection.each(function(data) { paulo@89: var container = d3.select(this), paulo@89: that = this; paulo@89: nv.utils.initSVG(container); paulo@89: var availableWidth = nv.utils.availableWidth(width, container, margin), paulo@89: availableHeight1 = nv.utils.availableHeight(height, container, margin) paulo@89: - (focusEnable ? focusHeight : 0), paulo@89: availableHeight2 = focusHeight - margin2.top - margin2.bottom; paulo@89: paulo@89: chart.update = function() { container.transition().duration(transitionDuration).call(chart); }; paulo@89: chart.container = this; paulo@89: paulo@89: state paulo@89: .setter(stateSetter(data), chart.update) paulo@89: .getter(stateGetter(data)) paulo@89: .update(); paulo@89: paulo@89: // DEPRECATED set state.disableddisabled paulo@89: state.disabled = data.map(function(d) { return !!d.disabled }); paulo@89: paulo@89: if (!defaultState) { paulo@89: var key; paulo@89: defaultState = {}; paulo@89: for (key in state) { paulo@89: if (state[key] instanceof Array) paulo@89: defaultState[key] = state[key].slice(0); paulo@89: else paulo@89: defaultState[key] = state[key]; paulo@89: } paulo@89: } paulo@89: paulo@89: // Display No Data message if there's nothing to show. paulo@89: if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) { paulo@89: nv.utils.noData(chart, container) paulo@89: return chart; paulo@89: } else { paulo@89: container.selectAll('.nv-noData').remove(); paulo@89: } paulo@89: paulo@89: // Setup Scales paulo@89: var dataBars = data.filter(function(d) { return !d.disabled && d.bar }); paulo@89: var dataLines = data.filter(function(d) { return !d.bar }); // removed the !d.disabled clause here to fix Issue #240 paulo@89: paulo@89: x = bars.xScale(); paulo@89: x2 = x2Axis.scale(); paulo@89: y1 = bars.yScale(); paulo@89: y2 = lines.yScale(); paulo@89: y3 = bars2.yScale(); paulo@89: y4 = lines2.yScale(); paulo@89: paulo@89: var series1 = data paulo@89: .filter(function(d) { return !d.disabled && d.bar }) paulo@89: .map(function(d) { paulo@89: return d.values.map(function(d,i) { paulo@89: return { x: getX(d,i), y: getY(d,i) } paulo@89: }) paulo@89: }); paulo@89: paulo@89: var series2 = data paulo@89: .filter(function(d) { return !d.disabled && !d.bar }) paulo@89: .map(function(d) { paulo@89: return d.values.map(function(d,i) { paulo@89: return { x: getX(d,i), y: getY(d,i) } paulo@89: }) paulo@89: }); paulo@89: paulo@89: x.range([0, availableWidth]); paulo@89: paulo@89: x2 .domain(d3.extent(d3.merge(series1.concat(series2)), function(d) { return d.x } )) paulo@89: .range([0, availableWidth]); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-linePlusBar').data([data]); paulo@89: var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-linePlusBar').append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-legendWrap'); paulo@89: paulo@89: // this is the main chart paulo@89: var focusEnter = gEnter.append('g').attr('class', 'nv-focus'); paulo@89: focusEnter.append('g').attr('class', 'nv-x nv-axis'); paulo@89: focusEnter.append('g').attr('class', 'nv-y1 nv-axis'); paulo@89: focusEnter.append('g').attr('class', 'nv-y2 nv-axis'); paulo@89: focusEnter.append('g').attr('class', 'nv-barsWrap'); paulo@89: focusEnter.append('g').attr('class', 'nv-linesWrap'); paulo@89: paulo@89: // context chart is where you can focus in paulo@89: var contextEnter = gEnter.append('g').attr('class', 'nv-context'); paulo@89: contextEnter.append('g').attr('class', 'nv-x nv-axis'); paulo@89: contextEnter.append('g').attr('class', 'nv-y1 nv-axis'); paulo@89: contextEnter.append('g').attr('class', 'nv-y2 nv-axis'); paulo@89: contextEnter.append('g').attr('class', 'nv-barsWrap'); paulo@89: contextEnter.append('g').attr('class', 'nv-linesWrap'); paulo@89: contextEnter.append('g').attr('class', 'nv-brushBackground'); paulo@89: contextEnter.append('g').attr('class', 'nv-x nv-brush'); paulo@89: paulo@89: //============================================================ paulo@89: // Legend paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: if (showLegend) { paulo@89: var legendWidth = legend.align() ? availableWidth / 2 : availableWidth; paulo@89: var legendXPosition = legend.align() ? legendWidth : 0; paulo@89: paulo@89: legend.width(legendWidth); paulo@89: paulo@89: g.select('.nv-legendWrap') paulo@89: .datum(data.map(function(series) { paulo@89: series.originalKey = series.originalKey === undefined ? series.key : series.originalKey; paulo@89: series.key = series.originalKey + (series.bar ? legendLeftAxisHint : legendRightAxisHint); paulo@89: return series; paulo@89: })) paulo@89: .call(legend); paulo@89: paulo@89: if ( margin.top != legend.height()) { paulo@89: margin.top = legend.height(); paulo@89: // FIXME: shouldn't this be "- (focusEnabled ? focusHeight : 0)"? paulo@89: availableHeight1 = nv.utils.availableHeight(height, container, margin) - focusHeight; paulo@89: } paulo@89: paulo@89: g.select('.nv-legendWrap') paulo@89: .attr('transform', 'translate(' + legendXPosition + ',' + (-margin.top) +')'); paulo@89: } paulo@89: paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: //============================================================ paulo@89: // Context chart (focus chart) components paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: // hide or show the focus context chart paulo@89: g.select('.nv-context').style('display', focusEnable ? 'initial' : 'none'); paulo@89: paulo@89: bars2 paulo@89: .width(availableWidth) paulo@89: .height(availableHeight2) paulo@89: .color(data.map(function (d, i) { paulo@89: return d.color || color(d, i); paulo@89: }).filter(function (d, i) { paulo@89: return !data[i].disabled && data[i].bar paulo@89: })); paulo@89: lines2 paulo@89: .width(availableWidth) paulo@89: .height(availableHeight2) paulo@89: .color(data.map(function (d, i) { paulo@89: return d.color || color(d, i); paulo@89: }).filter(function (d, i) { paulo@89: return !data[i].disabled && !data[i].bar paulo@89: })); paulo@89: paulo@89: var bars2Wrap = g.select('.nv-context .nv-barsWrap') paulo@89: .datum(dataBars.length ? dataBars : [ paulo@89: {values: []} paulo@89: ]); paulo@89: var lines2Wrap = g.select('.nv-context .nv-linesWrap') paulo@89: .datum(!dataLines[0].disabled ? dataLines : [ paulo@89: {values: []} paulo@89: ]); paulo@89: paulo@89: g.select('.nv-context') paulo@89: .attr('transform', 'translate(0,' + ( availableHeight1 + margin.bottom + margin2.top) + ')'); paulo@89: paulo@89: bars2Wrap.transition().call(bars2); paulo@89: lines2Wrap.transition().call(lines2); paulo@89: paulo@89: // context (focus chart) axis controls paulo@89: if (focusShowAxisX) { paulo@89: x2Axis paulo@89: ._ticks( nv.utils.calcTicksX(availableWidth / 100, data)) paulo@89: .tickSize(-availableHeight2, 0); paulo@89: g.select('.nv-context .nv-x.nv-axis') paulo@89: .attr('transform', 'translate(0,' + y3.range()[0] + ')'); paulo@89: g.select('.nv-context .nv-x.nv-axis').transition() paulo@89: .call(x2Axis); paulo@89: } paulo@89: paulo@89: if (focusShowAxisY) { paulo@89: y3Axis paulo@89: .scale(y3) paulo@89: ._ticks( availableHeight2 / 36 ) paulo@89: .tickSize( -availableWidth, 0); paulo@89: y4Axis paulo@89: .scale(y4) paulo@89: ._ticks( availableHeight2 / 36 ) paulo@89: .tickSize(dataBars.length ? 0 : -availableWidth, 0); // Show the y2 rules only if y1 has none paulo@89: paulo@89: g.select('.nv-context .nv-y3.nv-axis') paulo@89: .style('opacity', dataBars.length ? 1 : 0) paulo@89: .attr('transform', 'translate(0,' + x2.range()[0] + ')'); paulo@89: g.select('.nv-context .nv-y2.nv-axis') paulo@89: .style('opacity', dataLines.length ? 1 : 0) paulo@89: .attr('transform', 'translate(' + x2.range()[1] + ',0)'); paulo@89: paulo@89: g.select('.nv-context .nv-y1.nv-axis').transition() paulo@89: .call(y3Axis); paulo@89: g.select('.nv-context .nv-y2.nv-axis').transition() paulo@89: .call(y4Axis); paulo@89: } paulo@89: paulo@89: // Setup Brush paulo@89: brush.x(x2).on('brush', onBrush); paulo@89: paulo@89: if (brushExtent) brush.extent(brushExtent); paulo@89: paulo@89: var brushBG = g.select('.nv-brushBackground').selectAll('g') paulo@89: .data([brushExtent || brush.extent()]); paulo@89: paulo@89: var brushBGenter = brushBG.enter() paulo@89: .append('g'); paulo@89: paulo@89: brushBGenter.append('rect') paulo@89: .attr('class', 'left') paulo@89: .attr('x', 0) paulo@89: .attr('y', 0) paulo@89: .attr('height', availableHeight2); paulo@89: paulo@89: brushBGenter.append('rect') paulo@89: .attr('class', 'right') paulo@89: .attr('x', 0) paulo@89: .attr('y', 0) paulo@89: .attr('height', availableHeight2); paulo@89: paulo@89: var gBrush = g.select('.nv-x.nv-brush') paulo@89: .call(brush); paulo@89: gBrush.selectAll('rect') paulo@89: //.attr('y', -5) paulo@89: .attr('height', availableHeight2); paulo@89: gBrush.selectAll('.resize').append('path').attr('d', resizePath); paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (in chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: legend.dispatch.on('stateChange', function(newState) { paulo@89: for (var key in newState) paulo@89: state[key] = newState[key]; paulo@89: dispatch.stateChange(state); paulo@89: chart.update(); paulo@89: }); paulo@89: paulo@89: // Update chart from a state object passed to event handler paulo@89: dispatch.on('changeState', function(e) { paulo@89: if (typeof e.disabled !== 'undefined') { paulo@89: data.forEach(function(series,i) { paulo@89: series.disabled = e.disabled[i]; paulo@89: }); paulo@89: state.disabled = e.disabled; paulo@89: } paulo@89: chart.update(); paulo@89: }); paulo@89: paulo@89: //============================================================ paulo@89: // Functions paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: // Taken from crossfilter (http://square.github.com/crossfilter/) paulo@89: function resizePath(d) { paulo@89: var e = +(d == 'e'), paulo@89: x = e ? 1 : -1, paulo@89: y = availableHeight2 / 3; paulo@89: return 'M' + (.5 * x) + ',' + y paulo@89: + 'A6,6 0 0 ' + e + ' ' + (6.5 * x) + ',' + (y + 6) paulo@89: + 'V' + (2 * y - 6) paulo@89: + 'A6,6 0 0 ' + e + ' ' + (.5 * x) + ',' + (2 * y) paulo@89: + 'Z' paulo@89: + 'M' + (2.5 * x) + ',' + (y + 8) paulo@89: + 'V' + (2 * y - 8) paulo@89: + 'M' + (4.5 * x) + ',' + (y + 8) paulo@89: + 'V' + (2 * y - 8); paulo@89: } paulo@89: paulo@89: paulo@89: function updateBrushBG() { paulo@89: if (!brush.empty()) brush.extent(brushExtent); paulo@89: brushBG paulo@89: .data([brush.empty() ? x2.domain() : brushExtent]) paulo@89: .each(function(d,i) { paulo@89: var leftWidth = x2(d[0]) - x2.range()[0], paulo@89: rightWidth = x2.range()[1] - x2(d[1]); paulo@89: d3.select(this).select('.left') paulo@89: .attr('width', leftWidth < 0 ? 0 : leftWidth); paulo@89: paulo@89: d3.select(this).select('.right') paulo@89: .attr('x', x2(d[1])) paulo@89: .attr('width', rightWidth < 0 ? 0 : rightWidth); paulo@89: }); paulo@89: } paulo@89: paulo@89: function onBrush() { paulo@89: brushExtent = brush.empty() ? null : brush.extent(); paulo@89: extent = brush.empty() ? x2.domain() : brush.extent(); paulo@89: dispatch.brush({extent: extent, brush: brush}); paulo@89: updateBrushBG(); paulo@89: paulo@89: // Prepare Main (Focus) Bars and Lines paulo@89: bars paulo@89: .width(availableWidth) paulo@89: .height(availableHeight1) paulo@89: .color(data.map(function(d,i) { paulo@89: return d.color || color(d, i); paulo@89: }).filter(function(d,i) { return !data[i].disabled && data[i].bar })); paulo@89: paulo@89: lines paulo@89: .width(availableWidth) paulo@89: .height(availableHeight1) paulo@89: .color(data.map(function(d,i) { paulo@89: return d.color || color(d, i); paulo@89: }).filter(function(d,i) { return !data[i].disabled && !data[i].bar })); paulo@89: paulo@89: var focusBarsWrap = g.select('.nv-focus .nv-barsWrap') paulo@89: .datum(!dataBars.length ? [{values:[]}] : paulo@89: dataBars paulo@89: .map(function(d,i) { paulo@89: return { paulo@89: key: d.key, paulo@89: values: d.values.filter(function(d,i) { paulo@89: return bars.x()(d,i) >= extent[0] && bars.x()(d,i) <= extent[1]; paulo@89: }) paulo@89: } paulo@89: }) paulo@89: ); paulo@89: paulo@89: var focusLinesWrap = g.select('.nv-focus .nv-linesWrap') paulo@89: .datum(dataLines[0].disabled ? [{values:[]}] : paulo@89: dataLines paulo@89: .map(function(d,i) { paulo@89: return { paulo@89: area: d.area, paulo@89: fillOpacity: d.fillOpacity, paulo@89: key: d.key, paulo@89: values: d.values.filter(function(d,i) { paulo@89: return lines.x()(d,i) >= extent[0] && lines.x()(d,i) <= extent[1]; paulo@89: }) paulo@89: } paulo@89: }) paulo@89: ); paulo@89: paulo@89: // Update Main (Focus) X Axis paulo@89: if (dataBars.length) { paulo@89: x = bars.xScale(); paulo@89: } else { paulo@89: x = lines.xScale(); paulo@89: } paulo@89: paulo@89: xAxis paulo@89: .scale(x) paulo@89: ._ticks( nv.utils.calcTicksX(availableWidth/100, data) ) paulo@89: .tickSize(-availableHeight1, 0); paulo@89: paulo@89: xAxis.domain([Math.ceil(extent[0]), Math.floor(extent[1])]); paulo@89: paulo@89: g.select('.nv-x.nv-axis').transition().duration(transitionDuration) paulo@89: .call(xAxis); paulo@89: paulo@89: // Update Main (Focus) Bars and Lines paulo@89: focusBarsWrap.transition().duration(transitionDuration).call(bars); paulo@89: focusLinesWrap.transition().duration(transitionDuration).call(lines); paulo@89: paulo@89: // Setup and Update Main (Focus) Y Axes paulo@89: g.select('.nv-focus .nv-x.nv-axis') paulo@89: .attr('transform', 'translate(0,' + y1.range()[0] + ')'); paulo@89: paulo@89: y1Axis paulo@89: .scale(y1) paulo@89: ._ticks( nv.utils.calcTicksY(availableHeight1/36, data) ) paulo@89: .tickSize(-availableWidth, 0); paulo@89: y2Axis paulo@89: .scale(y2) paulo@89: ._ticks( nv.utils.calcTicksY(availableHeight1/36, data) ) paulo@89: .tickSize(dataBars.length ? 0 : -availableWidth, 0); // Show the y2 rules only if y1 has none paulo@89: paulo@89: g.select('.nv-focus .nv-y1.nv-axis') paulo@89: .style('opacity', dataBars.length ? 1 : 0); paulo@89: g.select('.nv-focus .nv-y2.nv-axis') paulo@89: .style('opacity', dataLines.length && !dataLines[0].disabled ? 1 : 0) paulo@89: .attr('transform', 'translate(' + x.range()[1] + ',0)'); paulo@89: paulo@89: g.select('.nv-focus .nv-y1.nv-axis').transition().duration(transitionDuration) paulo@89: .call(y1Axis); paulo@89: g.select('.nv-focus .nv-y2.nv-axis').transition().duration(transitionDuration) paulo@89: .call(y2Axis); paulo@89: } paulo@89: paulo@89: onBrush(); paulo@89: paulo@89: }); paulo@89: paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (out of chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: lines.dispatch.on('elementMouseover.tooltip', function(evt) { paulo@89: tooltip paulo@89: .duration(100) paulo@89: .valueFormatter(function(d, i) { paulo@89: return y2Axis.tickFormat()(d, i); paulo@89: }) paulo@89: .data(evt) paulo@89: .position(evt.pos) paulo@89: .hidden(false); paulo@89: }); paulo@89: paulo@89: lines.dispatch.on('elementMouseout.tooltip', function(evt) { paulo@89: tooltip.hidden(true) paulo@89: }); paulo@89: paulo@89: bars.dispatch.on('elementMouseover.tooltip', function(evt) { paulo@89: evt.value = chart.x()(evt.data); paulo@89: evt['series'] = { paulo@89: value: chart.y()(evt.data), paulo@89: color: evt.color paulo@89: }; paulo@89: tooltip paulo@89: .duration(0) paulo@89: .valueFormatter(function(d, i) { paulo@89: return y1Axis.tickFormat()(d, i); paulo@89: }) paulo@89: .data(evt) paulo@89: .hidden(false); paulo@89: }); paulo@89: paulo@89: bars.dispatch.on('elementMouseout.tooltip', function(evt) { paulo@89: tooltip.hidden(true); paulo@89: }); paulo@89: paulo@89: bars.dispatch.on('elementMousemove.tooltip', function(evt) { paulo@89: tooltip.position({top: d3.event.pageY, left: d3.event.pageX})(); paulo@89: }); paulo@89: paulo@89: //============================================================ paulo@89: paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: // expose chart's sub-components paulo@89: chart.dispatch = dispatch; paulo@89: chart.legend = legend; paulo@89: chart.lines = lines; paulo@89: chart.lines2 = lines2; paulo@89: chart.bars = bars; paulo@89: chart.bars2 = bars2; paulo@89: chart.xAxis = xAxis; paulo@89: chart.x2Axis = x2Axis; paulo@89: chart.y1Axis = y1Axis; paulo@89: chart.y2Axis = y2Axis; paulo@89: chart.y3Axis = y3Axis; paulo@89: chart.y4Axis = y4Axis; paulo@89: chart.tooltip = tooltip; paulo@89: paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}}, paulo@89: brushExtent: {get: function(){return brushExtent;}, set: function(_){brushExtent=_;}}, paulo@89: noData: {get: function(){return noData;}, set: function(_){noData=_;}}, paulo@89: focusEnable: {get: function(){return focusEnable;}, set: function(_){focusEnable=_;}}, paulo@89: focusHeight: {get: function(){return focusHeight;}, set: function(_){focusHeight=_;}}, paulo@89: focusShowAxisX: {get: function(){return focusShowAxisX;}, set: function(_){focusShowAxisX=_;}}, paulo@89: focusShowAxisY: {get: function(){return focusShowAxisY;}, set: function(_){focusShowAxisY=_;}}, paulo@89: legendLeftAxisHint: {get: function(){return legendLeftAxisHint;}, set: function(_){legendLeftAxisHint=_;}}, paulo@89: legendRightAxisHint: {get: function(){return legendRightAxisHint;}, set: function(_){legendRightAxisHint=_;}}, paulo@89: paulo@89: // deprecated options paulo@89: tooltips: {get: function(){return tooltip.enabled();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead'); paulo@89: tooltip.enabled(!!_); paulo@89: }}, paulo@89: tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead'); paulo@89: tooltip.contentGenerator(_); paulo@89: }}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: duration: {get: function(){return transitionDuration;}, set: function(_){ paulo@89: transitionDuration = _; paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: legend.color(color); paulo@89: }}, paulo@89: x: {get: function(){return getX;}, set: function(_){ paulo@89: getX = _; paulo@89: lines.x(_); paulo@89: lines2.x(_); paulo@89: bars.x(_); paulo@89: bars2.x(_); paulo@89: }}, paulo@89: y: {get: function(){return getY;}, set: function(_){ paulo@89: getY = _; paulo@89: lines.y(_); paulo@89: lines2.y(_); paulo@89: bars.y(_); paulo@89: bars2.y(_); paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.inheritOptions(chart, lines); paulo@89: nv.utils.initOptions(chart); paulo@89: paulo@89: return chart; paulo@89: }; paulo@89: nv.models.lineWithFocusChart = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var lines = nv.models.line() paulo@89: , lines2 = nv.models.line() paulo@89: , xAxis = nv.models.axis() paulo@89: , yAxis = nv.models.axis() paulo@89: , x2Axis = nv.models.axis() paulo@89: , y2Axis = nv.models.axis() paulo@89: , legend = nv.models.legend() paulo@89: , brush = d3.svg.brush() paulo@89: , tooltip = nv.models.tooltip() paulo@89: , interactiveLayer = nv.interactiveGuideline() paulo@89: ; paulo@89: paulo@89: var margin = {top: 30, right: 30, bottom: 30, left: 60} paulo@89: , margin2 = {top: 0, right: 30, bottom: 20, left: 60} paulo@89: , color = nv.utils.defaultColor() paulo@89: , width = null paulo@89: , height = null paulo@89: , height2 = 50 paulo@89: , useInteractiveGuideline = false paulo@89: , x paulo@89: , y paulo@89: , x2 paulo@89: , y2 paulo@89: , showLegend = true paulo@89: , brushExtent = null paulo@89: , noData = null paulo@89: , dispatch = d3.dispatch('brush', 'stateChange', 'changeState') paulo@89: , transitionDuration = 250 paulo@89: , state = nv.utils.state() paulo@89: , defaultState = null paulo@89: ; paulo@89: paulo@89: lines.clipEdge(true).duration(0); paulo@89: lines2.interactive(false); paulo@89: xAxis.orient('bottom').tickPadding(5); paulo@89: yAxis.orient('left'); paulo@89: x2Axis.orient('bottom').tickPadding(5); paulo@89: y2Axis.orient('left'); paulo@89: paulo@89: tooltip.valueFormatter(function(d, i) { paulo@89: return yAxis.tickFormat()(d, i); paulo@89: }).headerFormatter(function(d, i) { paulo@89: return xAxis.tickFormat()(d, i); paulo@89: }); paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var stateGetter = function(data) { paulo@89: return function(){ paulo@89: return { paulo@89: active: data.map(function(d) { return !d.disabled }) paulo@89: }; paulo@89: } paulo@89: }; paulo@89: paulo@89: var stateSetter = function(data) { paulo@89: return function(state) { paulo@89: if (state.active !== undefined) paulo@89: data.forEach(function(series,i) { paulo@89: series.disabled = !state.active[i]; paulo@89: }); paulo@89: } paulo@89: }; paulo@89: paulo@89: function chart(selection) { paulo@89: selection.each(function(data) { paulo@89: var container = d3.select(this), paulo@89: that = this; paulo@89: nv.utils.initSVG(container); paulo@89: var availableWidth = nv.utils.availableWidth(width, container, margin), paulo@89: availableHeight1 = nv.utils.availableHeight(height, container, margin) - height2, paulo@89: availableHeight2 = height2 - margin2.top - margin2.bottom; paulo@89: paulo@89: chart.update = function() { container.transition().duration(transitionDuration).call(chart) }; paulo@89: chart.container = this; paulo@89: paulo@89: state paulo@89: .setter(stateSetter(data), chart.update) paulo@89: .getter(stateGetter(data)) paulo@89: .update(); paulo@89: paulo@89: // DEPRECATED set state.disableddisabled paulo@89: state.disabled = data.map(function(d) { return !!d.disabled }); paulo@89: paulo@89: if (!defaultState) { paulo@89: var key; paulo@89: defaultState = {}; paulo@89: for (key in state) { paulo@89: if (state[key] instanceof Array) paulo@89: defaultState[key] = state[key].slice(0); paulo@89: else paulo@89: defaultState[key] = state[key]; paulo@89: } paulo@89: } paulo@89: paulo@89: // Display No Data message if there's nothing to show. paulo@89: if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) { paulo@89: nv.utils.noData(chart, container) paulo@89: return chart; paulo@89: } else { paulo@89: container.selectAll('.nv-noData').remove(); paulo@89: } paulo@89: paulo@89: // Setup Scales paulo@89: x = lines.xScale(); paulo@89: y = lines.yScale(); paulo@89: x2 = lines2.xScale(); paulo@89: y2 = lines2.yScale(); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-lineWithFocusChart').data([data]); paulo@89: var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-lineWithFocusChart').append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-legendWrap'); paulo@89: paulo@89: var focusEnter = gEnter.append('g').attr('class', 'nv-focus'); paulo@89: focusEnter.append('g').attr('class', 'nv-x nv-axis'); paulo@89: focusEnter.append('g').attr('class', 'nv-y nv-axis'); paulo@89: focusEnter.append('g').attr('class', 'nv-linesWrap'); paulo@89: focusEnter.append('g').attr('class', 'nv-interactive'); paulo@89: paulo@89: var contextEnter = gEnter.append('g').attr('class', 'nv-context'); paulo@89: contextEnter.append('g').attr('class', 'nv-x nv-axis'); paulo@89: contextEnter.append('g').attr('class', 'nv-y nv-axis'); paulo@89: contextEnter.append('g').attr('class', 'nv-linesWrap'); paulo@89: contextEnter.append('g').attr('class', 'nv-brushBackground'); paulo@89: contextEnter.append('g').attr('class', 'nv-x nv-brush'); paulo@89: paulo@89: // Legend paulo@89: if (showLegend) { paulo@89: legend.width(availableWidth); paulo@89: paulo@89: g.select('.nv-legendWrap') paulo@89: .datum(data) paulo@89: .call(legend); paulo@89: paulo@89: if ( margin.top != legend.height()) { paulo@89: margin.top = legend.height(); paulo@89: availableHeight1 = nv.utils.availableHeight(height, container, margin) - height2; paulo@89: } paulo@89: paulo@89: g.select('.nv-legendWrap') paulo@89: .attr('transform', 'translate(0,' + (-margin.top) +')') paulo@89: } paulo@89: paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: paulo@89: //Set up interactive layer paulo@89: if (useInteractiveGuideline) { paulo@89: interactiveLayer paulo@89: .width(availableWidth) paulo@89: .height(availableHeight1) paulo@89: .margin({left:margin.left, top:margin.top}) paulo@89: .svgContainer(container) paulo@89: .xScale(x); paulo@89: wrap.select(".nv-interactive").call(interactiveLayer); paulo@89: } paulo@89: paulo@89: // Main Chart Component(s) paulo@89: lines paulo@89: .width(availableWidth) paulo@89: .height(availableHeight1) paulo@89: .color( paulo@89: data paulo@89: .map(function(d,i) { paulo@89: return d.color || color(d, i); paulo@89: }) paulo@89: .filter(function(d,i) { paulo@89: return !data[i].disabled; paulo@89: }) paulo@89: ); paulo@89: paulo@89: lines2 paulo@89: .defined(lines.defined()) paulo@89: .width(availableWidth) paulo@89: .height(availableHeight2) paulo@89: .color( paulo@89: data paulo@89: .map(function(d,i) { paulo@89: return d.color || color(d, i); paulo@89: }) paulo@89: .filter(function(d,i) { paulo@89: return !data[i].disabled; paulo@89: }) paulo@89: ); paulo@89: paulo@89: g.select('.nv-context') paulo@89: .attr('transform', 'translate(0,' + ( availableHeight1 + margin.bottom + margin2.top) + ')') paulo@89: paulo@89: var contextLinesWrap = g.select('.nv-context .nv-linesWrap') paulo@89: .datum(data.filter(function(d) { return !d.disabled })) paulo@89: paulo@89: d3.transition(contextLinesWrap).call(lines2); paulo@89: paulo@89: // Setup Main (Focus) Axes paulo@89: xAxis paulo@89: .scale(x) paulo@89: ._ticks( nv.utils.calcTicksX(availableWidth/100, data) ) paulo@89: .tickSize(-availableHeight1, 0); paulo@89: paulo@89: yAxis paulo@89: .scale(y) paulo@89: ._ticks( nv.utils.calcTicksY(availableHeight1/36, data) ) paulo@89: .tickSize( -availableWidth, 0); paulo@89: paulo@89: g.select('.nv-focus .nv-x.nv-axis') paulo@89: .attr('transform', 'translate(0,' + availableHeight1 + ')'); paulo@89: paulo@89: // Setup Brush paulo@89: brush paulo@89: .x(x2) paulo@89: .on('brush', function() { paulo@89: onBrush(); paulo@89: }); paulo@89: paulo@89: if (brushExtent) brush.extent(brushExtent); paulo@89: paulo@89: var brushBG = g.select('.nv-brushBackground').selectAll('g') paulo@89: .data([brushExtent || brush.extent()]) paulo@89: paulo@89: var brushBGenter = brushBG.enter() paulo@89: .append('g'); paulo@89: paulo@89: brushBGenter.append('rect') paulo@89: .attr('class', 'left') paulo@89: .attr('x', 0) paulo@89: .attr('y', 0) paulo@89: .attr('height', availableHeight2); paulo@89: paulo@89: brushBGenter.append('rect') paulo@89: .attr('class', 'right') paulo@89: .attr('x', 0) paulo@89: .attr('y', 0) paulo@89: .attr('height', availableHeight2); paulo@89: paulo@89: var gBrush = g.select('.nv-x.nv-brush') paulo@89: .call(brush); paulo@89: gBrush.selectAll('rect') paulo@89: .attr('height', availableHeight2); paulo@89: gBrush.selectAll('.resize').append('path').attr('d', resizePath); paulo@89: paulo@89: onBrush(); paulo@89: paulo@89: // Setup Secondary (Context) Axes paulo@89: x2Axis paulo@89: .scale(x2) paulo@89: ._ticks( nv.utils.calcTicksX(availableWidth/100, data) ) paulo@89: .tickSize(-availableHeight2, 0); paulo@89: paulo@89: g.select('.nv-context .nv-x.nv-axis') paulo@89: .attr('transform', 'translate(0,' + y2.range()[0] + ')'); paulo@89: d3.transition(g.select('.nv-context .nv-x.nv-axis')) paulo@89: .call(x2Axis); paulo@89: paulo@89: y2Axis paulo@89: .scale(y2) paulo@89: ._ticks( nv.utils.calcTicksY(availableHeight2/36, data) ) paulo@89: .tickSize( -availableWidth, 0); paulo@89: paulo@89: d3.transition(g.select('.nv-context .nv-y.nv-axis')) paulo@89: .call(y2Axis); paulo@89: paulo@89: g.select('.nv-context .nv-x.nv-axis') paulo@89: .attr('transform', 'translate(0,' + y2.range()[0] + ')'); paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (in chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: legend.dispatch.on('stateChange', function(newState) { paulo@89: for (var key in newState) paulo@89: state[key] = newState[key]; paulo@89: dispatch.stateChange(state); paulo@89: chart.update(); paulo@89: }); paulo@89: paulo@89: interactiveLayer.dispatch.on('elementMousemove', function(e) { paulo@89: lines.clearHighlights(); paulo@89: var singlePoint, pointIndex, pointXLocation, allData = []; paulo@89: data paulo@89: .filter(function(series, i) { paulo@89: series.seriesIndex = i; paulo@89: return !series.disabled; paulo@89: }) paulo@89: .forEach(function(series,i) { paulo@89: var extent = brush.empty() ? x2.domain() : brush.extent(); paulo@89: var currentValues = series.values.filter(function(d,i) { paulo@89: return lines.x()(d,i) >= extent[0] && lines.x()(d,i) <= extent[1]; paulo@89: }); paulo@89: paulo@89: pointIndex = nv.interactiveBisect(currentValues, e.pointXValue, lines.x()); paulo@89: var point = currentValues[pointIndex]; paulo@89: var pointYValue = chart.y()(point, pointIndex); paulo@89: if (pointYValue != null) { paulo@89: lines.highlightPoint(i, pointIndex, true); paulo@89: } paulo@89: if (point === undefined) return; paulo@89: if (singlePoint === undefined) singlePoint = point; paulo@89: if (pointXLocation === undefined) pointXLocation = chart.xScale()(chart.x()(point,pointIndex)); paulo@89: allData.push({ paulo@89: key: series.key, paulo@89: value: chart.y()(point, pointIndex), paulo@89: color: color(series,series.seriesIndex) paulo@89: }); paulo@89: }); paulo@89: //Highlight the tooltip entry based on which point the mouse is closest to. paulo@89: if (allData.length > 2) { paulo@89: var yValue = chart.yScale().invert(e.mouseY); paulo@89: var domainExtent = Math.abs(chart.yScale().domain()[0] - chart.yScale().domain()[1]); paulo@89: var threshold = 0.03 * domainExtent; paulo@89: var indexToHighlight = nv.nearestValueIndex(allData.map(function(d){return d.value}),yValue,threshold); paulo@89: if (indexToHighlight !== null) paulo@89: allData[indexToHighlight].highlight = true; paulo@89: } paulo@89: paulo@89: var xValue = xAxis.tickFormat()(chart.x()(singlePoint,pointIndex)); paulo@89: interactiveLayer.tooltip paulo@89: .position({left: e.mouseX + margin.left, top: e.mouseY + margin.top}) paulo@89: .chartContainer(that.parentNode) paulo@89: .valueFormatter(function(d,i) { paulo@89: return d == null ? "N/A" : yAxis.tickFormat()(d); paulo@89: }) paulo@89: .data({ paulo@89: value: xValue, paulo@89: index: pointIndex, paulo@89: series: allData paulo@89: })(); paulo@89: paulo@89: interactiveLayer.renderGuideLine(pointXLocation); paulo@89: paulo@89: }); paulo@89: paulo@89: interactiveLayer.dispatch.on("elementMouseout",function(e) { paulo@89: lines.clearHighlights(); paulo@89: }); paulo@89: paulo@89: dispatch.on('changeState', function(e) { paulo@89: if (typeof e.disabled !== 'undefined') { paulo@89: data.forEach(function(series,i) { paulo@89: series.disabled = e.disabled[i]; paulo@89: }); paulo@89: } paulo@89: chart.update(); paulo@89: }); paulo@89: paulo@89: //============================================================ paulo@89: // Functions paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: // Taken from crossfilter (http://square.github.com/crossfilter/) paulo@89: function resizePath(d) { paulo@89: var e = +(d == 'e'), paulo@89: x = e ? 1 : -1, paulo@89: y = availableHeight2 / 3; paulo@89: return 'M' + (.5 * x) + ',' + y paulo@89: + 'A6,6 0 0 ' + e + ' ' + (6.5 * x) + ',' + (y + 6) paulo@89: + 'V' + (2 * y - 6) paulo@89: + 'A6,6 0 0 ' + e + ' ' + (.5 * x) + ',' + (2 * y) paulo@89: + 'Z' paulo@89: + 'M' + (2.5 * x) + ',' + (y + 8) paulo@89: + 'V' + (2 * y - 8) paulo@89: + 'M' + (4.5 * x) + ',' + (y + 8) paulo@89: + 'V' + (2 * y - 8); paulo@89: } paulo@89: paulo@89: paulo@89: function updateBrushBG() { paulo@89: if (!brush.empty()) brush.extent(brushExtent); paulo@89: brushBG paulo@89: .data([brush.empty() ? x2.domain() : brushExtent]) paulo@89: .each(function(d,i) { paulo@89: var leftWidth = x2(d[0]) - x.range()[0], paulo@89: rightWidth = availableWidth - x2(d[1]); paulo@89: d3.select(this).select('.left') paulo@89: .attr('width', leftWidth < 0 ? 0 : leftWidth); paulo@89: paulo@89: d3.select(this).select('.right') paulo@89: .attr('x', x2(d[1])) paulo@89: .attr('width', rightWidth < 0 ? 0 : rightWidth); paulo@89: }); paulo@89: } paulo@89: paulo@89: paulo@89: function onBrush() { paulo@89: brushExtent = brush.empty() ? null : brush.extent(); paulo@89: var extent = brush.empty() ? x2.domain() : brush.extent(); paulo@89: paulo@89: //The brush extent cannot be less than one. If it is, don't update the line chart. paulo@89: if (Math.abs(extent[0] - extent[1]) <= 1) { paulo@89: return; paulo@89: } paulo@89: paulo@89: dispatch.brush({extent: extent, brush: brush}); paulo@89: paulo@89: paulo@89: updateBrushBG(); paulo@89: paulo@89: // Update Main (Focus) paulo@89: var focusLinesWrap = g.select('.nv-focus .nv-linesWrap') paulo@89: .datum( paulo@89: data paulo@89: .filter(function(d) { return !d.disabled }) paulo@89: .map(function(d,i) { paulo@89: return { paulo@89: key: d.key, paulo@89: area: d.area, paulo@89: values: d.values.filter(function(d,i) { paulo@89: return lines.x()(d,i) >= extent[0] && lines.x()(d,i) <= extent[1]; paulo@89: }) paulo@89: } paulo@89: }) paulo@89: ); paulo@89: focusLinesWrap.transition().duration(transitionDuration).call(lines); paulo@89: paulo@89: paulo@89: // Update Main (Focus) Axes paulo@89: g.select('.nv-focus .nv-x.nv-axis').transition().duration(transitionDuration) paulo@89: .call(xAxis); paulo@89: g.select('.nv-focus .nv-y.nv-axis').transition().duration(transitionDuration) paulo@89: .call(yAxis); paulo@89: } paulo@89: }); paulo@89: paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (out of chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: lines.dispatch.on('elementMouseover.tooltip', function(evt) { paulo@89: tooltip.data(evt).position(evt.pos).hidden(false); paulo@89: }); paulo@89: paulo@89: lines.dispatch.on('elementMouseout.tooltip', function(evt) { paulo@89: tooltip.hidden(true) paulo@89: }); paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: // expose chart's sub-components paulo@89: chart.dispatch = dispatch; paulo@89: chart.legend = legend; paulo@89: chart.lines = lines; paulo@89: chart.lines2 = lines2; paulo@89: chart.xAxis = xAxis; paulo@89: chart.yAxis = yAxis; paulo@89: chart.x2Axis = x2Axis; paulo@89: chart.y2Axis = y2Axis; paulo@89: chart.interactiveLayer = interactiveLayer; paulo@89: chart.tooltip = tooltip; paulo@89: paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: focusHeight: {get: function(){return height2;}, set: function(_){height2=_;}}, paulo@89: showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}}, paulo@89: brushExtent: {get: function(){return brushExtent;}, set: function(_){brushExtent=_;}}, paulo@89: defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}}, paulo@89: noData: {get: function(){return noData;}, set: function(_){noData=_;}}, paulo@89: paulo@89: // deprecated options paulo@89: tooltips: {get: function(){return tooltip.enabled();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead'); paulo@89: tooltip.enabled(!!_); paulo@89: }}, paulo@89: tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead'); paulo@89: tooltip.contentGenerator(_); paulo@89: }}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: legend.color(color); paulo@89: // line color is handled above? paulo@89: }}, paulo@89: interpolate: {get: function(){return lines.interpolate();}, set: function(_){ paulo@89: lines.interpolate(_); paulo@89: lines2.interpolate(_); paulo@89: }}, paulo@89: xTickFormat: {get: function(){return xAxis.tickFormat();}, set: function(_){ paulo@89: xAxis.tickFormat(_); paulo@89: x2Axis.tickFormat(_); paulo@89: }}, paulo@89: yTickFormat: {get: function(){return yAxis.tickFormat();}, set: function(_){ paulo@89: yAxis.tickFormat(_); paulo@89: y2Axis.tickFormat(_); paulo@89: }}, paulo@89: duration: {get: function(){return transitionDuration;}, set: function(_){ paulo@89: transitionDuration=_; paulo@89: yAxis.duration(transitionDuration); paulo@89: y2Axis.duration(transitionDuration); paulo@89: xAxis.duration(transitionDuration); paulo@89: x2Axis.duration(transitionDuration); paulo@89: }}, paulo@89: x: {get: function(){return lines.x();}, set: function(_){ paulo@89: lines.x(_); paulo@89: lines2.x(_); paulo@89: }}, paulo@89: y: {get: function(){return lines.y();}, set: function(_){ paulo@89: lines.y(_); paulo@89: lines2.y(_); paulo@89: }}, paulo@89: useInteractiveGuideline: {get: function(){return useInteractiveGuideline;}, set: function(_){ paulo@89: useInteractiveGuideline = _; paulo@89: if (useInteractiveGuideline) { paulo@89: lines.interactive(false); paulo@89: lines.useVoronoi(false); paulo@89: } paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.inheritOptions(chart, lines); paulo@89: nv.utils.initOptions(chart); paulo@89: paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: nv.models.multiBar = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var margin = {top: 0, right: 0, bottom: 0, left: 0} paulo@89: , width = 960 paulo@89: , height = 500 paulo@89: , x = d3.scale.ordinal() paulo@89: , y = d3.scale.linear() paulo@89: , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one paulo@89: , container = null paulo@89: , getX = function(d) { return d.x } paulo@89: , getY = function(d) { return d.y } paulo@89: , forceY = [0] // 0 is forced by default.. this makes sense for the majority of bar graphs... user can always do chart.forceY([]) to remove paulo@89: , clipEdge = true paulo@89: , stacked = false paulo@89: , stackOffset = 'zero' // options include 'silhouette', 'wiggle', 'expand', 'zero', or a custom function paulo@89: , color = nv.utils.defaultColor() paulo@89: , hideable = false paulo@89: , barColor = null // adding the ability to set the color for each rather than the whole group paulo@89: , disabled // used in conjunction with barColor to communicate from multiBarHorizontalChart what series are disabled paulo@89: , duration = 500 paulo@89: , xDomain paulo@89: , yDomain paulo@89: , xRange paulo@89: , yRange paulo@89: , groupSpacing = 0.1 paulo@89: , dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd') paulo@89: ; paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var x0, y0 //used to store previous scales paulo@89: , renderWatch = nv.utils.renderWatch(dispatch, duration) paulo@89: ; paulo@89: paulo@89: var last_datalength = 0; paulo@89: paulo@89: function chart(selection) { paulo@89: renderWatch.reset(); paulo@89: selection.each(function(data) { paulo@89: var availableWidth = width - margin.left - margin.right, paulo@89: availableHeight = height - margin.top - margin.bottom; paulo@89: paulo@89: container = d3.select(this); paulo@89: nv.utils.initSVG(container); paulo@89: var nonStackableCount = 0; paulo@89: // This function defines the requirements for render complete paulo@89: var endFn = function(d, i) { paulo@89: if (d.series === data.length - 1 && i === data[0].values.length - 1) paulo@89: return true; paulo@89: return false; paulo@89: }; paulo@89: paulo@89: if(hideable && data.length) hideable = [{ paulo@89: values: data[0].values.map(function(d) { paulo@89: return { paulo@89: x: d.x, paulo@89: y: 0, paulo@89: series: d.series, paulo@89: size: 0.01 paulo@89: };} paulo@89: )}]; paulo@89: paulo@89: if (stacked) { paulo@89: var parsed = d3.layout.stack() paulo@89: .offset(stackOffset) paulo@89: .values(function(d){ return d.values }) paulo@89: .y(getY) paulo@89: (!data.length && hideable ? hideable : data); paulo@89: paulo@89: parsed.forEach(function(series, i){ paulo@89: // if series is non-stackable, use un-parsed data paulo@89: if (series.nonStackable) { paulo@89: data[i].nonStackableSeries = nonStackableCount++; paulo@89: parsed[i] = data[i]; paulo@89: } else { paulo@89: // don't stack this seires on top of the nonStackable seriees paulo@89: if (i > 0 && parsed[i - 1].nonStackable){ paulo@89: parsed[i].values.map(function(d,j){ paulo@89: d.y0 -= parsed[i - 1].values[j].y; paulo@89: d.y1 = d.y0 + d.y; paulo@89: }); paulo@89: } paulo@89: } paulo@89: }); paulo@89: data = parsed; paulo@89: } paulo@89: //add series index and key to each data point for reference paulo@89: data.forEach(function(series, i) { paulo@89: series.values.forEach(function(point) { paulo@89: point.series = i; paulo@89: point.key = series.key; paulo@89: }); paulo@89: }); paulo@89: paulo@89: // HACK for negative value stacking paulo@89: if (stacked) { paulo@89: data[0].values.map(function(d,i) { paulo@89: var posBase = 0, negBase = 0; paulo@89: data.map(function(d, idx) { paulo@89: if (!data[idx].nonStackable) { paulo@89: var f = d.values[i] paulo@89: f.size = Math.abs(f.y); paulo@89: if (f.y<0) { paulo@89: f.y1 = negBase; paulo@89: negBase = negBase - f.size; paulo@89: } else paulo@89: { paulo@89: f.y1 = f.size + posBase; paulo@89: posBase = posBase + f.size; paulo@89: } paulo@89: } paulo@89: paulo@89: }); paulo@89: }); paulo@89: } paulo@89: // Setup Scales paulo@89: // remap and flatten the data for use in calculating the scales' domains paulo@89: var seriesData = (xDomain && yDomain) ? [] : // if we know xDomain and yDomain, no need to calculate paulo@89: data.map(function(d, idx) { paulo@89: return d.values.map(function(d,i) { paulo@89: return { x: getX(d,i), y: getY(d,i), y0: d.y0, y1: d.y1, idx:idx } paulo@89: }) paulo@89: }); paulo@89: paulo@89: x.domain(xDomain || d3.merge(seriesData).map(function(d) { return d.x })) paulo@89: .rangeBands(xRange || [0, availableWidth], groupSpacing); paulo@89: paulo@89: y.domain(yDomain || d3.extent(d3.merge(seriesData).map(function(d) { paulo@89: var domain = d.y; paulo@89: // increase the domain range if this series is stackable paulo@89: if (stacked && !data[d.idx].nonStackable) { paulo@89: if (d.y > 0){ paulo@89: domain = d.y1 paulo@89: } else { paulo@89: domain = d.y1 + d.y paulo@89: } paulo@89: } paulo@89: return domain; paulo@89: }).concat(forceY))) paulo@89: .range(yRange || [availableHeight, 0]); paulo@89: paulo@89: // If scale's domain don't have a range, slightly adjust to make one... so a chart can show a single data point paulo@89: if (x.domain()[0] === x.domain()[1]) paulo@89: x.domain()[0] ? paulo@89: x.domain([x.domain()[0] - x.domain()[0] * 0.01, x.domain()[1] + x.domain()[1] * 0.01]) paulo@89: : x.domain([-1,1]); paulo@89: paulo@89: if (y.domain()[0] === y.domain()[1]) paulo@89: y.domain()[0] ? paulo@89: y.domain([y.domain()[0] + y.domain()[0] * 0.01, y.domain()[1] - y.domain()[1] * 0.01]) paulo@89: : y.domain([-1,1]); paulo@89: paulo@89: x0 = x0 || x; paulo@89: y0 = y0 || y; paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-multibar').data([data]); paulo@89: var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-multibar'); paulo@89: var defsEnter = wrapEnter.append('defs'); paulo@89: var gEnter = wrapEnter.append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-groups'); paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: defsEnter.append('clipPath') paulo@89: .attr('id', 'nv-edge-clip-' + id) paulo@89: .append('rect'); paulo@89: wrap.select('#nv-edge-clip-' + id + ' rect') paulo@89: .attr('width', availableWidth) paulo@89: .attr('height', availableHeight); paulo@89: paulo@89: g.attr('clip-path', clipEdge ? 'url(#nv-edge-clip-' + id + ')' : ''); paulo@89: paulo@89: var groups = wrap.select('.nv-groups').selectAll('.nv-group') paulo@89: .data(function(d) { return d }, function(d,i) { return i }); paulo@89: groups.enter().append('g') paulo@89: .style('stroke-opacity', 1e-6) paulo@89: .style('fill-opacity', 1e-6); paulo@89: paulo@89: var exitTransition = renderWatch paulo@89: .transition(groups.exit().selectAll('rect.nv-bar'), 'multibarExit', Math.min(100, duration)) paulo@89: .attr('y', function(d, i, j) { paulo@89: var yVal = y0(0) || 0; paulo@89: if (stacked) { paulo@89: if (data[d.series] && !data[d.series].nonStackable) { paulo@89: yVal = y0(d.y0); paulo@89: } paulo@89: } paulo@89: return yVal; paulo@89: }) paulo@89: .attr('height', 0) paulo@89: .remove(); paulo@89: if (exitTransition.delay) paulo@89: exitTransition.delay(function(d,i) { paulo@89: var delay = i * (duration / (last_datalength + 1)) - i; paulo@89: return delay; paulo@89: }); paulo@89: groups paulo@89: .attr('class', function(d,i) { return 'nv-group nv-series-' + i }) paulo@89: .classed('hover', function(d) { return d.hover }) paulo@89: .style('fill', function(d,i){ return color(d, i) }) paulo@89: .style('stroke', function(d,i){ return color(d, i) }); paulo@89: groups paulo@89: .style('stroke-opacity', 1) paulo@89: .style('fill-opacity', 0.75); paulo@89: paulo@89: var bars = groups.selectAll('rect.nv-bar') paulo@89: .data(function(d) { return (hideable && !data.length) ? hideable.values : d.values }); paulo@89: bars.exit().remove(); paulo@89: paulo@89: var barsEnter = bars.enter().append('rect') paulo@89: .attr('class', function(d,i) { return getY(d,i) < 0 ? 'nv-bar negative' : 'nv-bar positive'}) paulo@89: .attr('x', function(d,i,j) { paulo@89: return stacked && !data[j].nonStackable ? 0 : (j * x.rangeBand() / data.length ) paulo@89: }) paulo@89: .attr('y', function(d,i,j) { return y0(stacked && !data[j].nonStackable ? d.y0 : 0) || 0 }) paulo@89: .attr('height', 0) paulo@89: .attr('width', function(d,i,j) { return x.rangeBand() / (stacked && !data[j].nonStackable ? 1 : data.length) }) paulo@89: .attr('transform', function(d,i) { return 'translate(' + x(getX(d,i)) + ',0)'; }) paulo@89: ; paulo@89: bars paulo@89: .style('fill', function(d,i,j){ return color(d, j, i); }) paulo@89: .style('stroke', function(d,i,j){ return color(d, j, i); }) paulo@89: .on('mouseover', function(d,i) { //TODO: figure out why j works above, but not here paulo@89: d3.select(this).classed('hover', true); paulo@89: dispatch.elementMouseover({ paulo@89: data: d, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: }) paulo@89: .on('mouseout', function(d,i) { paulo@89: d3.select(this).classed('hover', false); paulo@89: dispatch.elementMouseout({ paulo@89: data: d, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: }) paulo@89: .on('mousemove', function(d,i) { paulo@89: dispatch.elementMousemove({ paulo@89: data: d, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: }) paulo@89: .on('click', function(d,i) { paulo@89: dispatch.elementClick({ paulo@89: data: d, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: d3.event.stopPropagation(); paulo@89: }) paulo@89: .on('dblclick', function(d,i) { paulo@89: dispatch.elementDblClick({ paulo@89: data: d, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: d3.event.stopPropagation(); paulo@89: }); paulo@89: bars paulo@89: .attr('class', function(d,i) { return getY(d,i) < 0 ? 'nv-bar negative' : 'nv-bar positive'}) paulo@89: .attr('transform', function(d,i) { return 'translate(' + x(getX(d,i)) + ',0)'; }) paulo@89: paulo@89: if (barColor) { paulo@89: if (!disabled) disabled = data.map(function() { return true }); paulo@89: bars paulo@89: .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(); }) paulo@89: .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(); }); paulo@89: } paulo@89: paulo@89: var barSelection = paulo@89: bars.watchTransition(renderWatch, 'multibar', Math.min(250, duration)) paulo@89: .delay(function(d,i) { paulo@89: return i * duration / data[0].values.length; paulo@89: }); paulo@89: if (stacked){ paulo@89: barSelection paulo@89: .attr('y', function(d,i,j) { paulo@89: var yVal = 0; paulo@89: // if stackable, stack it on top of the previous series paulo@89: if (!data[j].nonStackable) { paulo@89: yVal = y(d.y1); paulo@89: } else { paulo@89: if (getY(d,i) < 0){ paulo@89: yVal = y(0); paulo@89: } else { paulo@89: if (y(0) - y(getY(d,i)) < -1){ paulo@89: yVal = y(0) - 1; paulo@89: } else { paulo@89: yVal = y(getY(d, i)) || 0; paulo@89: } paulo@89: } paulo@89: } paulo@89: return yVal; paulo@89: }) paulo@89: .attr('height', function(d,i,j) { paulo@89: if (!data[j].nonStackable) { paulo@89: return Math.max(Math.abs(y(d.y+d.y0) - y(d.y0)), 1); paulo@89: } else { paulo@89: return Math.max(Math.abs(y(getY(d,i)) - y(0)),1) || 0; paulo@89: } paulo@89: }) paulo@89: .attr('x', function(d,i,j) { paulo@89: var width = 0; paulo@89: if (data[j].nonStackable) { paulo@89: width = d.series * x.rangeBand() / data.length; paulo@89: if (data.length !== nonStackableCount){ paulo@89: width = data[j].nonStackableSeries * x.rangeBand()/(nonStackableCount*2); paulo@89: } paulo@89: } paulo@89: return width; paulo@89: }) paulo@89: .attr('width', function(d,i,j){ paulo@89: if (!data[j].nonStackable) { paulo@89: return x.rangeBand(); paulo@89: } else { paulo@89: // if all series are nonStacable, take the full width paulo@89: var width = (x.rangeBand() / nonStackableCount); paulo@89: // otherwise, nonStackable graph will be only taking the half-width paulo@89: // of the x rangeBand paulo@89: if (data.length !== nonStackableCount) { paulo@89: width = x.rangeBand()/(nonStackableCount*2); paulo@89: } paulo@89: return width; paulo@89: } paulo@89: }); paulo@89: } paulo@89: else { paulo@89: barSelection paulo@89: .attr('x', function(d,i) { paulo@89: return d.series * x.rangeBand() / data.length; paulo@89: }) paulo@89: .attr('width', x.rangeBand() / data.length) paulo@89: .attr('y', function(d,i) { paulo@89: return getY(d,i) < 0 ? paulo@89: y(0) : paulo@89: y(0) - y(getY(d,i)) < 1 ? paulo@89: y(0) - 1 : paulo@89: y(getY(d,i)) || 0; paulo@89: }) paulo@89: .attr('height', function(d,i) { paulo@89: return Math.max(Math.abs(y(getY(d,i)) - y(0)),1) || 0; paulo@89: }); paulo@89: } paulo@89: paulo@89: //store old scales for use in transitions on update paulo@89: x0 = x.copy(); paulo@89: y0 = y.copy(); paulo@89: paulo@89: // keep track of the last data value length for transition calculations paulo@89: if (data[0] && data[0].values) { paulo@89: last_datalength = data[0].values.length; paulo@89: } paulo@89: paulo@89: }); paulo@89: paulo@89: renderWatch.renderEnd('multibar immediate'); paulo@89: paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: chart.dispatch = dispatch; paulo@89: paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: x: {get: function(){return getX;}, set: function(_){getX=_;}}, paulo@89: y: {get: function(){return getY;}, set: function(_){getY=_;}}, paulo@89: xScale: {get: function(){return x;}, set: function(_){x=_;}}, paulo@89: yScale: {get: function(){return y;}, set: function(_){y=_;}}, paulo@89: xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}}, paulo@89: yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}}, paulo@89: xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}}, paulo@89: yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}}, paulo@89: forceY: {get: function(){return forceY;}, set: function(_){forceY=_;}}, paulo@89: stacked: {get: function(){return stacked;}, set: function(_){stacked=_;}}, paulo@89: stackOffset: {get: function(){return stackOffset;}, set: function(_){stackOffset=_;}}, paulo@89: clipEdge: {get: function(){return clipEdge;}, set: function(_){clipEdge=_;}}, paulo@89: disabled: {get: function(){return disabled;}, set: function(_){disabled=_;}}, paulo@89: id: {get: function(){return id;}, set: function(_){id=_;}}, paulo@89: hideable: {get: function(){return hideable;}, set: function(_){hideable=_;}}, paulo@89: groupSpacing:{get: function(){return groupSpacing;}, set: function(_){groupSpacing=_;}}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: duration: {get: function(){return duration;}, set: function(_){ paulo@89: duration = _; paulo@89: renderWatch.reset(duration); paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: }}, paulo@89: barColor: {get: function(){return barColor;}, set: function(_){ paulo@89: barColor = _ ? nv.utils.getColor(_) : null; paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.initOptions(chart); paulo@89: paulo@89: return chart; paulo@89: }; paulo@89: nv.models.multiBarChart = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var multibar = nv.models.multiBar() paulo@89: , xAxis = nv.models.axis() paulo@89: , yAxis = nv.models.axis() paulo@89: , legend = nv.models.legend() paulo@89: , controls = nv.models.legend() paulo@89: , tooltip = nv.models.tooltip() paulo@89: ; paulo@89: paulo@89: var margin = {top: 30, right: 20, bottom: 50, left: 60} paulo@89: , width = null paulo@89: , height = null paulo@89: , color = nv.utils.defaultColor() paulo@89: , showControls = true paulo@89: , controlLabels = {} paulo@89: , showLegend = true paulo@89: , showXAxis = true paulo@89: , showYAxis = true paulo@89: , rightAlignYAxis = false paulo@89: , reduceXTicks = true // if false a tick will show for every data point paulo@89: , staggerLabels = false paulo@89: , rotateLabels = 0 paulo@89: , x //can be accessed via chart.xScale() paulo@89: , y //can be accessed via chart.yScale() paulo@89: , state = nv.utils.state() paulo@89: , defaultState = null paulo@89: , noData = null paulo@89: , dispatch = d3.dispatch('stateChange', 'changeState', 'renderEnd') paulo@89: , controlWidth = function() { return showControls ? 180 : 0 } paulo@89: , duration = 250 paulo@89: ; paulo@89: paulo@89: state.stacked = false // DEPRECATED Maintained for backward compatibility paulo@89: paulo@89: multibar.stacked(false); paulo@89: xAxis paulo@89: .orient('bottom') paulo@89: .tickPadding(7) paulo@89: .showMaxMin(false) paulo@89: .tickFormat(function(d) { return d }) paulo@89: ; paulo@89: yAxis paulo@89: .orient((rightAlignYAxis) ? 'right' : 'left') paulo@89: .tickFormat(d3.format(',.1f')) paulo@89: ; paulo@89: paulo@89: tooltip paulo@89: .duration(0) paulo@89: .valueFormatter(function(d, i) { paulo@89: return yAxis.tickFormat()(d, i); paulo@89: }) paulo@89: .headerFormatter(function(d, i) { paulo@89: return xAxis.tickFormat()(d, i); paulo@89: }); paulo@89: paulo@89: controls.updateState(false); paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var renderWatch = nv.utils.renderWatch(dispatch); paulo@89: var stacked = false; paulo@89: paulo@89: var stateGetter = function(data) { paulo@89: return function(){ paulo@89: return { paulo@89: active: data.map(function(d) { return !d.disabled }), paulo@89: stacked: stacked paulo@89: }; paulo@89: } paulo@89: }; paulo@89: paulo@89: var stateSetter = function(data) { paulo@89: return function(state) { paulo@89: if (state.stacked !== undefined) paulo@89: stacked = state.stacked; paulo@89: if (state.active !== undefined) paulo@89: data.forEach(function(series,i) { paulo@89: series.disabled = !state.active[i]; paulo@89: }); paulo@89: } paulo@89: }; paulo@89: paulo@89: function chart(selection) { paulo@89: renderWatch.reset(); paulo@89: renderWatch.models(multibar); paulo@89: if (showXAxis) renderWatch.models(xAxis); paulo@89: if (showYAxis) renderWatch.models(yAxis); paulo@89: paulo@89: selection.each(function(data) { paulo@89: var container = d3.select(this), paulo@89: that = this; paulo@89: nv.utils.initSVG(container); paulo@89: var availableWidth = nv.utils.availableWidth(width, container, margin), paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: paulo@89: chart.update = function() { paulo@89: if (duration === 0) paulo@89: container.call(chart); paulo@89: else paulo@89: container.transition() paulo@89: .duration(duration) paulo@89: .call(chart); paulo@89: }; paulo@89: chart.container = this; paulo@89: paulo@89: state paulo@89: .setter(stateSetter(data), chart.update) paulo@89: .getter(stateGetter(data)) paulo@89: .update(); paulo@89: paulo@89: // DEPRECATED set state.disableddisabled paulo@89: state.disabled = data.map(function(d) { return !!d.disabled }); paulo@89: paulo@89: if (!defaultState) { paulo@89: var key; paulo@89: defaultState = {}; paulo@89: for (key in state) { paulo@89: if (state[key] instanceof Array) paulo@89: defaultState[key] = state[key].slice(0); paulo@89: else paulo@89: defaultState[key] = state[key]; paulo@89: } paulo@89: } paulo@89: paulo@89: // Display noData message if there's nothing to show. paulo@89: if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) { paulo@89: nv.utils.noData(chart, container) paulo@89: return chart; paulo@89: } else { paulo@89: container.selectAll('.nv-noData').remove(); paulo@89: } paulo@89: paulo@89: // Setup Scales paulo@89: x = multibar.xScale(); paulo@89: y = multibar.yScale(); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-multiBarWithLegend').data([data]); paulo@89: var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-multiBarWithLegend').append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-x nv-axis'); paulo@89: gEnter.append('g').attr('class', 'nv-y nv-axis'); paulo@89: gEnter.append('g').attr('class', 'nv-barsWrap'); paulo@89: gEnter.append('g').attr('class', 'nv-legendWrap'); paulo@89: gEnter.append('g').attr('class', 'nv-controlsWrap'); paulo@89: paulo@89: // Legend paulo@89: if (showLegend) { paulo@89: legend.width(availableWidth - controlWidth()); paulo@89: paulo@89: g.select('.nv-legendWrap') paulo@89: .datum(data) paulo@89: .call(legend); paulo@89: paulo@89: if ( margin.top != legend.height()) { paulo@89: margin.top = legend.height(); paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: } paulo@89: paulo@89: g.select('.nv-legendWrap') paulo@89: .attr('transform', 'translate(' + controlWidth() + ',' + (-margin.top) +')'); paulo@89: } paulo@89: paulo@89: // Controls paulo@89: if (showControls) { paulo@89: var controlsData = [ paulo@89: { key: controlLabels.grouped || 'Grouped', disabled: multibar.stacked() }, paulo@89: { key: controlLabels.stacked || 'Stacked', disabled: !multibar.stacked() } paulo@89: ]; paulo@89: paulo@89: controls.width(controlWidth()).color(['#444', '#444', '#444']); paulo@89: g.select('.nv-controlsWrap') paulo@89: .datum(controlsData) paulo@89: .attr('transform', 'translate(0,' + (-margin.top) +')') paulo@89: .call(controls); paulo@89: } paulo@89: paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: if (rightAlignYAxis) { paulo@89: g.select(".nv-y.nv-axis") paulo@89: .attr("transform", "translate(" + availableWidth + ",0)"); paulo@89: } paulo@89: paulo@89: // Main Chart Component(s) paulo@89: multibar paulo@89: .disabled(data.map(function(series) { return series.disabled })) paulo@89: .width(availableWidth) paulo@89: .height(availableHeight) paulo@89: .color(data.map(function(d,i) { paulo@89: return d.color || color(d, i); paulo@89: }).filter(function(d,i) { return !data[i].disabled })); paulo@89: paulo@89: paulo@89: var barsWrap = g.select('.nv-barsWrap') paulo@89: .datum(data.filter(function(d) { return !d.disabled })); paulo@89: paulo@89: barsWrap.call(multibar); paulo@89: paulo@89: // Setup Axes paulo@89: if (showXAxis) { paulo@89: xAxis paulo@89: .scale(x) paulo@89: ._ticks( nv.utils.calcTicksX(availableWidth/100, data) ) paulo@89: .tickSize(-availableHeight, 0); paulo@89: paulo@89: g.select('.nv-x.nv-axis') paulo@89: .attr('transform', 'translate(0,' + y.range()[0] + ')'); paulo@89: g.select('.nv-x.nv-axis') paulo@89: .call(xAxis); paulo@89: paulo@89: var xTicks = g.select('.nv-x.nv-axis > g').selectAll('g'); paulo@89: paulo@89: xTicks paulo@89: .selectAll('line, text') paulo@89: .style('opacity', 1) paulo@89: paulo@89: if (staggerLabels) { paulo@89: var getTranslate = function(x,y) { paulo@89: return "translate(" + x + "," + y + ")"; paulo@89: }; paulo@89: paulo@89: var staggerUp = 5, staggerDown = 17; //pixels to stagger by paulo@89: // Issue #140 paulo@89: xTicks paulo@89: .selectAll("text") paulo@89: .attr('transform', function(d,i,j) { paulo@89: return getTranslate(0, (j % 2 == 0 ? staggerUp : staggerDown)); paulo@89: }); paulo@89: paulo@89: var totalInBetweenTicks = d3.selectAll(".nv-x.nv-axis .nv-wrap g g text")[0].length; paulo@89: g.selectAll(".nv-x.nv-axis .nv-axisMaxMin text") paulo@89: .attr("transform", function(d,i) { paulo@89: return getTranslate(0, (i === 0 || totalInBetweenTicks % 2 !== 0) ? staggerDown : staggerUp); paulo@89: }); paulo@89: } paulo@89: paulo@89: if (reduceXTicks) paulo@89: xTicks paulo@89: .filter(function(d,i) { paulo@89: return i % Math.ceil(data[0].values.length / (availableWidth / 100)) !== 0; paulo@89: }) paulo@89: .selectAll('text, line') paulo@89: .style('opacity', 0); paulo@89: paulo@89: if(rotateLabels) paulo@89: xTicks paulo@89: .selectAll('.tick text') paulo@89: .attr('transform', 'rotate(' + rotateLabels + ' 0,0)') paulo@89: .style('text-anchor', rotateLabels > 0 ? 'start' : 'end'); paulo@89: paulo@89: g.select('.nv-x.nv-axis').selectAll('g.nv-axisMaxMin text') paulo@89: .style('opacity', 1); paulo@89: } paulo@89: paulo@89: if (showYAxis) { paulo@89: yAxis paulo@89: .scale(y) paulo@89: ._ticks( nv.utils.calcTicksY(availableHeight/36, data) ) paulo@89: .tickSize( -availableWidth, 0); paulo@89: paulo@89: g.select('.nv-y.nv-axis') paulo@89: .call(yAxis); paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (in chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: legend.dispatch.on('stateChange', function(newState) { paulo@89: for (var key in newState) paulo@89: state[key] = newState[key]; paulo@89: dispatch.stateChange(state); paulo@89: chart.update(); paulo@89: }); paulo@89: paulo@89: controls.dispatch.on('legendClick', function(d,i) { paulo@89: if (!d.disabled) return; paulo@89: controlsData = controlsData.map(function(s) { paulo@89: s.disabled = true; paulo@89: return s; paulo@89: }); paulo@89: d.disabled = false; paulo@89: paulo@89: switch (d.key) { paulo@89: case 'Grouped': paulo@89: case controlLabels.grouped: paulo@89: multibar.stacked(false); paulo@89: break; paulo@89: case 'Stacked': paulo@89: case controlLabels.stacked: paulo@89: multibar.stacked(true); paulo@89: break; paulo@89: } paulo@89: paulo@89: state.stacked = multibar.stacked(); paulo@89: dispatch.stateChange(state); paulo@89: chart.update(); paulo@89: }); paulo@89: paulo@89: // Update chart from a state object passed to event handler paulo@89: dispatch.on('changeState', function(e) { paulo@89: if (typeof e.disabled !== 'undefined') { paulo@89: data.forEach(function(series,i) { paulo@89: series.disabled = e.disabled[i]; paulo@89: }); paulo@89: state.disabled = e.disabled; paulo@89: } paulo@89: if (typeof e.stacked !== 'undefined') { paulo@89: multibar.stacked(e.stacked); paulo@89: state.stacked = e.stacked; paulo@89: stacked = e.stacked; paulo@89: } paulo@89: chart.update(); paulo@89: }); paulo@89: }); paulo@89: paulo@89: renderWatch.renderEnd('multibarchart immediate'); paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (out of chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: multibar.dispatch.on('elementMouseover.tooltip', function(evt) { paulo@89: evt.value = chart.x()(evt.data); paulo@89: evt['series'] = { paulo@89: key: evt.data.key, paulo@89: value: chart.y()(evt.data), paulo@89: color: evt.color paulo@89: }; paulo@89: tooltip.data(evt).hidden(false); paulo@89: }); paulo@89: paulo@89: multibar.dispatch.on('elementMouseout.tooltip', function(evt) { paulo@89: tooltip.hidden(true); paulo@89: }); paulo@89: paulo@89: multibar.dispatch.on('elementMousemove.tooltip', function(evt) { paulo@89: tooltip.position({top: d3.event.pageY, left: d3.event.pageX})(); paulo@89: }); paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: // expose chart's sub-components paulo@89: chart.dispatch = dispatch; paulo@89: chart.multibar = multibar; paulo@89: chart.legend = legend; paulo@89: chart.controls = controls; paulo@89: chart.xAxis = xAxis; paulo@89: chart.yAxis = yAxis; paulo@89: chart.state = state; paulo@89: chart.tooltip = tooltip; paulo@89: paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}}, paulo@89: showControls: {get: function(){return showControls;}, set: function(_){showControls=_;}}, paulo@89: controlLabels: {get: function(){return controlLabels;}, set: function(_){controlLabels=_;}}, paulo@89: showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}}, paulo@89: showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}}, paulo@89: defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}}, paulo@89: noData: {get: function(){return noData;}, set: function(_){noData=_;}}, paulo@89: reduceXTicks: {get: function(){return reduceXTicks;}, set: function(_){reduceXTicks=_;}}, paulo@89: rotateLabels: {get: function(){return rotateLabels;}, set: function(_){rotateLabels=_;}}, paulo@89: staggerLabels: {get: function(){return staggerLabels;}, set: function(_){staggerLabels=_;}}, paulo@89: paulo@89: // deprecated options paulo@89: tooltips: {get: function(){return tooltip.enabled();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead'); paulo@89: tooltip.enabled(!!_); paulo@89: }}, paulo@89: tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead'); paulo@89: tooltip.contentGenerator(_); paulo@89: }}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: duration: {get: function(){return duration;}, set: function(_){ paulo@89: duration = _; paulo@89: multibar.duration(duration); paulo@89: xAxis.duration(duration); paulo@89: yAxis.duration(duration); paulo@89: renderWatch.reset(duration); paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: legend.color(color); paulo@89: }}, paulo@89: rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){ paulo@89: rightAlignYAxis = _; paulo@89: yAxis.orient( rightAlignYAxis ? 'right' : 'left'); paulo@89: }}, paulo@89: barColor: {get: function(){return multibar.barColor;}, set: function(_){ paulo@89: multibar.barColor(_); paulo@89: legend.color(function(d,i) {return d3.rgb('#ccc').darker(i * 1.5).toString();}) paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.inheritOptions(chart, multibar); paulo@89: nv.utils.initOptions(chart); paulo@89: paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: nv.models.multiBarHorizontal = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var margin = {top: 0, right: 0, bottom: 0, left: 0} paulo@89: , width = 960 paulo@89: , height = 500 paulo@89: , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one paulo@89: , container = null paulo@89: , x = d3.scale.ordinal() paulo@89: , y = d3.scale.linear() paulo@89: , getX = function(d) { return d.x } paulo@89: , getY = function(d) { return d.y } paulo@89: , getYerr = function(d) { return d.yErr } paulo@89: , forceY = [0] // 0 is forced by default.. this makes sense for the majority of bar graphs... user can always do chart.forceY([]) to remove paulo@89: , color = nv.utils.defaultColor() paulo@89: , barColor = null // adding the ability to set the color for each rather than the whole group paulo@89: , disabled // used in conjunction with barColor to communicate from multiBarHorizontalChart what series are disabled paulo@89: , stacked = false paulo@89: , showValues = false paulo@89: , showBarLabels = false paulo@89: , valuePadding = 60 paulo@89: , groupSpacing = 0.1 paulo@89: , valueFormat = d3.format(',.2f') paulo@89: , delay = 1200 paulo@89: , xDomain paulo@89: , yDomain paulo@89: , xRange paulo@89: , yRange paulo@89: , duration = 250 paulo@89: , dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd') paulo@89: ; paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var x0, y0; //used to store previous scales paulo@89: var renderWatch = nv.utils.renderWatch(dispatch, duration); paulo@89: paulo@89: function chart(selection) { paulo@89: renderWatch.reset(); paulo@89: selection.each(function(data) { paulo@89: var availableWidth = width - margin.left - margin.right, paulo@89: availableHeight = height - margin.top - margin.bottom; paulo@89: paulo@89: container = d3.select(this); paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: if (stacked) paulo@89: data = d3.layout.stack() paulo@89: .offset('zero') paulo@89: .values(function(d){ return d.values }) paulo@89: .y(getY) paulo@89: (data); paulo@89: paulo@89: //add series index and key to each data point for reference paulo@89: data.forEach(function(series, i) { paulo@89: series.values.forEach(function(point) { paulo@89: point.series = i; paulo@89: point.key = series.key; paulo@89: }); paulo@89: }); paulo@89: paulo@89: // HACK for negative value stacking paulo@89: if (stacked) paulo@89: data[0].values.map(function(d,i) { paulo@89: var posBase = 0, negBase = 0; paulo@89: data.map(function(d) { paulo@89: var f = d.values[i] paulo@89: f.size = Math.abs(f.y); paulo@89: if (f.y<0) { paulo@89: f.y1 = negBase - f.size; paulo@89: negBase = negBase - f.size; paulo@89: } else paulo@89: { paulo@89: f.y1 = posBase; paulo@89: posBase = posBase + f.size; paulo@89: } paulo@89: }); paulo@89: }); paulo@89: paulo@89: // Setup Scales paulo@89: // remap and flatten the data for use in calculating the scales' domains paulo@89: var seriesData = (xDomain && yDomain) ? [] : // if we know xDomain and yDomain, no need to calculate paulo@89: data.map(function(d) { paulo@89: return d.values.map(function(d,i) { paulo@89: return { x: getX(d,i), y: getY(d,i), y0: d.y0, y1: d.y1 } paulo@89: }) paulo@89: }); paulo@89: paulo@89: x.domain(xDomain || d3.merge(seriesData).map(function(d) { return d.x })) paulo@89: .rangeBands(xRange || [0, availableHeight], groupSpacing); paulo@89: paulo@89: 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))) paulo@89: paulo@89: if (showValues && !stacked) paulo@89: y.range(yRange || [(y.domain()[0] < 0 ? valuePadding : 0), availableWidth - (y.domain()[1] > 0 ? valuePadding : 0) ]); paulo@89: else paulo@89: y.range(yRange || [0, availableWidth]); paulo@89: paulo@89: x0 = x0 || x; paulo@89: y0 = y0 || d3.scale.linear().domain(y.domain()).range([y(0),y(0)]); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = d3.select(this).selectAll('g.nv-wrap.nv-multibarHorizontal').data([data]); paulo@89: var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-multibarHorizontal'); paulo@89: var defsEnter = wrapEnter.append('defs'); paulo@89: var gEnter = wrapEnter.append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-groups'); paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: var groups = wrap.select('.nv-groups').selectAll('.nv-group') paulo@89: .data(function(d) { return d }, function(d,i) { return i }); paulo@89: groups.enter().append('g') paulo@89: .style('stroke-opacity', 1e-6) paulo@89: .style('fill-opacity', 1e-6); paulo@89: groups.exit().watchTransition(renderWatch, 'multibarhorizontal: exit groups') paulo@89: .style('stroke-opacity', 1e-6) paulo@89: .style('fill-opacity', 1e-6) paulo@89: .remove(); paulo@89: groups paulo@89: .attr('class', function(d,i) { return 'nv-group nv-series-' + i }) paulo@89: .classed('hover', function(d) { return d.hover }) paulo@89: .style('fill', function(d,i){ return color(d, i) }) paulo@89: .style('stroke', function(d,i){ return color(d, i) }); paulo@89: groups.watchTransition(renderWatch, 'multibarhorizontal: groups') paulo@89: .style('stroke-opacity', 1) paulo@89: .style('fill-opacity', .75); paulo@89: paulo@89: var bars = groups.selectAll('g.nv-bar') paulo@89: .data(function(d) { return d.values }); paulo@89: bars.exit().remove(); paulo@89: paulo@89: var barsEnter = bars.enter().append('g') paulo@89: .attr('transform', function(d,i,j) { paulo@89: return 'translate(' + y0(stacked ? d.y0 : 0) + ',' + (stacked ? 0 : (j * x.rangeBand() / data.length ) + x(getX(d,i))) + ')' paulo@89: }); paulo@89: paulo@89: barsEnter.append('rect') paulo@89: .attr('width', 0) paulo@89: .attr('height', x.rangeBand() / (stacked ? 1 : data.length) ) paulo@89: paulo@89: bars paulo@89: .on('mouseover', function(d,i) { //TODO: figure out why j works above, but not here paulo@89: d3.select(this).classed('hover', true); paulo@89: dispatch.elementMouseover({ paulo@89: data: d, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: }) paulo@89: .on('mouseout', function(d,i) { paulo@89: d3.select(this).classed('hover', false); paulo@89: dispatch.elementMouseout({ paulo@89: data: d, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: }) paulo@89: .on('mouseout', function(d,i) { paulo@89: dispatch.elementMouseout({ paulo@89: data: d, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: }) paulo@89: .on('mousemove', function(d,i) { paulo@89: dispatch.elementMousemove({ paulo@89: data: d, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: }) paulo@89: .on('click', function(d,i) { paulo@89: dispatch.elementClick({ paulo@89: data: d, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: d3.event.stopPropagation(); paulo@89: }) paulo@89: .on('dblclick', function(d,i) { paulo@89: dispatch.elementDblClick({ paulo@89: data: d, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: d3.event.stopPropagation(); paulo@89: }); paulo@89: paulo@89: if (getYerr(data[0],0)) { paulo@89: barsEnter.append('polyline'); paulo@89: paulo@89: bars.select('polyline') paulo@89: .attr('fill', 'none') paulo@89: .attr('points', function(d,i) { paulo@89: var xerr = getYerr(d,i) paulo@89: , mid = 0.8 * x.rangeBand() / ((stacked ? 1 : data.length) * 2); paulo@89: xerr = xerr.length ? xerr : [-Math.abs(xerr), Math.abs(xerr)]; paulo@89: xerr = xerr.map(function(e) { return y(e) - y(0); }); paulo@89: var a = [[xerr[0],-mid], [xerr[0],mid], [xerr[0],0], [xerr[1],0], [xerr[1],-mid], [xerr[1],mid]]; paulo@89: return a.map(function (path) { return path.join(',') }).join(' '); paulo@89: }) paulo@89: .attr('transform', function(d,i) { paulo@89: var mid = x.rangeBand() / ((stacked ? 1 : data.length) * 2); paulo@89: return 'translate(' + (getY(d,i) < 0 ? 0 : y(getY(d,i)) - y(0)) + ', ' + mid + ')' paulo@89: }); paulo@89: } paulo@89: paulo@89: barsEnter.append('text'); paulo@89: paulo@89: if (showValues && !stacked) { paulo@89: bars.select('text') paulo@89: .attr('text-anchor', function(d,i) { return getY(d,i) < 0 ? 'end' : 'start' }) paulo@89: .attr('y', x.rangeBand() / (data.length * 2)) paulo@89: .attr('dy', '.32em') paulo@89: .text(function(d,i) { paulo@89: var t = valueFormat(getY(d,i)) paulo@89: , yerr = getYerr(d,i); paulo@89: if (yerr === undefined) paulo@89: return t; paulo@89: if (!yerr.length) paulo@89: return t + '±' + valueFormat(Math.abs(yerr)); paulo@89: return t + '+' + valueFormat(Math.abs(yerr[1])) + '-' + valueFormat(Math.abs(yerr[0])); paulo@89: }); paulo@89: bars.watchTransition(renderWatch, 'multibarhorizontal: bars') paulo@89: .select('text') paulo@89: .attr('x', function(d,i) { return getY(d,i) < 0 ? -4 : y(getY(d,i)) - y(0) + 4 }) paulo@89: } else { paulo@89: bars.selectAll('text').text(''); paulo@89: } paulo@89: paulo@89: if (showBarLabels && !stacked) { paulo@89: barsEnter.append('text').classed('nv-bar-label',true); paulo@89: bars.select('text.nv-bar-label') paulo@89: .attr('text-anchor', function(d,i) { return getY(d,i) < 0 ? 'start' : 'end' }) paulo@89: .attr('y', x.rangeBand() / (data.length * 2)) paulo@89: .attr('dy', '.32em') paulo@89: .text(function(d,i) { return getX(d,i) }); paulo@89: bars.watchTransition(renderWatch, 'multibarhorizontal: bars') paulo@89: .select('text.nv-bar-label') paulo@89: .attr('x', function(d,i) { return getY(d,i) < 0 ? y(0) - y(getY(d,i)) + 4 : -4 }); paulo@89: } paulo@89: else { paulo@89: bars.selectAll('text.nv-bar-label').text(''); paulo@89: } paulo@89: paulo@89: bars paulo@89: .attr('class', function(d,i) { return getY(d,i) < 0 ? 'nv-bar negative' : 'nv-bar positive'}) paulo@89: paulo@89: if (barColor) { paulo@89: if (!disabled) disabled = data.map(function() { return true }); paulo@89: bars paulo@89: .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(); }) paulo@89: .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(); }); paulo@89: } paulo@89: paulo@89: if (stacked) paulo@89: bars.watchTransition(renderWatch, 'multibarhorizontal: bars') paulo@89: .attr('transform', function(d,i) { paulo@89: return 'translate(' + y(d.y1) + ',' + x(getX(d,i)) + ')' paulo@89: }) paulo@89: .select('rect') paulo@89: .attr('width', function(d,i) { paulo@89: return Math.abs(y(getY(d,i) + d.y0) - y(d.y0)) paulo@89: }) paulo@89: .attr('height', x.rangeBand() ); paulo@89: else paulo@89: bars.watchTransition(renderWatch, 'multibarhorizontal: bars') paulo@89: .attr('transform', function(d,i) { paulo@89: //TODO: stacked must be all positive or all negative, not both? paulo@89: return 'translate(' + paulo@89: (getY(d,i) < 0 ? y(getY(d,i)) : y(0)) paulo@89: + ',' + paulo@89: (d.series * x.rangeBand() / data.length paulo@89: + paulo@89: x(getX(d,i)) ) paulo@89: + ')' paulo@89: }) paulo@89: .select('rect') paulo@89: .attr('height', x.rangeBand() / data.length ) paulo@89: .attr('width', function(d,i) { paulo@89: return Math.max(Math.abs(y(getY(d,i)) - y(0)),1) paulo@89: }); paulo@89: paulo@89: //store old scales for use in transitions on update paulo@89: x0 = x.copy(); paulo@89: y0 = y.copy(); paulo@89: paulo@89: }); paulo@89: paulo@89: renderWatch.renderEnd('multibarHorizontal immediate'); paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: chart.dispatch = dispatch; paulo@89: paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: x: {get: function(){return getX;}, set: function(_){getX=_;}}, paulo@89: y: {get: function(){return getY;}, set: function(_){getY=_;}}, paulo@89: yErr: {get: function(){return getYerr;}, set: function(_){getYerr=_;}}, paulo@89: xScale: {get: function(){return x;}, set: function(_){x=_;}}, paulo@89: yScale: {get: function(){return y;}, set: function(_){y=_;}}, paulo@89: xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}}, paulo@89: yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}}, paulo@89: xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}}, paulo@89: yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}}, paulo@89: forceY: {get: function(){return forceY;}, set: function(_){forceY=_;}}, paulo@89: stacked: {get: function(){return stacked;}, set: function(_){stacked=_;}}, paulo@89: showValues: {get: function(){return showValues;}, set: function(_){showValues=_;}}, paulo@89: // this shows the group name, seems pointless? paulo@89: //showBarLabels: {get: function(){return showBarLabels;}, set: function(_){showBarLabels=_;}}, paulo@89: disabled: {get: function(){return disabled;}, set: function(_){disabled=_;}}, paulo@89: id: {get: function(){return id;}, set: function(_){id=_;}}, paulo@89: valueFormat: {get: function(){return valueFormat;}, set: function(_){valueFormat=_;}}, paulo@89: valuePadding: {get: function(){return valuePadding;}, set: function(_){valuePadding=_;}}, paulo@89: groupSpacing:{get: function(){return groupSpacing;}, set: function(_){groupSpacing=_;}}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: duration: {get: function(){return duration;}, set: function(_){ paulo@89: duration = _; paulo@89: renderWatch.reset(duration); paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: }}, paulo@89: barColor: {get: function(){return barColor;}, set: function(_){ paulo@89: barColor = _ ? nv.utils.getColor(_) : null; paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.initOptions(chart); paulo@89: paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: nv.models.multiBarHorizontalChart = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var multibar = nv.models.multiBarHorizontal() paulo@89: , xAxis = nv.models.axis() paulo@89: , yAxis = nv.models.axis() paulo@89: , legend = nv.models.legend().height(30) paulo@89: , controls = nv.models.legend().height(30) paulo@89: , tooltip = nv.models.tooltip() paulo@89: ; paulo@89: paulo@89: var margin = {top: 30, right: 20, bottom: 50, left: 60} paulo@89: , width = null paulo@89: , height = null paulo@89: , color = nv.utils.defaultColor() paulo@89: , showControls = true paulo@89: , controlLabels = {} paulo@89: , showLegend = true paulo@89: , showXAxis = true paulo@89: , showYAxis = true paulo@89: , stacked = false paulo@89: , x //can be accessed via chart.xScale() paulo@89: , y //can be accessed via chart.yScale() paulo@89: , state = nv.utils.state() paulo@89: , defaultState = null paulo@89: , noData = null paulo@89: , dispatch = d3.dispatch('stateChange', 'changeState','renderEnd') paulo@89: , controlWidth = function() { return showControls ? 180 : 0 } paulo@89: , duration = 250 paulo@89: ; paulo@89: paulo@89: state.stacked = false; // DEPRECATED Maintained for backward compatibility paulo@89: paulo@89: multibar.stacked(stacked); paulo@89: paulo@89: xAxis paulo@89: .orient('left') paulo@89: .tickPadding(5) paulo@89: .showMaxMin(false) paulo@89: .tickFormat(function(d) { return d }) paulo@89: ; paulo@89: yAxis paulo@89: .orient('bottom') paulo@89: .tickFormat(d3.format(',.1f')) paulo@89: ; paulo@89: paulo@89: tooltip paulo@89: .duration(0) paulo@89: .valueFormatter(function(d, i) { paulo@89: return yAxis.tickFormat()(d, i); paulo@89: }) paulo@89: .headerFormatter(function(d, i) { paulo@89: return xAxis.tickFormat()(d, i); paulo@89: }); paulo@89: paulo@89: controls.updateState(false); paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var stateGetter = function(data) { paulo@89: return function(){ paulo@89: return { paulo@89: active: data.map(function(d) { return !d.disabled }), paulo@89: stacked: stacked paulo@89: }; paulo@89: } paulo@89: }; paulo@89: paulo@89: var stateSetter = function(data) { paulo@89: return function(state) { paulo@89: if (state.stacked !== undefined) paulo@89: stacked = state.stacked; paulo@89: if (state.active !== undefined) paulo@89: data.forEach(function(series,i) { paulo@89: series.disabled = !state.active[i]; paulo@89: }); paulo@89: } paulo@89: }; paulo@89: paulo@89: var renderWatch = nv.utils.renderWatch(dispatch, duration); paulo@89: paulo@89: function chart(selection) { paulo@89: renderWatch.reset(); paulo@89: renderWatch.models(multibar); paulo@89: if (showXAxis) renderWatch.models(xAxis); paulo@89: if (showYAxis) renderWatch.models(yAxis); paulo@89: paulo@89: selection.each(function(data) { paulo@89: var container = d3.select(this), paulo@89: that = this; paulo@89: nv.utils.initSVG(container); paulo@89: var availableWidth = nv.utils.availableWidth(width, container, margin), paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: paulo@89: chart.update = function() { container.transition().duration(duration).call(chart) }; paulo@89: chart.container = this; paulo@89: paulo@89: stacked = multibar.stacked(); paulo@89: paulo@89: state paulo@89: .setter(stateSetter(data), chart.update) paulo@89: .getter(stateGetter(data)) paulo@89: .update(); paulo@89: paulo@89: // DEPRECATED set state.disableddisabled paulo@89: state.disabled = data.map(function(d) { return !!d.disabled }); paulo@89: paulo@89: if (!defaultState) { paulo@89: var key; paulo@89: defaultState = {}; paulo@89: for (key in state) { paulo@89: if (state[key] instanceof Array) paulo@89: defaultState[key] = state[key].slice(0); paulo@89: else paulo@89: defaultState[key] = state[key]; paulo@89: } paulo@89: } paulo@89: paulo@89: // Display No Data message if there's nothing to show. paulo@89: if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) { paulo@89: nv.utils.noData(chart, container) paulo@89: return chart; paulo@89: } else { paulo@89: container.selectAll('.nv-noData').remove(); paulo@89: } paulo@89: paulo@89: // Setup Scales paulo@89: x = multibar.xScale(); paulo@89: y = multibar.yScale(); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-multiBarHorizontalChart').data([data]); paulo@89: var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-multiBarHorizontalChart').append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-x nv-axis'); paulo@89: gEnter.append('g').attr('class', 'nv-y nv-axis') paulo@89: .append('g').attr('class', 'nv-zeroLine') paulo@89: .append('line'); paulo@89: gEnter.append('g').attr('class', 'nv-barsWrap'); paulo@89: gEnter.append('g').attr('class', 'nv-legendWrap'); paulo@89: gEnter.append('g').attr('class', 'nv-controlsWrap'); paulo@89: paulo@89: // Legend paulo@89: if (showLegend) { paulo@89: legend.width(availableWidth - controlWidth()); paulo@89: paulo@89: g.select('.nv-legendWrap') paulo@89: .datum(data) paulo@89: .call(legend); paulo@89: paulo@89: if ( margin.top != legend.height()) { paulo@89: margin.top = legend.height(); paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: } paulo@89: paulo@89: g.select('.nv-legendWrap') paulo@89: .attr('transform', 'translate(' + controlWidth() + ',' + (-margin.top) +')'); paulo@89: } paulo@89: paulo@89: // Controls paulo@89: if (showControls) { paulo@89: var controlsData = [ paulo@89: { key: controlLabels.grouped || 'Grouped', disabled: multibar.stacked() }, paulo@89: { key: controlLabels.stacked || 'Stacked', disabled: !multibar.stacked() } paulo@89: ]; paulo@89: paulo@89: controls.width(controlWidth()).color(['#444', '#444', '#444']); paulo@89: g.select('.nv-controlsWrap') paulo@89: .datum(controlsData) paulo@89: .attr('transform', 'translate(0,' + (-margin.top) +')') paulo@89: .call(controls); paulo@89: } paulo@89: paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: // Main Chart Component(s) paulo@89: multibar paulo@89: .disabled(data.map(function(series) { return series.disabled })) paulo@89: .width(availableWidth) paulo@89: .height(availableHeight) paulo@89: .color(data.map(function(d,i) { paulo@89: return d.color || color(d, i); paulo@89: }).filter(function(d,i) { return !data[i].disabled })); paulo@89: paulo@89: var barsWrap = g.select('.nv-barsWrap') paulo@89: .datum(data.filter(function(d) { return !d.disabled })); paulo@89: paulo@89: barsWrap.transition().call(multibar); paulo@89: paulo@89: // Setup Axes paulo@89: if (showXAxis) { paulo@89: xAxis paulo@89: .scale(x) paulo@89: ._ticks( nv.utils.calcTicksY(availableHeight/24, data) ) paulo@89: .tickSize(-availableWidth, 0); paulo@89: paulo@89: g.select('.nv-x.nv-axis').call(xAxis); paulo@89: paulo@89: var xTicks = g.select('.nv-x.nv-axis').selectAll('g'); paulo@89: paulo@89: xTicks paulo@89: .selectAll('line, text'); paulo@89: } paulo@89: paulo@89: if (showYAxis) { paulo@89: yAxis paulo@89: .scale(y) paulo@89: ._ticks( nv.utils.calcTicksX(availableWidth/100, data) ) paulo@89: .tickSize( -availableHeight, 0); paulo@89: paulo@89: g.select('.nv-y.nv-axis') paulo@89: .attr('transform', 'translate(0,' + availableHeight + ')'); paulo@89: g.select('.nv-y.nv-axis').call(yAxis); paulo@89: } paulo@89: paulo@89: // Zero line paulo@89: g.select(".nv-zeroLine line") paulo@89: .attr("x1", y(0)) paulo@89: .attr("x2", y(0)) paulo@89: .attr("y1", 0) paulo@89: .attr("y2", -availableHeight) paulo@89: ; paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (in chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: legend.dispatch.on('stateChange', function(newState) { paulo@89: for (var key in newState) paulo@89: state[key] = newState[key]; paulo@89: dispatch.stateChange(state); paulo@89: chart.update(); paulo@89: }); paulo@89: paulo@89: controls.dispatch.on('legendClick', function(d,i) { paulo@89: if (!d.disabled) return; paulo@89: controlsData = controlsData.map(function(s) { paulo@89: s.disabled = true; paulo@89: return s; paulo@89: }); paulo@89: d.disabled = false; paulo@89: paulo@89: switch (d.key) { paulo@89: case 'Grouped': paulo@89: multibar.stacked(false); paulo@89: break; paulo@89: case 'Stacked': paulo@89: multibar.stacked(true); paulo@89: break; paulo@89: } paulo@89: paulo@89: state.stacked = multibar.stacked(); paulo@89: dispatch.stateChange(state); paulo@89: stacked = multibar.stacked(); paulo@89: paulo@89: chart.update(); paulo@89: }); paulo@89: paulo@89: // Update chart from a state object passed to event handler paulo@89: dispatch.on('changeState', function(e) { paulo@89: paulo@89: if (typeof e.disabled !== 'undefined') { paulo@89: data.forEach(function(series,i) { paulo@89: series.disabled = e.disabled[i]; paulo@89: }); paulo@89: paulo@89: state.disabled = e.disabled; paulo@89: } paulo@89: paulo@89: if (typeof e.stacked !== 'undefined') { paulo@89: multibar.stacked(e.stacked); paulo@89: state.stacked = e.stacked; paulo@89: stacked = e.stacked; paulo@89: } paulo@89: paulo@89: chart.update(); paulo@89: }); paulo@89: }); paulo@89: renderWatch.renderEnd('multibar horizontal chart immediate'); paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (out of chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: multibar.dispatch.on('elementMouseover.tooltip', function(evt) { paulo@89: evt.value = chart.x()(evt.data); paulo@89: evt['series'] = { paulo@89: key: evt.data.key, paulo@89: value: chart.y()(evt.data), paulo@89: color: evt.color paulo@89: }; paulo@89: tooltip.data(evt).hidden(false); paulo@89: }); paulo@89: paulo@89: multibar.dispatch.on('elementMouseout.tooltip', function(evt) { paulo@89: tooltip.hidden(true); paulo@89: }); paulo@89: paulo@89: multibar.dispatch.on('elementMousemove.tooltip', function(evt) { paulo@89: tooltip.position({top: d3.event.pageY, left: d3.event.pageX})(); paulo@89: }); paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: // expose chart's sub-components paulo@89: chart.dispatch = dispatch; paulo@89: chart.multibar = multibar; paulo@89: chart.legend = legend; paulo@89: chart.controls = controls; paulo@89: chart.xAxis = xAxis; paulo@89: chart.yAxis = yAxis; paulo@89: chart.state = state; paulo@89: chart.tooltip = tooltip; paulo@89: paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}}, paulo@89: showControls: {get: function(){return showControls;}, set: function(_){showControls=_;}}, paulo@89: controlLabels: {get: function(){return controlLabels;}, set: function(_){controlLabels=_;}}, paulo@89: showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}}, paulo@89: showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}}, paulo@89: defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}}, paulo@89: noData: {get: function(){return noData;}, set: function(_){noData=_;}}, paulo@89: paulo@89: // deprecated options paulo@89: tooltips: {get: function(){return tooltip.enabled();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead'); paulo@89: tooltip.enabled(!!_); paulo@89: }}, paulo@89: tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead'); paulo@89: tooltip.contentGenerator(_); paulo@89: }}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: duration: {get: function(){return duration;}, set: function(_){ paulo@89: duration = _; paulo@89: renderWatch.reset(duration); paulo@89: multibar.duration(duration); paulo@89: xAxis.duration(duration); paulo@89: yAxis.duration(duration); paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: legend.color(color); paulo@89: }}, paulo@89: barColor: {get: function(){return multibar.barColor;}, set: function(_){ paulo@89: multibar.barColor(_); paulo@89: legend.color(function(d,i) {return d3.rgb('#ccc').darker(i * 1.5).toString();}) paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.inheritOptions(chart, multibar); paulo@89: nv.utils.initOptions(chart); paulo@89: paulo@89: return chart; paulo@89: }; paulo@89: nv.models.multiChart = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var margin = {top: 30, right: 20, bottom: 50, left: 60}, paulo@89: color = nv.utils.defaultColor(), paulo@89: width = null, paulo@89: height = null, paulo@89: showLegend = true, paulo@89: noData = null, paulo@89: yDomain1, paulo@89: yDomain2, paulo@89: getX = function(d) { return d.x }, paulo@89: getY = function(d) { return d.y}, paulo@89: interpolate = 'monotone', paulo@89: useVoronoi = true paulo@89: ; paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var x = d3.scale.linear(), paulo@89: yScale1 = d3.scale.linear(), paulo@89: yScale2 = d3.scale.linear(), paulo@89: paulo@89: lines1 = nv.models.line().yScale(yScale1), paulo@89: lines2 = nv.models.line().yScale(yScale2), paulo@89: paulo@89: bars1 = nv.models.multiBar().stacked(false).yScale(yScale1), paulo@89: bars2 = nv.models.multiBar().stacked(false).yScale(yScale2), paulo@89: paulo@89: stack1 = nv.models.stackedArea().yScale(yScale1), paulo@89: stack2 = nv.models.stackedArea().yScale(yScale2), paulo@89: paulo@89: xAxis = nv.models.axis().scale(x).orient('bottom').tickPadding(5), paulo@89: yAxis1 = nv.models.axis().scale(yScale1).orient('left'), paulo@89: yAxis2 = nv.models.axis().scale(yScale2).orient('right'), paulo@89: paulo@89: legend = nv.models.legend().height(30), paulo@89: tooltip = nv.models.tooltip(), paulo@89: dispatch = d3.dispatch(); paulo@89: paulo@89: function chart(selection) { paulo@89: selection.each(function(data) { paulo@89: var container = d3.select(this), paulo@89: that = this; paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: chart.update = function() { container.transition().call(chart); }; paulo@89: chart.container = this; paulo@89: paulo@89: var availableWidth = nv.utils.availableWidth(width, container, margin), paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: paulo@89: var dataLines1 = data.filter(function(d) {return d.type == 'line' && d.yAxis == 1}); paulo@89: var dataLines2 = data.filter(function(d) {return d.type == 'line' && d.yAxis == 2}); paulo@89: var dataBars1 = data.filter(function(d) {return d.type == 'bar' && d.yAxis == 1}); paulo@89: var dataBars2 = data.filter(function(d) {return d.type == 'bar' && d.yAxis == 2}); paulo@89: var dataStack1 = data.filter(function(d) {return d.type == 'area' && d.yAxis == 1}); paulo@89: var dataStack2 = data.filter(function(d) {return d.type == 'area' && d.yAxis == 2}); paulo@89: paulo@89: // Display noData message if there's nothing to show. paulo@89: if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) { paulo@89: nv.utils.noData(chart, container); paulo@89: return chart; paulo@89: } else { paulo@89: container.selectAll('.nv-noData').remove(); paulo@89: } paulo@89: paulo@89: var series1 = data.filter(function(d) {return !d.disabled && d.yAxis == 1}) paulo@89: .map(function(d) { paulo@89: return d.values.map(function(d,i) { paulo@89: return { x: d.x, y: d.y } paulo@89: }) paulo@89: }); paulo@89: paulo@89: var series2 = data.filter(function(d) {return !d.disabled && d.yAxis == 2}) paulo@89: .map(function(d) { paulo@89: return d.values.map(function(d,i) { paulo@89: return { x: d.x, y: d.y } paulo@89: }) paulo@89: }); paulo@89: paulo@89: x .domain(d3.extent(d3.merge(series1.concat(series2)), function(d) { return d.x } )) paulo@89: .range([0, availableWidth]); paulo@89: paulo@89: var wrap = container.selectAll('g.wrap.multiChart').data([data]); paulo@89: var gEnter = wrap.enter().append('g').attr('class', 'wrap nvd3 multiChart').append('g'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-x nv-axis'); paulo@89: gEnter.append('g').attr('class', 'nv-y1 nv-axis'); paulo@89: gEnter.append('g').attr('class', 'nv-y2 nv-axis'); paulo@89: gEnter.append('g').attr('class', 'lines1Wrap'); paulo@89: gEnter.append('g').attr('class', 'lines2Wrap'); paulo@89: gEnter.append('g').attr('class', 'bars1Wrap'); paulo@89: gEnter.append('g').attr('class', 'bars2Wrap'); paulo@89: gEnter.append('g').attr('class', 'stack1Wrap'); paulo@89: gEnter.append('g').attr('class', 'stack2Wrap'); paulo@89: gEnter.append('g').attr('class', 'legendWrap'); paulo@89: paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: var color_array = data.map(function(d,i) { paulo@89: return data[i].color || color(d, i); paulo@89: }); paulo@89: paulo@89: if (showLegend) { paulo@89: var legendWidth = legend.align() ? availableWidth / 2 : availableWidth; paulo@89: var legendXPosition = legend.align() ? legendWidth : 0; paulo@89: paulo@89: legend.width(legendWidth); paulo@89: legend.color(color_array); paulo@89: paulo@89: g.select('.legendWrap') paulo@89: .datum(data.map(function(series) { paulo@89: series.originalKey = series.originalKey === undefined ? series.key : series.originalKey; paulo@89: series.key = series.originalKey + (series.yAxis == 1 ? '' : ' (right axis)'); paulo@89: return series; paulo@89: })) paulo@89: .call(legend); paulo@89: paulo@89: if ( margin.top != legend.height()) { paulo@89: margin.top = legend.height(); paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: } paulo@89: paulo@89: g.select('.legendWrap') paulo@89: .attr('transform', 'translate(' + legendXPosition + ',' + (-margin.top) +')'); paulo@89: } paulo@89: paulo@89: lines1 paulo@89: .width(availableWidth) paulo@89: .height(availableHeight) paulo@89: .interpolate(interpolate) paulo@89: .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 1 && data[i].type == 'line'})); paulo@89: lines2 paulo@89: .width(availableWidth) paulo@89: .height(availableHeight) paulo@89: .interpolate(interpolate) paulo@89: .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 2 && data[i].type == 'line'})); paulo@89: bars1 paulo@89: .width(availableWidth) paulo@89: .height(availableHeight) paulo@89: .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 1 && data[i].type == 'bar'})); paulo@89: bars2 paulo@89: .width(availableWidth) paulo@89: .height(availableHeight) paulo@89: .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 2 && data[i].type == 'bar'})); paulo@89: stack1 paulo@89: .width(availableWidth) paulo@89: .height(availableHeight) paulo@89: .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 1 && data[i].type == 'area'})); paulo@89: stack2 paulo@89: .width(availableWidth) paulo@89: .height(availableHeight) paulo@89: .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 2 && data[i].type == 'area'})); paulo@89: paulo@89: g.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: var lines1Wrap = g.select('.lines1Wrap') paulo@89: .datum(dataLines1.filter(function(d){return !d.disabled})); paulo@89: var bars1Wrap = g.select('.bars1Wrap') paulo@89: .datum(dataBars1.filter(function(d){return !d.disabled})); paulo@89: var stack1Wrap = g.select('.stack1Wrap') paulo@89: .datum(dataStack1.filter(function(d){return !d.disabled})); paulo@89: var lines2Wrap = g.select('.lines2Wrap') paulo@89: .datum(dataLines2.filter(function(d){return !d.disabled})); paulo@89: var bars2Wrap = g.select('.bars2Wrap') paulo@89: .datum(dataBars2.filter(function(d){return !d.disabled})); paulo@89: var stack2Wrap = g.select('.stack2Wrap') paulo@89: .datum(dataStack2.filter(function(d){return !d.disabled})); paulo@89: paulo@89: var extraValue1 = dataStack1.length ? dataStack1.map(function(a){return a.values}).reduce(function(a,b){ paulo@89: return a.map(function(aVal,i){return {x: aVal.x, y: aVal.y + b[i].y}}) paulo@89: }).concat([{x:0, y:0}]) : []; paulo@89: var extraValue2 = dataStack2.length ? dataStack2.map(function(a){return a.values}).reduce(function(a,b){ paulo@89: return a.map(function(aVal,i){return {x: aVal.x, y: aVal.y + b[i].y}}) paulo@89: }).concat([{x:0, y:0}]) : []; paulo@89: paulo@89: yScale1 .domain(yDomain1 || d3.extent(d3.merge(series1).concat(extraValue1), function(d) { return d.y } )) paulo@89: .range([0, availableHeight]); paulo@89: paulo@89: yScale2 .domain(yDomain2 || d3.extent(d3.merge(series2).concat(extraValue2), function(d) { return d.y } )) paulo@89: .range([0, availableHeight]); paulo@89: paulo@89: lines1.yDomain(yScale1.domain()); paulo@89: bars1.yDomain(yScale1.domain()); paulo@89: stack1.yDomain(yScale1.domain()); paulo@89: paulo@89: lines2.yDomain(yScale2.domain()); paulo@89: bars2.yDomain(yScale2.domain()); paulo@89: stack2.yDomain(yScale2.domain()); paulo@89: paulo@89: if(dataStack1.length){d3.transition(stack1Wrap).call(stack1);} paulo@89: if(dataStack2.length){d3.transition(stack2Wrap).call(stack2);} paulo@89: paulo@89: if(dataBars1.length){d3.transition(bars1Wrap).call(bars1);} paulo@89: if(dataBars2.length){d3.transition(bars2Wrap).call(bars2);} paulo@89: paulo@89: if(dataLines1.length){d3.transition(lines1Wrap).call(lines1);} paulo@89: if(dataLines2.length){d3.transition(lines2Wrap).call(lines2);} paulo@89: paulo@89: xAxis paulo@89: ._ticks( nv.utils.calcTicksX(availableWidth/100, data) ) paulo@89: .tickSize(-availableHeight, 0); paulo@89: paulo@89: g.select('.nv-x.nv-axis') paulo@89: .attr('transform', 'translate(0,' + availableHeight + ')'); paulo@89: d3.transition(g.select('.nv-x.nv-axis')) paulo@89: .call(xAxis); paulo@89: paulo@89: yAxis1 paulo@89: ._ticks( nv.utils.calcTicksY(availableHeight/36, data) ) paulo@89: .tickSize( -availableWidth, 0); paulo@89: paulo@89: paulo@89: d3.transition(g.select('.nv-y1.nv-axis')) paulo@89: .call(yAxis1); paulo@89: paulo@89: yAxis2 paulo@89: ._ticks( nv.utils.calcTicksY(availableHeight/36, data) ) paulo@89: .tickSize( -availableWidth, 0); paulo@89: paulo@89: d3.transition(g.select('.nv-y2.nv-axis')) paulo@89: .call(yAxis2); paulo@89: paulo@89: g.select('.nv-y1.nv-axis') paulo@89: .classed('nv-disabled', series1.length ? false : true) paulo@89: .attr('transform', 'translate(' + x.range()[0] + ',0)'); paulo@89: paulo@89: g.select('.nv-y2.nv-axis') paulo@89: .classed('nv-disabled', series2.length ? false : true) paulo@89: .attr('transform', 'translate(' + x.range()[1] + ',0)'); paulo@89: paulo@89: legend.dispatch.on('stateChange', function(newState) { paulo@89: chart.update(); paulo@89: }); paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: function mouseover_line(evt) { paulo@89: var yaxis = data[evt.seriesIndex].yAxis === 2 ? yAxis2 : yAxis1; paulo@89: evt.value = evt.point.x; paulo@89: evt.series = { paulo@89: value: evt.point.y, paulo@89: color: evt.point.color paulo@89: }; paulo@89: tooltip paulo@89: .duration(100) paulo@89: .valueFormatter(function(d, i) { paulo@89: return yaxis.tickFormat()(d, i); paulo@89: }) paulo@89: .data(evt) paulo@89: .position(evt.pos) paulo@89: .hidden(false); paulo@89: } paulo@89: paulo@89: function mouseover_stack(evt) { paulo@89: var yaxis = data[evt.seriesIndex].yAxis === 2 ? yAxis2 : yAxis1; paulo@89: evt.point['x'] = stack1.x()(evt.point); paulo@89: evt.point['y'] = stack1.y()(evt.point); paulo@89: tooltip paulo@89: .duration(100) paulo@89: .valueFormatter(function(d, i) { paulo@89: return yaxis.tickFormat()(d, i); paulo@89: }) paulo@89: .data(evt) paulo@89: .position(evt.pos) paulo@89: .hidden(false); paulo@89: } paulo@89: paulo@89: function mouseover_bar(evt) { paulo@89: var yaxis = data[evt.data.series].yAxis === 2 ? yAxis2 : yAxis1; paulo@89: paulo@89: evt.value = bars1.x()(evt.data); paulo@89: evt['series'] = { paulo@89: value: bars1.y()(evt.data), paulo@89: color: evt.color paulo@89: }; paulo@89: tooltip paulo@89: .duration(0) paulo@89: .valueFormatter(function(d, i) { paulo@89: return yaxis.tickFormat()(d, i); paulo@89: }) paulo@89: .data(evt) paulo@89: .hidden(false); paulo@89: } paulo@89: paulo@89: lines1.dispatch.on('elementMouseover.tooltip', mouseover_line); paulo@89: lines2.dispatch.on('elementMouseover.tooltip', mouseover_line); paulo@89: lines1.dispatch.on('elementMouseout.tooltip', function(evt) { paulo@89: tooltip.hidden(true) paulo@89: }); paulo@89: lines2.dispatch.on('elementMouseout.tooltip', function(evt) { paulo@89: tooltip.hidden(true) paulo@89: }); paulo@89: paulo@89: stack1.dispatch.on('elementMouseover.tooltip', mouseover_stack); paulo@89: stack2.dispatch.on('elementMouseover.tooltip', mouseover_stack); paulo@89: stack1.dispatch.on('elementMouseout.tooltip', function(evt) { paulo@89: tooltip.hidden(true) paulo@89: }); paulo@89: stack2.dispatch.on('elementMouseout.tooltip', function(evt) { paulo@89: tooltip.hidden(true) paulo@89: }); paulo@89: paulo@89: bars1.dispatch.on('elementMouseover.tooltip', mouseover_bar); paulo@89: bars2.dispatch.on('elementMouseover.tooltip', mouseover_bar); paulo@89: paulo@89: bars1.dispatch.on('elementMouseout.tooltip', function(evt) { paulo@89: tooltip.hidden(true); paulo@89: }); paulo@89: bars2.dispatch.on('elementMouseout.tooltip', function(evt) { paulo@89: tooltip.hidden(true); paulo@89: }); paulo@89: bars1.dispatch.on('elementMousemove.tooltip', function(evt) { paulo@89: tooltip.position({top: d3.event.pageY, left: d3.event.pageX})(); paulo@89: }); paulo@89: bars2.dispatch.on('elementMousemove.tooltip', function(evt) { paulo@89: tooltip.position({top: d3.event.pageY, left: d3.event.pageX})(); paulo@89: }); paulo@89: paulo@89: }); paulo@89: paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Global getters and setters paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: chart.dispatch = dispatch; paulo@89: chart.lines1 = lines1; paulo@89: chart.lines2 = lines2; paulo@89: chart.bars1 = bars1; paulo@89: chart.bars2 = bars2; paulo@89: chart.stack1 = stack1; paulo@89: chart.stack2 = stack2; paulo@89: chart.xAxis = xAxis; paulo@89: chart.yAxis1 = yAxis1; paulo@89: chart.yAxis2 = yAxis2; paulo@89: chart.tooltip = tooltip; paulo@89: paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}}, paulo@89: yDomain1: {get: function(){return yDomain1;}, set: function(_){yDomain1=_;}}, paulo@89: yDomain2: {get: function(){return yDomain2;}, set: function(_){yDomain2=_;}}, paulo@89: noData: {get: function(){return noData;}, set: function(_){noData=_;}}, paulo@89: interpolate: {get: function(){return interpolate;}, set: function(_){interpolate=_;}}, paulo@89: paulo@89: // deprecated options paulo@89: tooltips: {get: function(){return tooltip.enabled();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead'); paulo@89: tooltip.enabled(!!_); paulo@89: }}, paulo@89: tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead'); paulo@89: tooltip.contentGenerator(_); paulo@89: }}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: }}, paulo@89: x: {get: function(){return getX;}, set: function(_){ paulo@89: getX = _; paulo@89: lines1.x(_); paulo@89: lines2.x(_); paulo@89: bars1.x(_); paulo@89: bars2.x(_); paulo@89: stack1.x(_); paulo@89: stack2.x(_); paulo@89: }}, paulo@89: y: {get: function(){return getY;}, set: function(_){ paulo@89: getY = _; paulo@89: lines1.y(_); paulo@89: lines2.y(_); paulo@89: stack1.y(_); paulo@89: stack2.y(_); paulo@89: bars1.y(_); paulo@89: bars2.y(_); paulo@89: }}, paulo@89: useVoronoi: {get: function(){return useVoronoi;}, set: function(_){ paulo@89: useVoronoi=_; paulo@89: lines1.useVoronoi(_); paulo@89: lines2.useVoronoi(_); paulo@89: stack1.useVoronoi(_); paulo@89: stack2.useVoronoi(_); paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.initOptions(chart); paulo@89: paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: paulo@89: nv.models.ohlcBar = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var margin = {top: 0, right: 0, bottom: 0, left: 0} paulo@89: , width = null paulo@89: , height = null paulo@89: , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one paulo@89: , container = null paulo@89: , x = d3.scale.linear() paulo@89: , y = d3.scale.linear() paulo@89: , getX = function(d) { return d.x } paulo@89: , getY = function(d) { return d.y } paulo@89: , getOpen = function(d) { return d.open } paulo@89: , getClose = function(d) { return d.close } paulo@89: , getHigh = function(d) { return d.high } paulo@89: , getLow = function(d) { return d.low } paulo@89: , forceX = [] paulo@89: , forceY = [] paulo@89: , padData = false // If true, adds half a data points width to front and back, for lining up a line chart with a bar chart paulo@89: , clipEdge = true paulo@89: , color = nv.utils.defaultColor() paulo@89: , interactive = false paulo@89: , xDomain paulo@89: , yDomain paulo@89: , xRange paulo@89: , yRange paulo@89: , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'stateChange', 'changeState', 'renderEnd', 'chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'elementMousemove') paulo@89: ; paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: function chart(selection) { paulo@89: selection.each(function(data) { paulo@89: container = d3.select(this); paulo@89: var availableWidth = nv.utils.availableWidth(width, container, margin), paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: // ohlc bar width. paulo@89: var w = (availableWidth / data[0].values.length) * .9; paulo@89: paulo@89: // Setup Scales paulo@89: x.domain(xDomain || d3.extent(data[0].values.map(getX).concat(forceX) )); paulo@89: paulo@89: if (padData) paulo@89: x.range(xRange || [availableWidth * .5 / data[0].values.length, availableWidth * (data[0].values.length - .5) / data[0].values.length ]); paulo@89: else paulo@89: x.range(xRange || [5 + w/2, availableWidth - w/2 - 5]); paulo@89: paulo@89: y.domain(yDomain || [ paulo@89: d3.min(data[0].values.map(getLow).concat(forceY)), paulo@89: d3.max(data[0].values.map(getHigh).concat(forceY)) paulo@89: ] paulo@89: ).range(yRange || [availableHeight, 0]); paulo@89: paulo@89: // If scale's domain don't have a range, slightly adjust to make one... so a chart can show a single data point paulo@89: if (x.domain()[0] === x.domain()[1]) paulo@89: x.domain()[0] ? paulo@89: x.domain([x.domain()[0] - x.domain()[0] * 0.01, x.domain()[1] + x.domain()[1] * 0.01]) paulo@89: : x.domain([-1,1]); paulo@89: paulo@89: if (y.domain()[0] === y.domain()[1]) paulo@89: y.domain()[0] ? paulo@89: y.domain([y.domain()[0] + y.domain()[0] * 0.01, y.domain()[1] - y.domain()[1] * 0.01]) paulo@89: : y.domain([-1,1]); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = d3.select(this).selectAll('g.nv-wrap.nv-ohlcBar').data([data[0].values]); paulo@89: var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-ohlcBar'); paulo@89: var defsEnter = wrapEnter.append('defs'); paulo@89: var gEnter = wrapEnter.append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-ticks'); paulo@89: paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: container paulo@89: .on('click', function(d,i) { paulo@89: dispatch.chartClick({ paulo@89: data: d, paulo@89: index: i, paulo@89: pos: d3.event, paulo@89: id: id paulo@89: }); paulo@89: }); paulo@89: paulo@89: defsEnter.append('clipPath') paulo@89: .attr('id', 'nv-chart-clip-path-' + id) paulo@89: .append('rect'); paulo@89: paulo@89: wrap.select('#nv-chart-clip-path-' + id + ' rect') paulo@89: .attr('width', availableWidth) paulo@89: .attr('height', availableHeight); paulo@89: paulo@89: g .attr('clip-path', clipEdge ? 'url(#nv-chart-clip-path-' + id + ')' : ''); paulo@89: paulo@89: var ticks = wrap.select('.nv-ticks').selectAll('.nv-tick') paulo@89: .data(function(d) { return d }); paulo@89: ticks.exit().remove(); paulo@89: paulo@89: ticks.enter().append('path') paulo@89: .attr('class', function(d,i,j) { return (getOpen(d,i) > getClose(d,i) ? 'nv-tick negative' : 'nv-tick positive') + ' nv-tick-' + j + '-' + i }) paulo@89: .attr('d', function(d,i) { paulo@89: return 'm0,0l0,' paulo@89: + (y(getOpen(d,i)) paulo@89: - y(getHigh(d,i))) paulo@89: + 'l' paulo@89: + (-w/2) paulo@89: + ',0l' paulo@89: + (w/2) paulo@89: + ',0l0,' paulo@89: + (y(getLow(d,i)) - y(getOpen(d,i))) paulo@89: + 'l0,' paulo@89: + (y(getClose(d,i)) paulo@89: - y(getLow(d,i))) paulo@89: + 'l' paulo@89: + (w/2) paulo@89: + ',0l' paulo@89: + (-w/2) paulo@89: + ',0z'; paulo@89: }) paulo@89: .attr('transform', function(d,i) { return 'translate(' + x(getX(d,i)) + ',' + y(getHigh(d,i)) + ')'; }) paulo@89: .attr('fill', function(d,i) { return color[0]; }) paulo@89: .attr('stroke', function(d,i) { return color[0]; }) paulo@89: .attr('x', 0 ) paulo@89: .attr('y', function(d,i) { return y(Math.max(0, getY(d,i))) }) paulo@89: .attr('height', function(d,i) { return Math.abs(y(getY(d,i)) - y(0)) }); paulo@89: paulo@89: // the bar colors are controlled by CSS currently paulo@89: ticks.attr('class', function(d,i,j) { paulo@89: return (getOpen(d,i) > getClose(d,i) ? 'nv-tick negative' : 'nv-tick positive') + ' nv-tick-' + j + '-' + i; paulo@89: }); paulo@89: paulo@89: d3.transition(ticks) paulo@89: .attr('transform', function(d,i) { return 'translate(' + x(getX(d,i)) + ',' + y(getHigh(d,i)) + ')'; }) paulo@89: .attr('d', function(d,i) { paulo@89: var w = (availableWidth / data[0].values.length) * .9; paulo@89: return 'm0,0l0,' paulo@89: + (y(getOpen(d,i)) paulo@89: - y(getHigh(d,i))) paulo@89: + 'l' paulo@89: + (-w/2) paulo@89: + ',0l' paulo@89: + (w/2) paulo@89: + ',0l0,' paulo@89: + (y(getLow(d,i)) paulo@89: - y(getOpen(d,i))) paulo@89: + 'l0,' paulo@89: + (y(getClose(d,i)) paulo@89: - y(getLow(d,i))) paulo@89: + 'l' paulo@89: + (w/2) paulo@89: + ',0l' paulo@89: + (-w/2) paulo@89: + ',0z'; paulo@89: }); paulo@89: }); paulo@89: paulo@89: return chart; paulo@89: } paulo@89: paulo@89: paulo@89: //Create methods to allow outside functions to highlight a specific bar. paulo@89: chart.highlightPoint = function(pointIndex, isHoverOver) { paulo@89: chart.clearHighlights(); paulo@89: container.select(".nv-ohlcBar .nv-tick-0-" + pointIndex) paulo@89: .classed("hover", isHoverOver) paulo@89: ; paulo@89: }; paulo@89: paulo@89: chart.clearHighlights = function() { paulo@89: container.select(".nv-ohlcBar .nv-tick.hover") paulo@89: .classed("hover", false) paulo@89: ; paulo@89: }; paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: chart.dispatch = dispatch; paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: xScale: {get: function(){return x;}, set: function(_){x=_;}}, paulo@89: yScale: {get: function(){return y;}, set: function(_){y=_;}}, paulo@89: xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}}, paulo@89: yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}}, paulo@89: xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}}, paulo@89: yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}}, paulo@89: forceX: {get: function(){return forceX;}, set: function(_){forceX=_;}}, paulo@89: forceY: {get: function(){return forceY;}, set: function(_){forceY=_;}}, paulo@89: padData: {get: function(){return padData;}, set: function(_){padData=_;}}, paulo@89: clipEdge: {get: function(){return clipEdge;}, set: function(_){clipEdge=_;}}, paulo@89: id: {get: function(){return id;}, set: function(_){id=_;}}, paulo@89: interactive: {get: function(){return interactive;}, set: function(_){interactive=_;}}, paulo@89: paulo@89: x: {get: function(){return getX;}, set: function(_){getX=_;}}, paulo@89: y: {get: function(){return getY;}, set: function(_){getY=_;}}, paulo@89: open: {get: function(){return getOpen();}, set: function(_){getOpen=_;}}, paulo@89: close: {get: function(){return getClose();}, set: function(_){getClose=_;}}, paulo@89: high: {get: function(){return getHigh;}, set: function(_){getHigh=_;}}, paulo@89: low: {get: function(){return getLow;}, set: function(_){getLow=_;}}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top != undefined ? _.top : margin.top; paulo@89: margin.right = _.right != undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom != undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left != undefined ? _.left : margin.left; paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.initOptions(chart); paulo@89: return chart; paulo@89: }; paulo@89: // Code adapted from Jason Davies' "Parallel Coordinates" paulo@89: // http://bl.ocks.org/jasondavies/1341281 paulo@89: nv.models.parallelCoordinates = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var margin = {top: 30, right: 0, bottom: 10, left: 0} paulo@89: , width = null paulo@89: , height = null paulo@89: , x = d3.scale.ordinal() paulo@89: , y = {} paulo@89: , dimensionNames = [] paulo@89: , dimensionFormats = [] paulo@89: , color = nv.utils.defaultColor() paulo@89: , filters = [] paulo@89: , active = [] paulo@89: , dragging = [] paulo@89: , lineTension = 1 paulo@89: , dispatch = d3.dispatch('brush', 'elementMouseover', 'elementMouseout') paulo@89: ; paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: function chart(selection) { paulo@89: selection.each(function(data) { paulo@89: var container = d3.select(this); paulo@89: var availableWidth = nv.utils.availableWidth(width, container, margin), paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: active = data; //set all active before first brush call paulo@89: paulo@89: // Setup Scales paulo@89: x.rangePoints([0, availableWidth], 1).domain(dimensionNames); paulo@89: paulo@89: //Set as true if all values on an axis are missing. paulo@89: var onlyNanValues = {}; paulo@89: // Extract the list of dimensions and create a scale for each. paulo@89: dimensionNames.forEach(function(d) { paulo@89: var extent = d3.extent(data, function(p) { return +p[d]; }); paulo@89: onlyNanValues[d] = false; paulo@89: //If there is no values to display on an axis, set the extent to 0 paulo@89: if (extent[0] === undefined) { paulo@89: onlyNanValues[d] = true; paulo@89: extent[0] = 0; paulo@89: extent[1] = 0; paulo@89: } paulo@89: //Scale axis if there is only one value paulo@89: if (extent[0] === extent[1]) { paulo@89: extent[0] = extent[0] - 1; paulo@89: extent[1] = extent[1] + 1; paulo@89: } paulo@89: //Use 90% of (availableHeight - 12) for the axis range, 12 reprensenting the space necessary to display "undefined values" text. paulo@89: //The remaining 10% are used to display the missingValue line. paulo@89: y[d] = d3.scale.linear() paulo@89: .domain(extent) paulo@89: .range([(availableHeight - 12) * 0.9, 0]); paulo@89: paulo@89: y[d].brush = d3.svg.brush().y(y[d]).on('brush', brush); paulo@89: paulo@89: return d != 'name'; paulo@89: }); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-parallelCoordinates').data([data]); paulo@89: var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-parallelCoordinates'); paulo@89: var gEnter = wrapEnter.append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-parallelCoordinates background'); paulo@89: gEnter.append('g').attr('class', 'nv-parallelCoordinates foreground'); paulo@89: gEnter.append('g').attr('class', 'nv-parallelCoordinates missingValuesline'); paulo@89: paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: var line = d3.svg.line().interpolate('cardinal').tension(lineTension), paulo@89: axis = d3.svg.axis().orient('left'), paulo@89: axisDrag = d3.behavior.drag() paulo@89: .on('dragstart', dragStart) paulo@89: .on('drag', dragMove) paulo@89: .on('dragend', dragEnd); paulo@89: paulo@89: //Add missing value line at the bottom of the chart paulo@89: var missingValuesline, missingValueslineText; paulo@89: var step = x.range()[1] - x.range()[0]; paulo@89: var axisWithMissingValues = []; paulo@89: var lineData = [0 + step / 2, availableHeight - 12, availableWidth - step / 2, availableHeight - 12]; paulo@89: missingValuesline = wrap.select('.missingValuesline').selectAll('line').data([lineData]); paulo@89: missingValuesline.enter().append('line'); paulo@89: missingValuesline.exit().remove(); paulo@89: missingValuesline.attr("x1", function(d) { return d[0]; }) paulo@89: .attr("y1", function(d) { return d[1]; }) paulo@89: .attr("x2", function(d) { return d[2]; }) paulo@89: .attr("y2", function(d) { return d[3]; }); paulo@89: paulo@89: //Add the text "undefined values" under the missing value line paulo@89: missingValueslineText = wrap.select('.missingValuesline').selectAll('text').data(["undefined values"]); paulo@89: missingValueslineText.append('text').data(["undefined values"]); paulo@89: missingValueslineText.enter().append('text'); paulo@89: missingValueslineText.exit().remove(); paulo@89: missingValueslineText.attr("y", availableHeight) paulo@89: //To have the text right align with the missingValues line, substract 92 representing the text size. paulo@89: .attr("x", availableWidth - 92 - step / 2) paulo@89: .text(function(d) { return d; }); paulo@89: paulo@89: // Add grey background lines for context. paulo@89: var background = wrap.select('.background').selectAll('path').data(data); paulo@89: background.enter().append('path'); paulo@89: background.exit().remove(); paulo@89: background.attr('d', path); paulo@89: paulo@89: // Add blue foreground lines for focus. paulo@89: var foreground = wrap.select('.foreground').selectAll('path').data(data); paulo@89: foreground.enter().append('path') paulo@89: foreground.exit().remove(); paulo@89: foreground.attr('d', path).attr('stroke', color); paulo@89: foreground.on("mouseover", function (d, i) { paulo@89: d3.select(this).classed('hover', true); paulo@89: dispatch.elementMouseover({ paulo@89: label: d.name, paulo@89: data: d.data, paulo@89: index: i, paulo@89: pos: [d3.mouse(this.parentNode)[0], d3.mouse(this.parentNode)[1]] paulo@89: }); paulo@89: paulo@89: }); paulo@89: foreground.on("mouseout", function (d, i) { paulo@89: d3.select(this).classed('hover', false); paulo@89: dispatch.elementMouseout({ paulo@89: label: d.name, paulo@89: data: d.data, paulo@89: index: i paulo@89: }); paulo@89: }); paulo@89: paulo@89: // Add a group element for each dimension. paulo@89: var dimensions = g.selectAll('.dimension').data(dimensionNames); paulo@89: var dimensionsEnter = dimensions.enter().append('g').attr('class', 'nv-parallelCoordinates dimension'); paulo@89: dimensionsEnter.append('g').attr('class', 'nv-parallelCoordinates nv-axis'); paulo@89: dimensionsEnter.append('g').attr('class', 'nv-parallelCoordinates-brush'); paulo@89: dimensionsEnter.append('text').attr('class', 'nv-parallelCoordinates nv-label'); paulo@89: paulo@89: dimensions.attr('transform', function(d) { return 'translate(' + x(d) + ',0)'; }); paulo@89: dimensions.exit().remove(); paulo@89: paulo@89: // Add an axis and title. paulo@89: dimensions.select('.nv-label') paulo@89: .style("cursor", "move") paulo@89: .attr('dy', '-1em') paulo@89: .attr('text-anchor', 'middle') paulo@89: .text(String) paulo@89: .on("mouseover", function(d, i) { paulo@89: dispatch.elementMouseover({ paulo@89: dim: d, paulo@89: pos: [d3.mouse(this.parentNode.parentNode)[0], d3.mouse(this.parentNode.parentNode)[1]] paulo@89: }); paulo@89: }) paulo@89: .on("mouseout", function(d, i) { paulo@89: dispatch.elementMouseout({ paulo@89: dim: d paulo@89: }); paulo@89: }) paulo@89: .call(axisDrag); paulo@89: paulo@89: dimensions.select('.nv-axis') paulo@89: .each(function (d, i) { paulo@89: d3.select(this).call(axis.scale(y[d]).tickFormat(d3.format(dimensionFormats[i]))); paulo@89: }); paulo@89: paulo@89: dimensions.select('.nv-parallelCoordinates-brush') paulo@89: .each(function (d) { paulo@89: d3.select(this).call(y[d].brush); paulo@89: }) paulo@89: .selectAll('rect') paulo@89: .attr('x', -8) paulo@89: .attr('width', 16); paulo@89: paulo@89: // Returns the path for a given data point. paulo@89: function path(d) { paulo@89: return line(dimensionNames.map(function (p) { paulo@89: //If value if missing, put the value on the missing value line paulo@89: if(isNaN(d[p]) || isNaN(parseFloat(d[p]))) { paulo@89: var domain = y[p].domain(); paulo@89: var range = y[p].range(); paulo@89: var min = domain[0] - (domain[1] - domain[0]) / 9; paulo@89: paulo@89: //If it's not already the case, allow brush to select undefined values paulo@89: if(axisWithMissingValues.indexOf(p) < 0) { paulo@89: paulo@89: var newscale = d3.scale.linear().domain([min, domain[1]]).range([availableHeight - 12, range[1]]); paulo@89: y[p].brush.y(newscale); paulo@89: axisWithMissingValues.push(p); paulo@89: } paulo@89: paulo@89: return [x(p), y[p](min)]; paulo@89: } paulo@89: paulo@89: //If parallelCoordinate contain missing values show the missing values line otherwise, hide it. paulo@89: if(axisWithMissingValues.length > 0) { paulo@89: missingValuesline.style("display", "inline"); paulo@89: missingValueslineText.style("display", "inline"); paulo@89: } else { paulo@89: missingValuesline.style("display", "none"); paulo@89: missingValueslineText.style("display", "none"); paulo@89: } paulo@89: paulo@89: return [x(p), y[p](d[p])]; paulo@89: })); paulo@89: } paulo@89: paulo@89: // Handles a brush event, toggling the display of foreground lines. paulo@89: function brush() { paulo@89: var actives = dimensionNames.filter(function(p) { return !y[p].brush.empty(); }), paulo@89: extents = actives.map(function(p) { return y[p].brush.extent(); }); paulo@89: paulo@89: filters = []; //erase current filters paulo@89: actives.forEach(function(d,i) { paulo@89: filters[i] = { paulo@89: dimension: d, paulo@89: extent: extents[i] paulo@89: } paulo@89: }); paulo@89: paulo@89: active = []; //erase current active list paulo@89: foreground.style('display', function(d) { paulo@89: var isActive = actives.every(function(p, i) { paulo@89: if(isNaN(d[p]) && extents[i][0] == y[p].brush.y().domain()[0]) return true; paulo@89: return extents[i][0] <= d[p] && d[p] <= extents[i][1]; paulo@89: }); paulo@89: if (isActive) active.push(d); paulo@89: return isActive ? null : 'none'; paulo@89: }); paulo@89: paulo@89: dispatch.brush({ paulo@89: filters: filters, paulo@89: active: active paulo@89: }); paulo@89: } paulo@89: paulo@89: function dragStart(d, i) { paulo@89: dragging[d] = this.parentNode.__origin__ = x(d); paulo@89: background.attr("visibility", "hidden"); paulo@89: paulo@89: } paulo@89: paulo@89: function dragMove(d, i) { paulo@89: dragging[d] = Math.min(availableWidth, Math.max(0, this.parentNode.__origin__ += d3.event.x)); paulo@89: foreground.attr("d", path); paulo@89: dimensionNames.sort(function (a, b) { return position(a) - position(b); }); paulo@89: x.domain(dimensionNames); paulo@89: dimensions.attr("transform", function(d) { return "translate(" + position(d) + ")"; }); paulo@89: } paulo@89: paulo@89: function dragEnd(d, i) { paulo@89: delete this.parentNode.__origin__; paulo@89: delete dragging[d]; paulo@89: d3.select(this.parentNode).attr("transform", "translate(" + x(d) + ")"); paulo@89: foreground paulo@89: .attr("d", path); paulo@89: background paulo@89: .attr("d", path) paulo@89: .attr("visibility", null); paulo@89: paulo@89: } paulo@89: paulo@89: function position(d) { paulo@89: var v = dragging[d]; paulo@89: return v == null ? x(d) : v; paulo@89: } paulo@89: }); paulo@89: paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: chart.dispatch = dispatch; paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width= _;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height= _;}}, paulo@89: dimensionNames: {get: function() { return dimensionNames;}, set: function(_){dimensionNames= _;}}, paulo@89: dimensionFormats : {get: function(){return dimensionFormats;}, set: function (_){dimensionFormats=_;}}, paulo@89: lineTension: {get: function(){return lineTension;}, set: function(_){lineTension = _;}}, paulo@89: paulo@89: // deprecated options paulo@89: dimensions: {get: function (){return dimensionNames;}, set: function(_){ paulo@89: // deprecated after 1.8.1 paulo@89: nv.deprecated('dimensions', 'use dimensionNames instead'); paulo@89: dimensionNames = _; paulo@89: }}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.initOptions(chart); paulo@89: return chart; paulo@89: }; paulo@89: nv.models.pie = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var margin = {top: 0, right: 0, bottom: 0, left: 0} paulo@89: , width = 500 paulo@89: , height = 500 paulo@89: , getX = function(d) { return d.x } paulo@89: , getY = function(d) { return d.y } paulo@89: , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one paulo@89: , container = null paulo@89: , color = nv.utils.defaultColor() paulo@89: , valueFormat = d3.format(',.2f') paulo@89: , showLabels = true paulo@89: , labelsOutside = false paulo@89: , labelType = "key" paulo@89: , labelThreshold = .02 //if slice percentage is under this, don't show label paulo@89: , donut = false paulo@89: , title = false paulo@89: , growOnHover = true paulo@89: , titleOffset = 0 paulo@89: , labelSunbeamLayout = false paulo@89: , startAngle = false paulo@89: , padAngle = false paulo@89: , endAngle = false paulo@89: , cornerRadius = 0 paulo@89: , donutRatio = 0.5 paulo@89: , arcsRadius = [] paulo@89: , dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd') paulo@89: ; paulo@89: paulo@89: var arcs = []; paulo@89: var arcsOver = []; paulo@89: paulo@89: //============================================================ paulo@89: // chart function paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var renderWatch = nv.utils.renderWatch(dispatch); paulo@89: paulo@89: function chart(selection) { paulo@89: renderWatch.reset(); paulo@89: selection.each(function(data) { paulo@89: var availableWidth = width - margin.left - margin.right paulo@89: , availableHeight = height - margin.top - margin.bottom paulo@89: , radius = Math.min(availableWidth, availableHeight) / 2 paulo@89: , arcsRadiusOuter = [] paulo@89: , arcsRadiusInner = [] paulo@89: ; paulo@89: paulo@89: container = d3.select(this) paulo@89: if (arcsRadius.length === 0) { paulo@89: var outer = radius - radius / 5; paulo@89: var inner = donutRatio * radius; paulo@89: for (var i = 0; i < data[0].length; i++) { paulo@89: arcsRadiusOuter.push(outer); paulo@89: arcsRadiusInner.push(inner); paulo@89: } paulo@89: } else { paulo@89: arcsRadiusOuter = arcsRadius.map(function (d) { return (d.outer - d.outer / 5) * radius; }); paulo@89: arcsRadiusInner = arcsRadius.map(function (d) { return (d.inner - d.inner / 5) * radius; }); paulo@89: donutRatio = d3.min(arcsRadius.map(function (d) { return (d.inner - d.inner / 5); })); paulo@89: } paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('.nv-wrap.nv-pie').data(data); paulo@89: var wrapEnter = wrap.enter().append('g').attr('class','nvd3 nv-wrap nv-pie nv-chart-' + id); paulo@89: var gEnter = wrapEnter.append('g'); paulo@89: var g = wrap.select('g'); paulo@89: var g_pie = gEnter.append('g').attr('class', 'nv-pie'); paulo@89: gEnter.append('g').attr('class', 'nv-pieLabels'); paulo@89: paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: g.select('.nv-pie').attr('transform', 'translate(' + availableWidth / 2 + ',' + availableHeight / 2 + ')'); paulo@89: g.select('.nv-pieLabels').attr('transform', 'translate(' + availableWidth / 2 + ',' + availableHeight / 2 + ')'); paulo@89: paulo@89: // paulo@89: container.on('click', function(d,i) { paulo@89: dispatch.chartClick({ paulo@89: data: d, paulo@89: index: i, paulo@89: pos: d3.event, paulo@89: id: id paulo@89: }); paulo@89: }); paulo@89: paulo@89: arcs = []; paulo@89: arcsOver = []; paulo@89: for (var i = 0; i < data[0].length; i++) { paulo@89: paulo@89: var arc = d3.svg.arc().outerRadius(arcsRadiusOuter[i]); paulo@89: var arcOver = d3.svg.arc().outerRadius(arcsRadiusOuter[i] + 5); paulo@89: paulo@89: if (startAngle !== false) { paulo@89: arc.startAngle(startAngle); paulo@89: arcOver.startAngle(startAngle); paulo@89: } paulo@89: if (endAngle !== false) { paulo@89: arc.endAngle(endAngle); paulo@89: arcOver.endAngle(endAngle); paulo@89: } paulo@89: if (donut) { paulo@89: arc.innerRadius(arcsRadiusInner[i]); paulo@89: arcOver.innerRadius(arcsRadiusInner[i]); paulo@89: } paulo@89: paulo@89: if (arc.cornerRadius && cornerRadius) { paulo@89: arc.cornerRadius(cornerRadius); paulo@89: arcOver.cornerRadius(cornerRadius); paulo@89: } paulo@89: paulo@89: arcs.push(arc); paulo@89: arcsOver.push(arcOver); paulo@89: } paulo@89: paulo@89: // Setup the Pie chart and choose the data element paulo@89: var pie = d3.layout.pie() paulo@89: .sort(null) paulo@89: .value(function(d) { return d.disabled ? 0 : getY(d) }); paulo@89: paulo@89: // padAngle added in d3 3.5 paulo@89: if (pie.padAngle && padAngle) { paulo@89: pie.padAngle(padAngle); paulo@89: } paulo@89: paulo@89: // if title is specified and donut, put it in the middle paulo@89: if (donut && title) { paulo@89: g_pie.append("text").attr('class', 'nv-pie-title'); paulo@89: paulo@89: wrap.select('.nv-pie-title') paulo@89: .style("text-anchor", "middle") paulo@89: .text(function (d) { paulo@89: return title; paulo@89: }) paulo@89: .style("font-size", (Math.min(availableWidth, availableHeight)) * donutRatio * 2 / (title.length + 2) + "px") paulo@89: .attr("dy", "0.35em") // trick to vertically center text paulo@89: .attr('transform', function(d, i) { paulo@89: return 'translate(0, '+ titleOffset + ')'; paulo@89: }); paulo@89: } paulo@89: paulo@89: var slices = wrap.select('.nv-pie').selectAll('.nv-slice').data(pie); paulo@89: var pieLabels = wrap.select('.nv-pieLabels').selectAll('.nv-label').data(pie); paulo@89: paulo@89: slices.exit().remove(); paulo@89: pieLabels.exit().remove(); paulo@89: paulo@89: var ae = slices.enter().append('g'); paulo@89: ae.attr('class', 'nv-slice'); paulo@89: ae.on('mouseover', function(d, i) { paulo@89: d3.select(this).classed('hover', true); paulo@89: if (growOnHover) { paulo@89: d3.select(this).select("path").transition() paulo@89: .duration(70) paulo@89: .attr("d", arcsOver[i]); paulo@89: } paulo@89: dispatch.elementMouseover({ paulo@89: data: d.data, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: }); paulo@89: ae.on('mouseout', function(d, i) { paulo@89: d3.select(this).classed('hover', false); paulo@89: if (growOnHover) { paulo@89: d3.select(this).select("path").transition() paulo@89: .duration(50) paulo@89: .attr("d", arcs[i]); paulo@89: } paulo@89: dispatch.elementMouseout({data: d.data, index: i}); paulo@89: }); paulo@89: ae.on('mousemove', function(d, i) { paulo@89: dispatch.elementMousemove({data: d.data, index: i}); paulo@89: }); paulo@89: ae.on('click', function(d, i) { paulo@89: dispatch.elementClick({ paulo@89: data: d.data, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: }); paulo@89: ae.on('dblclick', function(d, i) { paulo@89: dispatch.elementDblClick({ paulo@89: data: d.data, paulo@89: index: i, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: }); paulo@89: paulo@89: slices.attr('fill', function(d,i) { return color(d.data, i); }); paulo@89: slices.attr('stroke', function(d,i) { return color(d.data, i); }); paulo@89: paulo@89: var paths = ae.append('path').each(function(d) { paulo@89: this._current = d; paulo@89: }); paulo@89: paulo@89: slices.select('path') paulo@89: .transition() paulo@89: .attr('d', function (d, i) { return arcs[i](d); }) paulo@89: .attrTween('d', arcTween); paulo@89: paulo@89: if (showLabels) { paulo@89: // This does the normal label paulo@89: var labelsArc = []; paulo@89: for (var i = 0; i < data[0].length; i++) { paulo@89: labelsArc.push(arcs[i]); paulo@89: paulo@89: if (labelsOutside) { paulo@89: if (donut) { paulo@89: labelsArc[i] = d3.svg.arc().outerRadius(arcs[i].outerRadius()); paulo@89: if (startAngle !== false) labelsArc[i].startAngle(startAngle); paulo@89: if (endAngle !== false) labelsArc[i].endAngle(endAngle); paulo@89: } paulo@89: } else if (!donut) { paulo@89: labelsArc[i].innerRadius(0); paulo@89: } paulo@89: } paulo@89: paulo@89: pieLabels.enter().append("g").classed("nv-label",true).each(function(d,i) { paulo@89: var group = d3.select(this); paulo@89: paulo@89: group.attr('transform', function (d, i) { paulo@89: if (labelSunbeamLayout) { paulo@89: d.outerRadius = arcsRadiusOuter[i] + 10; // Set Outer Coordinate paulo@89: d.innerRadius = arcsRadiusOuter[i] + 15; // Set Inner Coordinate paulo@89: var rotateAngle = (d.startAngle + d.endAngle) / 2 * (180 / Math.PI); paulo@89: if ((d.startAngle + d.endAngle) / 2 < Math.PI) { paulo@89: rotateAngle -= 90; paulo@89: } else { paulo@89: rotateAngle += 90; paulo@89: } paulo@89: return 'translate(' + labelsArc[i].centroid(d) + ') rotate(' + rotateAngle + ')'; paulo@89: } else { paulo@89: d.outerRadius = radius + 10; // Set Outer Coordinate paulo@89: d.innerRadius = radius + 15; // Set Inner Coordinate paulo@89: return 'translate(' + labelsArc[i].centroid(d) + ')' paulo@89: } paulo@89: }); paulo@89: paulo@89: group.append('rect') paulo@89: .style('stroke', '#fff') paulo@89: .style('fill', '#fff') paulo@89: .attr("rx", 3) paulo@89: .attr("ry", 3); paulo@89: paulo@89: group.append('text') paulo@89: .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 paulo@89: .style('fill', '#000') paulo@89: }); paulo@89: paulo@89: var labelLocationHash = {}; paulo@89: var avgHeight = 14; paulo@89: var avgWidth = 140; paulo@89: var createHashKey = function(coordinates) { paulo@89: return Math.floor(coordinates[0]/avgWidth) * avgWidth + ',' + Math.floor(coordinates[1]/avgHeight) * avgHeight; paulo@89: }; paulo@89: paulo@89: pieLabels.watchTransition(renderWatch, 'pie labels').attr('transform', function (d, i) { paulo@89: if (labelSunbeamLayout) { paulo@89: d.outerRadius = arcsRadiusOuter[i] + 10; // Set Outer Coordinate paulo@89: d.innerRadius = arcsRadiusOuter[i] + 15; // Set Inner Coordinate paulo@89: var rotateAngle = (d.startAngle + d.endAngle) / 2 * (180 / Math.PI); paulo@89: if ((d.startAngle + d.endAngle) / 2 < Math.PI) { paulo@89: rotateAngle -= 90; paulo@89: } else { paulo@89: rotateAngle += 90; paulo@89: } paulo@89: return 'translate(' + labelsArc[i].centroid(d) + ') rotate(' + rotateAngle + ')'; paulo@89: } else { paulo@89: d.outerRadius = radius + 10; // Set Outer Coordinate paulo@89: d.innerRadius = radius + 15; // Set Inner Coordinate paulo@89: paulo@89: /* paulo@89: Overlapping pie labels are not good. What this attempts to do is, prevent overlapping. paulo@89: Each label location is hashed, and if a hash collision occurs, we assume an overlap. paulo@89: Adjust the label's y-position to remove the overlap. paulo@89: */ paulo@89: var center = labelsArc[i].centroid(d); paulo@89: if (d.value) { paulo@89: var hashKey = createHashKey(center); paulo@89: if (labelLocationHash[hashKey]) { paulo@89: center[1] -= avgHeight; paulo@89: } paulo@89: labelLocationHash[createHashKey(center)] = true; paulo@89: } paulo@89: return 'translate(' + center + ')' paulo@89: } paulo@89: }); paulo@89: paulo@89: pieLabels.select(".nv-label text") paulo@89: .style('text-anchor', function(d,i) { paulo@89: //center the text on it's origin or begin/end if orthogonal aligned paulo@89: return labelSunbeamLayout ? ((d.startAngle + d.endAngle) / 2 < Math.PI ? 'start' : 'end') : 'middle'; paulo@89: }) paulo@89: .text(function(d, i) { paulo@89: var percent = (d.endAngle - d.startAngle) / (2 * Math.PI); paulo@89: var label = ''; paulo@89: if (!d.value || percent < labelThreshold) return ''; paulo@89: paulo@89: if(typeof labelType === 'function') { paulo@89: label = labelType(d, i, { paulo@89: 'key': getX(d.data), paulo@89: 'value': getY(d.data), paulo@89: 'percent': valueFormat(percent) paulo@89: }); paulo@89: } else { paulo@89: switch (labelType) { paulo@89: case 'key': paulo@89: label = getX(d.data); paulo@89: break; paulo@89: case 'value': paulo@89: label = valueFormat(getY(d.data)); paulo@89: break; paulo@89: case 'percent': paulo@89: label = d3.format('%')(percent); paulo@89: break; paulo@89: } paulo@89: } paulo@89: return label; paulo@89: }) paulo@89: ; paulo@89: } paulo@89: paulo@89: paulo@89: // Computes the angle of an arc, converting from radians to degrees. paulo@89: function angle(d) { paulo@89: var a = (d.startAngle + d.endAngle) * 90 / Math.PI - 90; paulo@89: return a > 90 ? a - 180 : a; paulo@89: } paulo@89: paulo@89: function arcTween(a, idx) { paulo@89: a.endAngle = isNaN(a.endAngle) ? 0 : a.endAngle; paulo@89: a.startAngle = isNaN(a.startAngle) ? 0 : a.startAngle; paulo@89: if (!donut) a.innerRadius = 0; paulo@89: var i = d3.interpolate(this._current, a); paulo@89: this._current = i(0); paulo@89: return function (t) { paulo@89: return arcs[idx](i(t)); paulo@89: }; paulo@89: } paulo@89: }); paulo@89: paulo@89: renderWatch.renderEnd('pie immediate'); paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: chart.dispatch = dispatch; paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: arcsRadius: { get: function () { return arcsRadius; }, set: function (_) { arcsRadius = _; } }, paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: showLabels: {get: function(){return showLabels;}, set: function(_){showLabels=_;}}, paulo@89: title: {get: function(){return title;}, set: function(_){title=_;}}, paulo@89: titleOffset: {get: function(){return titleOffset;}, set: function(_){titleOffset=_;}}, paulo@89: labelThreshold: {get: function(){return labelThreshold;}, set: function(_){labelThreshold=_;}}, paulo@89: valueFormat: {get: function(){return valueFormat;}, set: function(_){valueFormat=_;}}, paulo@89: x: {get: function(){return getX;}, set: function(_){getX=_;}}, paulo@89: id: {get: function(){return id;}, set: function(_){id=_;}}, paulo@89: endAngle: {get: function(){return endAngle;}, set: function(_){endAngle=_;}}, paulo@89: startAngle: {get: function(){return startAngle;}, set: function(_){startAngle=_;}}, paulo@89: padAngle: {get: function(){return padAngle;}, set: function(_){padAngle=_;}}, paulo@89: cornerRadius: {get: function(){return cornerRadius;}, set: function(_){cornerRadius=_;}}, paulo@89: donutRatio: {get: function(){return donutRatio;}, set: function(_){donutRatio=_;}}, paulo@89: labelsOutside: {get: function(){return labelsOutside;}, set: function(_){labelsOutside=_;}}, paulo@89: labelSunbeamLayout: {get: function(){return labelSunbeamLayout;}, set: function(_){labelSunbeamLayout=_;}}, paulo@89: donut: {get: function(){return donut;}, set: function(_){donut=_;}}, paulo@89: growOnHover: {get: function(){return growOnHover;}, set: function(_){growOnHover=_;}}, paulo@89: paulo@89: // depreciated after 1.7.1 paulo@89: pieLabelsOutside: {get: function(){return labelsOutside;}, set: function(_){ paulo@89: labelsOutside=_; paulo@89: nv.deprecated('pieLabelsOutside', 'use labelsOutside instead'); paulo@89: }}, paulo@89: // depreciated after 1.7.1 paulo@89: donutLabelsOutside: {get: function(){return labelsOutside;}, set: function(_){ paulo@89: labelsOutside=_; paulo@89: nv.deprecated('donutLabelsOutside', 'use labelsOutside instead'); paulo@89: }}, paulo@89: // deprecated after 1.7.1 paulo@89: labelFormat: {get: function(){ return valueFormat;}, set: function(_) { paulo@89: valueFormat=_; paulo@89: nv.deprecated('labelFormat','use valueFormat instead'); paulo@89: }}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = typeof _.top != 'undefined' ? _.top : margin.top; paulo@89: margin.right = typeof _.right != 'undefined' ? _.right : margin.right; paulo@89: margin.bottom = typeof _.bottom != 'undefined' ? _.bottom : margin.bottom; paulo@89: margin.left = typeof _.left != 'undefined' ? _.left : margin.left; paulo@89: }}, paulo@89: y: {get: function(){return getY;}, set: function(_){ paulo@89: getY=d3.functor(_); paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color=nv.utils.getColor(_); paulo@89: }}, paulo@89: labelType: {get: function(){return labelType;}, set: function(_){ paulo@89: labelType= _ || 'key'; paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.initOptions(chart); paulo@89: return chart; paulo@89: }; paulo@89: nv.models.pieChart = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var pie = nv.models.pie(); paulo@89: var legend = nv.models.legend(); paulo@89: var tooltip = nv.models.tooltip(); paulo@89: paulo@89: var margin = {top: 30, right: 20, bottom: 20, left: 20} paulo@89: , width = null paulo@89: , height = null paulo@89: , showLegend = true paulo@89: , legendPosition = "top" paulo@89: , color = nv.utils.defaultColor() paulo@89: , state = nv.utils.state() paulo@89: , defaultState = null paulo@89: , noData = null paulo@89: , duration = 250 paulo@89: , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'stateChange', 'changeState','renderEnd') paulo@89: ; paulo@89: paulo@89: tooltip paulo@89: .headerEnabled(false) paulo@89: .duration(0) paulo@89: .valueFormatter(function(d, i) { paulo@89: return pie.valueFormat()(d, i); paulo@89: }); paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var renderWatch = nv.utils.renderWatch(dispatch); paulo@89: paulo@89: var stateGetter = function(data) { paulo@89: return function(){ paulo@89: return { paulo@89: active: data.map(function(d) { return !d.disabled }) paulo@89: }; paulo@89: } paulo@89: }; paulo@89: paulo@89: var stateSetter = function(data) { paulo@89: return function(state) { paulo@89: if (state.active !== undefined) { paulo@89: data.forEach(function (series, i) { paulo@89: series.disabled = !state.active[i]; paulo@89: }); paulo@89: } paulo@89: } paulo@89: }; paulo@89: paulo@89: //============================================================ paulo@89: // Chart function paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: function chart(selection) { paulo@89: renderWatch.reset(); paulo@89: renderWatch.models(pie); paulo@89: paulo@89: selection.each(function(data) { paulo@89: var container = d3.select(this); paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: var that = this; paulo@89: var availableWidth = nv.utils.availableWidth(width, container, margin), paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: paulo@89: chart.update = function() { container.transition().call(chart); }; paulo@89: chart.container = this; paulo@89: paulo@89: state.setter(stateSetter(data), chart.update) paulo@89: .getter(stateGetter(data)) paulo@89: .update(); paulo@89: paulo@89: //set state.disabled paulo@89: state.disabled = data.map(function(d) { return !!d.disabled }); paulo@89: paulo@89: if (!defaultState) { paulo@89: var key; paulo@89: defaultState = {}; paulo@89: for (key in state) { paulo@89: if (state[key] instanceof Array) paulo@89: defaultState[key] = state[key].slice(0); paulo@89: else paulo@89: defaultState[key] = state[key]; paulo@89: } paulo@89: } paulo@89: paulo@89: // Display No Data message if there's nothing to show. paulo@89: if (!data || !data.length) { paulo@89: nv.utils.noData(chart, container); paulo@89: return chart; paulo@89: } else { paulo@89: container.selectAll('.nv-noData').remove(); paulo@89: } paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-pieChart').data([data]); paulo@89: var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-pieChart').append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-pieWrap'); paulo@89: gEnter.append('g').attr('class', 'nv-legendWrap'); paulo@89: paulo@89: // Legend paulo@89: if (showLegend) { paulo@89: if (legendPosition === "top") { paulo@89: legend.width( availableWidth ).key(pie.x()); paulo@89: paulo@89: wrap.select('.nv-legendWrap') paulo@89: .datum(data) paulo@89: .call(legend); paulo@89: paulo@89: if ( margin.top != legend.height()) { paulo@89: margin.top = legend.height(); paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: } paulo@89: paulo@89: wrap.select('.nv-legendWrap') paulo@89: .attr('transform', 'translate(0,' + (-margin.top) +')'); paulo@89: } else if (legendPosition === "right") { paulo@89: var legendWidth = nv.models.legend().width(); paulo@89: if (availableWidth / 2 < legendWidth) { paulo@89: legendWidth = (availableWidth / 2) paulo@89: } paulo@89: legend.height(availableHeight).key(pie.x()); paulo@89: legend.width(legendWidth); paulo@89: availableWidth -= legend.width(); paulo@89: paulo@89: wrap.select('.nv-legendWrap') paulo@89: .datum(data) paulo@89: .call(legend) paulo@89: .attr('transform', 'translate(' + (availableWidth) +',0)'); paulo@89: } paulo@89: } paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: // Main Chart Component(s) paulo@89: pie.width(availableWidth).height(availableHeight); paulo@89: var pieWrap = g.select('.nv-pieWrap').datum([data]); paulo@89: d3.transition(pieWrap).call(pie); paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (in chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: legend.dispatch.on('stateChange', function(newState) { paulo@89: for (var key in newState) { paulo@89: state[key] = newState[key]; paulo@89: } paulo@89: dispatch.stateChange(state); paulo@89: chart.update(); paulo@89: }); paulo@89: paulo@89: // Update chart from a state object passed to event handler paulo@89: dispatch.on('changeState', function(e) { paulo@89: if (typeof e.disabled !== 'undefined') { paulo@89: data.forEach(function(series,i) { paulo@89: series.disabled = e.disabled[i]; paulo@89: }); paulo@89: state.disabled = e.disabled; paulo@89: } paulo@89: chart.update(); paulo@89: }); paulo@89: }); paulo@89: paulo@89: renderWatch.renderEnd('pieChart immediate'); paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (out of chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: pie.dispatch.on('elementMouseover.tooltip', function(evt) { paulo@89: evt['series'] = { paulo@89: key: chart.x()(evt.data), paulo@89: value: chart.y()(evt.data), paulo@89: color: evt.color paulo@89: }; paulo@89: tooltip.data(evt).hidden(false); paulo@89: }); paulo@89: paulo@89: pie.dispatch.on('elementMouseout.tooltip', function(evt) { paulo@89: tooltip.hidden(true); paulo@89: }); paulo@89: paulo@89: pie.dispatch.on('elementMousemove.tooltip', function(evt) { paulo@89: tooltip.position({top: d3.event.pageY, left: d3.event.pageX})(); paulo@89: }); paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: // expose chart's sub-components paulo@89: chart.legend = legend; paulo@89: chart.dispatch = dispatch; paulo@89: chart.pie = pie; paulo@89: chart.tooltip = tooltip; paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: // use Object get/set functionality to map between vars and chart functions paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: noData: {get: function(){return noData;}, set: function(_){noData=_;}}, paulo@89: showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}}, paulo@89: legendPosition: {get: function(){return legendPosition;}, set: function(_){legendPosition=_;}}, paulo@89: defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}}, paulo@89: paulo@89: // deprecated options paulo@89: tooltips: {get: function(){return tooltip.enabled();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead'); paulo@89: tooltip.enabled(!!_); paulo@89: }}, paulo@89: tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead'); paulo@89: tooltip.contentGenerator(_); paulo@89: }}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = _; paulo@89: legend.color(color); paulo@89: pie.color(color); paulo@89: }}, paulo@89: duration: {get: function(){return duration;}, set: function(_){ paulo@89: duration = _; paulo@89: renderWatch.reset(duration); paulo@89: }}, paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }} paulo@89: }); paulo@89: nv.utils.inheritOptions(chart, pie); paulo@89: nv.utils.initOptions(chart); paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: nv.models.scatter = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var margin = {top: 0, right: 0, bottom: 0, left: 0} paulo@89: , width = null paulo@89: , height = null paulo@89: , color = nv.utils.defaultColor() // chooses color paulo@89: , id = Math.floor(Math.random() * 100000) //Create semi-unique ID incase user doesn't select one paulo@89: , container = null paulo@89: , x = d3.scale.linear() paulo@89: , y = d3.scale.linear() paulo@89: , z = d3.scale.linear() //linear because d3.svg.shape.size is treated as area paulo@89: , getX = function(d) { return d.x } // accessor to get the x value paulo@89: , getY = function(d) { return d.y } // accessor to get the y value paulo@89: , getSize = function(d) { return d.size || 1} // accessor to get the point size paulo@89: , getShape = function(d) { return d.shape || 'circle' } // accessor to get point shape paulo@89: , forceX = [] // List of numbers to Force into the X scale (ie. 0, or a max / min, etc.) paulo@89: , forceY = [] // List of numbers to Force into the Y scale paulo@89: , forceSize = [] // List of numbers to Force into the Size scale paulo@89: , interactive = true // If true, plots a voronoi overlay for advanced point intersection paulo@89: , pointActive = function(d) { return !d.notActive } // any points that return false will be filtered out paulo@89: , padData = false // If true, adds half a data points width to front and back, for lining up a line chart with a bar chart paulo@89: , padDataOuter = .1 //outerPadding to imitate ordinal scale outer padding paulo@89: , clipEdge = false // if true, masks points within x and y scale paulo@89: , clipVoronoi = true // if true, masks each point with a circle... can turn off to slightly increase performance paulo@89: , showVoronoi = false // display the voronoi areas paulo@89: , clipRadius = function() { return 25 } // function to get the radius for voronoi point clips paulo@89: , xDomain = null // Override x domain (skips the calculation from data) paulo@89: , yDomain = null // Override y domain paulo@89: , xRange = null // Override x range paulo@89: , yRange = null // Override y range paulo@89: , sizeDomain = null // Override point size domain paulo@89: , sizeRange = null paulo@89: , singlePoint = false paulo@89: , dispatch = d3.dispatch('elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'renderEnd') paulo@89: , useVoronoi = true paulo@89: , duration = 250 paulo@89: ; paulo@89: paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var x0, y0, z0 // used to store previous scales paulo@89: , timeoutID paulo@89: , needsUpdate = false // Flag for when the points are visually updating, but the interactive layer is behind, to disable tooltips paulo@89: , renderWatch = nv.utils.renderWatch(dispatch, duration) paulo@89: , _sizeRange_def = [16, 256] paulo@89: ; paulo@89: paulo@89: function chart(selection) { paulo@89: renderWatch.reset(); paulo@89: selection.each(function(data) { paulo@89: container = d3.select(this); paulo@89: var availableWidth = nv.utils.availableWidth(width, container, margin), paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: //add series index to each data point for reference paulo@89: data.forEach(function(series, i) { paulo@89: series.values.forEach(function(point) { paulo@89: point.series = i; paulo@89: }); paulo@89: }); paulo@89: paulo@89: // Setup Scales paulo@89: // remap and flatten the data for use in calculating the scales' domains paulo@89: 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 paulo@89: d3.merge( paulo@89: data.map(function(d) { paulo@89: return d.values.map(function(d,i) { paulo@89: return { x: getX(d,i), y: getY(d,i), size: getSize(d,i) } paulo@89: }) paulo@89: }) paulo@89: ); paulo@89: paulo@89: x .domain(xDomain || d3.extent(seriesData.map(function(d) { return d.x; }).concat(forceX))) paulo@89: paulo@89: if (padData && data[0]) paulo@89: x.range(xRange || [(availableWidth * padDataOuter + availableWidth) / (2 *data[0].values.length), availableWidth - availableWidth * (1 + padDataOuter) / (2 * data[0].values.length) ]); paulo@89: //x.range([availableWidth * .5 / data[0].values.length, availableWidth * (data[0].values.length - .5) / data[0].values.length ]); paulo@89: else paulo@89: x.range(xRange || [0, availableWidth]); paulo@89: paulo@89: y .domain(yDomain || d3.extent(seriesData.map(function(d) { return d.y }).concat(forceY))) paulo@89: .range(yRange || [availableHeight, 0]); paulo@89: paulo@89: z .domain(sizeDomain || d3.extent(seriesData.map(function(d) { return d.size }).concat(forceSize))) paulo@89: .range(sizeRange || _sizeRange_def); paulo@89: paulo@89: // If scale's domain don't have a range, slightly adjust to make one... so a chart can show a single data point paulo@89: singlePoint = x.domain()[0] === x.domain()[1] || y.domain()[0] === y.domain()[1]; paulo@89: paulo@89: if (x.domain()[0] === x.domain()[1]) paulo@89: x.domain()[0] ? paulo@89: x.domain([x.domain()[0] - x.domain()[0] * 0.01, x.domain()[1] + x.domain()[1] * 0.01]) paulo@89: : x.domain([-1,1]); paulo@89: paulo@89: if (y.domain()[0] === y.domain()[1]) paulo@89: y.domain()[0] ? paulo@89: y.domain([y.domain()[0] - y.domain()[0] * 0.01, y.domain()[1] + y.domain()[1] * 0.01]) paulo@89: : y.domain([-1,1]); paulo@89: paulo@89: if ( isNaN(x.domain()[0])) { paulo@89: x.domain([-1,1]); paulo@89: } paulo@89: paulo@89: if ( isNaN(y.domain()[0])) { paulo@89: y.domain([-1,1]); paulo@89: } paulo@89: paulo@89: x0 = x0 || x; paulo@89: y0 = y0 || y; paulo@89: z0 = z0 || z; paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-scatter').data([data]); paulo@89: var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-scatter nv-chart-' + id); paulo@89: var defsEnter = wrapEnter.append('defs'); paulo@89: var gEnter = wrapEnter.append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: wrap.classed('nv-single-point', singlePoint); paulo@89: gEnter.append('g').attr('class', 'nv-groups'); paulo@89: gEnter.append('g').attr('class', 'nv-point-paths'); paulo@89: wrapEnter.append('g').attr('class', 'nv-point-clips'); paulo@89: paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: defsEnter.append('clipPath') paulo@89: .attr('id', 'nv-edge-clip-' + id) paulo@89: .append('rect'); paulo@89: paulo@89: wrap.select('#nv-edge-clip-' + id + ' rect') paulo@89: .attr('width', availableWidth) paulo@89: .attr('height', (availableHeight > 0) ? availableHeight : 0); paulo@89: paulo@89: g.attr('clip-path', clipEdge ? 'url(#nv-edge-clip-' + id + ')' : ''); paulo@89: paulo@89: function updateInteractiveLayer() { paulo@89: // Always clear needs-update flag regardless of whether or not paulo@89: // we will actually do anything (avoids needless invocations). paulo@89: needsUpdate = false; paulo@89: paulo@89: if (!interactive) return false; paulo@89: paulo@89: // inject series and point index for reference into voronoi paulo@89: if (useVoronoi === true) { paulo@89: var vertices = d3.merge(data.map(function(group, groupIndex) { paulo@89: return group.values paulo@89: .map(function(point, pointIndex) { paulo@89: // *Adding noise to make duplicates very unlikely paulo@89: // *Injecting series and point index for reference paulo@89: /* *Adding a 'jitter' to the points, because there's an issue in d3.geom.voronoi. paulo@89: */ paulo@89: var pX = getX(point,pointIndex); paulo@89: var pY = getY(point,pointIndex); paulo@89: paulo@89: return [x(pX)+ Math.random() * 1e-4, paulo@89: y(pY)+ Math.random() * 1e-4, paulo@89: groupIndex, paulo@89: pointIndex, point]; //temp hack to add noise until I think of a better way so there are no duplicates paulo@89: }) paulo@89: .filter(function(pointArray, pointIndex) { paulo@89: return pointActive(pointArray[4], pointIndex); // Issue #237.. move filter to after map, so pointIndex is correct! paulo@89: }) paulo@89: }) paulo@89: ); paulo@89: paulo@89: if (vertices.length == 0) return false; // No active points, we're done paulo@89: if (vertices.length < 3) { paulo@89: // Issue #283 - Adding 2 dummy points to the voronoi b/c voronoi requires min 3 points to work paulo@89: vertices.push([x.range()[0] - 20, y.range()[0] - 20, null, null]); paulo@89: vertices.push([x.range()[1] + 20, y.range()[1] + 20, null, null]); paulo@89: vertices.push([x.range()[0] - 20, y.range()[0] + 20, null, null]); paulo@89: vertices.push([x.range()[1] + 20, y.range()[1] - 20, null, null]); paulo@89: } paulo@89: paulo@89: // keep voronoi sections from going more than 10 outside of graph paulo@89: // to avoid overlap with other things like legend etc paulo@89: var bounds = d3.geom.polygon([ paulo@89: [-10,-10], paulo@89: [-10,height + 10], paulo@89: [width + 10,height + 10], paulo@89: [width + 10,-10] paulo@89: ]); paulo@89: paulo@89: var voronoi = d3.geom.voronoi(vertices).map(function(d, i) { paulo@89: return { paulo@89: 'data': bounds.clip(d), paulo@89: 'series': vertices[i][2], paulo@89: 'point': vertices[i][3] paulo@89: } paulo@89: }); paulo@89: paulo@89: // nuke all voronoi paths on reload and recreate them paulo@89: wrap.select('.nv-point-paths').selectAll('path').remove(); paulo@89: var pointPaths = wrap.select('.nv-point-paths').selectAll('path').data(voronoi); paulo@89: var vPointPaths = pointPaths paulo@89: .enter().append("svg:path") paulo@89: .attr("d", function(d) { paulo@89: if (!d || !d.data || d.data.length === 0) paulo@89: return 'M 0 0'; paulo@89: else paulo@89: return "M" + d.data.join(",") + "Z"; paulo@89: }) paulo@89: .attr("id", function(d,i) { paulo@89: return "nv-path-"+i; }) paulo@89: .attr("clip-path", function(d,i) { return "url(#nv-clip-"+i+")"; }) paulo@89: ; paulo@89: paulo@89: // good for debugging point hover issues paulo@89: if (showVoronoi) { paulo@89: vPointPaths.style("fill", d3.rgb(230, 230, 230)) paulo@89: .style('fill-opacity', 0.4) paulo@89: .style('stroke-opacity', 1) paulo@89: .style("stroke", d3.rgb(200,200,200)); paulo@89: } paulo@89: paulo@89: if (clipVoronoi) { paulo@89: // voronoi sections are already set to clip, paulo@89: // just create the circles with the IDs they expect paulo@89: wrap.select('.nv-point-clips').selectAll('clipPath').remove(); paulo@89: wrap.select('.nv-point-clips').selectAll("clipPath") paulo@89: .data(vertices) paulo@89: .enter().append("svg:clipPath") paulo@89: .attr("id", function(d, i) { return "nv-clip-"+i;}) paulo@89: .append("svg:circle") paulo@89: .attr('cx', function(d) { return d[0]; }) paulo@89: .attr('cy', function(d) { return d[1]; }) paulo@89: .attr('r', clipRadius); paulo@89: } paulo@89: paulo@89: var mouseEventCallback = function(d, mDispatch) { paulo@89: if (needsUpdate) return 0; paulo@89: var series = data[d.series]; paulo@89: if (series === undefined) return; paulo@89: var point = series.values[d.point]; paulo@89: point['color'] = color(series, d.series); paulo@89: paulo@89: // standardize attributes for tooltip. paulo@89: point['x'] = getX(point); paulo@89: point['y'] = getY(point); paulo@89: paulo@89: // can't just get box of event node since it's actually a voronoi polygon paulo@89: var box = container.node().getBoundingClientRect(); paulo@89: var scrollTop = window.pageYOffset || document.documentElement.scrollTop; paulo@89: var scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; paulo@89: paulo@89: var pos = { paulo@89: left: x(getX(point, d.point)) + box.left + scrollLeft + margin.left + 10, paulo@89: top: y(getY(point, d.point)) + box.top + scrollTop + margin.top + 10 paulo@89: }; paulo@89: paulo@89: mDispatch({ paulo@89: point: point, paulo@89: series: series, paulo@89: pos: pos, paulo@89: seriesIndex: d.series, paulo@89: pointIndex: d.point paulo@89: }); paulo@89: }; paulo@89: paulo@89: pointPaths paulo@89: .on('click', function(d) { paulo@89: mouseEventCallback(d, dispatch.elementClick); paulo@89: }) paulo@89: .on('dblclick', function(d) { paulo@89: mouseEventCallback(d, dispatch.elementDblClick); paulo@89: }) paulo@89: .on('mouseover', function(d) { paulo@89: mouseEventCallback(d, dispatch.elementMouseover); paulo@89: }) paulo@89: .on('mouseout', function(d, i) { paulo@89: mouseEventCallback(d, dispatch.elementMouseout); paulo@89: }); paulo@89: paulo@89: } else { paulo@89: // add event handlers to points instead voronoi paths paulo@89: wrap.select('.nv-groups').selectAll('.nv-group') paulo@89: .selectAll('.nv-point') paulo@89: //.data(dataWithPoints) paulo@89: //.style('pointer-events', 'auto') // recativate events, disabled by css paulo@89: .on('click', function(d,i) { paulo@89: //nv.log('test', d, i); paulo@89: if (needsUpdate || !data[d.series]) return 0; //check if this is a dummy point paulo@89: var series = data[d.series], paulo@89: point = series.values[i]; paulo@89: paulo@89: dispatch.elementClick({ paulo@89: point: point, paulo@89: series: series, paulo@89: pos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top], paulo@89: seriesIndex: d.series, paulo@89: pointIndex: i paulo@89: }); paulo@89: }) paulo@89: .on('dblclick', function(d,i) { paulo@89: if (needsUpdate || !data[d.series]) return 0; //check if this is a dummy point paulo@89: var series = data[d.series], paulo@89: point = series.values[i]; paulo@89: paulo@89: dispatch.elementDblClick({ paulo@89: point: point, paulo@89: series: series, paulo@89: pos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top], paulo@89: seriesIndex: d.series, paulo@89: pointIndex: i paulo@89: }); paulo@89: }) paulo@89: .on('mouseover', function(d,i) { paulo@89: if (needsUpdate || !data[d.series]) return 0; //check if this is a dummy point paulo@89: var series = data[d.series], paulo@89: point = series.values[i]; paulo@89: paulo@89: dispatch.elementMouseover({ paulo@89: point: point, paulo@89: series: series, paulo@89: pos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top], paulo@89: seriesIndex: d.series, paulo@89: pointIndex: i, paulo@89: color: color(d, i) paulo@89: }); paulo@89: }) paulo@89: .on('mouseout', function(d,i) { paulo@89: if (needsUpdate || !data[d.series]) return 0; //check if this is a dummy point paulo@89: var series = data[d.series], paulo@89: point = series.values[i]; paulo@89: paulo@89: dispatch.elementMouseout({ paulo@89: point: point, paulo@89: series: series, paulo@89: seriesIndex: d.series, paulo@89: pointIndex: i, paulo@89: color: color(d, i) paulo@89: }); paulo@89: }); paulo@89: } paulo@89: } paulo@89: paulo@89: needsUpdate = true; paulo@89: var groups = wrap.select('.nv-groups').selectAll('.nv-group') paulo@89: .data(function(d) { return d }, function(d) { return d.key }); paulo@89: groups.enter().append('g') paulo@89: .style('stroke-opacity', 1e-6) paulo@89: .style('fill-opacity', 1e-6); paulo@89: groups.exit() paulo@89: .remove(); paulo@89: groups paulo@89: .attr('class', function(d,i) { return 'nv-group nv-series-' + i }) paulo@89: .classed('hover', function(d) { return d.hover }); paulo@89: groups.watchTransition(renderWatch, 'scatter: groups') paulo@89: .style('fill', function(d,i) { return color(d, i) }) paulo@89: .style('stroke', function(d,i) { return color(d, i) }) paulo@89: .style('stroke-opacity', 1) paulo@89: .style('fill-opacity', .5); paulo@89: paulo@89: // create the points, maintaining their IDs from the original data set paulo@89: var points = groups.selectAll('path.nv-point') paulo@89: .data(function(d) { paulo@89: return d.values.map( paulo@89: function (point, pointIndex) { paulo@89: return [point, pointIndex] paulo@89: }).filter( paulo@89: function(pointArray, pointIndex) { paulo@89: return pointActive(pointArray[0], pointIndex) paulo@89: }) paulo@89: }); paulo@89: points.enter().append('path') paulo@89: .style('fill', function (d) { return d.color }) paulo@89: .style('stroke', function (d) { return d.color }) paulo@89: .attr('transform', function(d) { paulo@89: return 'translate(' + x0(getX(d[0],d[1])) + ',' + y0(getY(d[0],d[1])) + ')' paulo@89: }) paulo@89: .attr('d', paulo@89: nv.utils.symbol() paulo@89: .type(function(d) { return getShape(d[0]); }) paulo@89: .size(function(d) { return z(getSize(d[0],d[1])) }) paulo@89: ); paulo@89: points.exit().remove(); paulo@89: groups.exit().selectAll('path.nv-point') paulo@89: .watchTransition(renderWatch, 'scatter exit') paulo@89: .attr('transform', function(d) { paulo@89: return 'translate(' + x(getX(d[0],d[1])) + ',' + y(getY(d[0],d[1])) + ')' paulo@89: }) paulo@89: .remove(); paulo@89: points.each(function(d) { paulo@89: d3.select(this) paulo@89: .classed('nv-point', true) paulo@89: .classed('nv-point-' + d[1], true) paulo@89: .classed('nv-noninteractive', !interactive) paulo@89: .classed('hover',false) paulo@89: ; paulo@89: }); paulo@89: points paulo@89: .watchTransition(renderWatch, 'scatter points') paulo@89: .attr('transform', function(d) { paulo@89: //nv.log(d, getX(d[0],d[1]), x(getX(d[0],d[1]))); paulo@89: return 'translate(' + x(getX(d[0],d[1])) + ',' + y(getY(d[0],d[1])) + ')' paulo@89: }) paulo@89: .attr('d', paulo@89: nv.utils.symbol() paulo@89: .type(function(d) { return getShape(d[0]); }) paulo@89: .size(function(d) { return z(getSize(d[0],d[1])) }) paulo@89: ); paulo@89: paulo@89: // Delay updating the invisible interactive layer for smoother animation paulo@89: clearTimeout(timeoutID); // stop repeat calls to updateInteractiveLayer paulo@89: timeoutID = setTimeout(updateInteractiveLayer, 300); paulo@89: //updateInteractiveLayer(); paulo@89: paulo@89: //store old scales for use in transitions on update paulo@89: x0 = x.copy(); paulo@89: y0 = y.copy(); paulo@89: z0 = z.copy(); paulo@89: paulo@89: }); paulo@89: renderWatch.renderEnd('scatter immediate'); paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: chart.dispatch = dispatch; paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: // utility function calls provided by this chart paulo@89: chart._calls = new function() { paulo@89: this.clearHighlights = function () { paulo@89: nv.dom.write(function() { paulo@89: container.selectAll(".nv-point.hover").classed("hover", false); paulo@89: }); paulo@89: return null; paulo@89: }; paulo@89: this.highlightPoint = function (seriesIndex, pointIndex, isHoverOver) { paulo@89: nv.dom.write(function() { paulo@89: container.select(" .nv-series-" + seriesIndex + " .nv-point-" + pointIndex) paulo@89: .classed("hover", isHoverOver); paulo@89: }); paulo@89: }; paulo@89: }; paulo@89: paulo@89: // trigger calls from events too paulo@89: dispatch.on('elementMouseover.point', function(d) { paulo@89: if (interactive) chart._calls.highlightPoint(d.seriesIndex,d.pointIndex,true); paulo@89: }); paulo@89: paulo@89: dispatch.on('elementMouseout.point', function(d) { paulo@89: if (interactive) chart._calls.highlightPoint(d.seriesIndex,d.pointIndex,false); paulo@89: }); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: xScale: {get: function(){return x;}, set: function(_){x=_;}}, paulo@89: yScale: {get: function(){return y;}, set: function(_){y=_;}}, paulo@89: pointScale: {get: function(){return z;}, set: function(_){z=_;}}, paulo@89: xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}}, paulo@89: yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}}, paulo@89: pointDomain: {get: function(){return sizeDomain;}, set: function(_){sizeDomain=_;}}, paulo@89: xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}}, paulo@89: yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}}, paulo@89: pointRange: {get: function(){return sizeRange;}, set: function(_){sizeRange=_;}}, paulo@89: forceX: {get: function(){return forceX;}, set: function(_){forceX=_;}}, paulo@89: forceY: {get: function(){return forceY;}, set: function(_){forceY=_;}}, paulo@89: forcePoint: {get: function(){return forceSize;}, set: function(_){forceSize=_;}}, paulo@89: interactive: {get: function(){return interactive;}, set: function(_){interactive=_;}}, paulo@89: pointActive: {get: function(){return pointActive;}, set: function(_){pointActive=_;}}, paulo@89: padDataOuter: {get: function(){return padDataOuter;}, set: function(_){padDataOuter=_;}}, paulo@89: padData: {get: function(){return padData;}, set: function(_){padData=_;}}, paulo@89: clipEdge: {get: function(){return clipEdge;}, set: function(_){clipEdge=_;}}, paulo@89: clipVoronoi: {get: function(){return clipVoronoi;}, set: function(_){clipVoronoi=_;}}, paulo@89: clipRadius: {get: function(){return clipRadius;}, set: function(_){clipRadius=_;}}, paulo@89: showVoronoi: {get: function(){return showVoronoi;}, set: function(_){showVoronoi=_;}}, paulo@89: id: {get: function(){return id;}, set: function(_){id=_;}}, paulo@89: paulo@89: paulo@89: // simple functor options paulo@89: x: {get: function(){return getX;}, set: function(_){getX = d3.functor(_);}}, paulo@89: y: {get: function(){return getY;}, set: function(_){getY = d3.functor(_);}}, paulo@89: pointSize: {get: function(){return getSize;}, set: function(_){getSize = d3.functor(_);}}, paulo@89: pointShape: {get: function(){return getShape;}, set: function(_){getShape = d3.functor(_);}}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: duration: {get: function(){return duration;}, set: function(_){ paulo@89: duration = _; paulo@89: renderWatch.reset(duration); paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: }}, paulo@89: useVoronoi: {get: function(){return useVoronoi;}, set: function(_){ paulo@89: useVoronoi = _; paulo@89: if (useVoronoi === false) { paulo@89: clipVoronoi = false; paulo@89: } paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.initOptions(chart); paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: nv.models.scatterChart = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var scatter = nv.models.scatter() paulo@89: , xAxis = nv.models.axis() paulo@89: , yAxis = nv.models.axis() paulo@89: , legend = nv.models.legend() paulo@89: , distX = nv.models.distribution() paulo@89: , distY = nv.models.distribution() paulo@89: , tooltip = nv.models.tooltip() paulo@89: ; paulo@89: paulo@89: var margin = {top: 30, right: 20, bottom: 50, left: 75} paulo@89: , width = null paulo@89: , height = null paulo@89: , container = null paulo@89: , color = nv.utils.defaultColor() paulo@89: , x = scatter.xScale() paulo@89: , y = scatter.yScale() paulo@89: , showDistX = false paulo@89: , showDistY = false paulo@89: , showLegend = true paulo@89: , showXAxis = true paulo@89: , showYAxis = true paulo@89: , rightAlignYAxis = false paulo@89: , state = nv.utils.state() paulo@89: , defaultState = null paulo@89: , dispatch = d3.dispatch('stateChange', 'changeState', 'renderEnd') paulo@89: , noData = null paulo@89: , duration = 250 paulo@89: ; paulo@89: paulo@89: scatter.xScale(x).yScale(y); paulo@89: xAxis.orient('bottom').tickPadding(10); paulo@89: yAxis paulo@89: .orient((rightAlignYAxis) ? 'right' : 'left') paulo@89: .tickPadding(10) paulo@89: ; paulo@89: distX.axis('x'); paulo@89: distY.axis('y'); paulo@89: tooltip paulo@89: .headerFormatter(function(d, i) { paulo@89: return xAxis.tickFormat()(d, i); paulo@89: }) paulo@89: .valueFormatter(function(d, i) { paulo@89: return yAxis.tickFormat()(d, i); paulo@89: }); paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var x0, y0 paulo@89: , renderWatch = nv.utils.renderWatch(dispatch, duration); paulo@89: paulo@89: var stateGetter = function(data) { paulo@89: return function(){ paulo@89: return { paulo@89: active: data.map(function(d) { return !d.disabled }) paulo@89: }; paulo@89: } paulo@89: }; paulo@89: paulo@89: var stateSetter = function(data) { paulo@89: return function(state) { paulo@89: if (state.active !== undefined) paulo@89: data.forEach(function(series,i) { paulo@89: series.disabled = !state.active[i]; paulo@89: }); paulo@89: } paulo@89: }; paulo@89: paulo@89: function chart(selection) { paulo@89: renderWatch.reset(); paulo@89: renderWatch.models(scatter); paulo@89: if (showXAxis) renderWatch.models(xAxis); paulo@89: if (showYAxis) renderWatch.models(yAxis); paulo@89: if (showDistX) renderWatch.models(distX); paulo@89: if (showDistY) renderWatch.models(distY); paulo@89: paulo@89: selection.each(function(data) { paulo@89: var that = this; paulo@89: paulo@89: container = d3.select(this); paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: var availableWidth = nv.utils.availableWidth(width, container, margin), paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: paulo@89: chart.update = function() { paulo@89: if (duration === 0) paulo@89: container.call(chart); paulo@89: else paulo@89: container.transition().duration(duration).call(chart); paulo@89: }; paulo@89: chart.container = this; paulo@89: paulo@89: state paulo@89: .setter(stateSetter(data), chart.update) paulo@89: .getter(stateGetter(data)) paulo@89: .update(); paulo@89: paulo@89: // DEPRECATED set state.disableddisabled paulo@89: state.disabled = data.map(function(d) { return !!d.disabled }); paulo@89: paulo@89: if (!defaultState) { paulo@89: var key; paulo@89: defaultState = {}; paulo@89: for (key in state) { paulo@89: if (state[key] instanceof Array) paulo@89: defaultState[key] = state[key].slice(0); paulo@89: else paulo@89: defaultState[key] = state[key]; paulo@89: } paulo@89: } paulo@89: paulo@89: // Display noData message if there's nothing to show. paulo@89: if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) { paulo@89: nv.utils.noData(chart, container); paulo@89: renderWatch.renderEnd('scatter immediate'); paulo@89: return chart; paulo@89: } else { paulo@89: container.selectAll('.nv-noData').remove(); paulo@89: } paulo@89: paulo@89: // Setup Scales paulo@89: x = scatter.xScale(); paulo@89: y = scatter.yScale(); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-scatterChart').data([data]); paulo@89: var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-scatterChart nv-chart-' + scatter.id()); paulo@89: var gEnter = wrapEnter.append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: // background for pointer events paulo@89: gEnter.append('rect').attr('class', 'nvd3 nv-background').style("pointer-events","none"); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-x nv-axis'); paulo@89: gEnter.append('g').attr('class', 'nv-y nv-axis'); paulo@89: gEnter.append('g').attr('class', 'nv-scatterWrap'); paulo@89: gEnter.append('g').attr('class', 'nv-regressionLinesWrap'); paulo@89: gEnter.append('g').attr('class', 'nv-distWrap'); paulo@89: gEnter.append('g').attr('class', 'nv-legendWrap'); paulo@89: paulo@89: if (rightAlignYAxis) { paulo@89: g.select(".nv-y.nv-axis") paulo@89: .attr("transform", "translate(" + availableWidth + ",0)"); paulo@89: } paulo@89: paulo@89: // Legend paulo@89: if (showLegend) { paulo@89: var legendWidth = availableWidth; paulo@89: legend.width(legendWidth); paulo@89: paulo@89: wrap.select('.nv-legendWrap') paulo@89: .datum(data) paulo@89: .call(legend); paulo@89: paulo@89: if ( margin.top != legend.height()) { paulo@89: margin.top = legend.height(); paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: } paulo@89: paulo@89: wrap.select('.nv-legendWrap') paulo@89: .attr('transform', 'translate(0' + ',' + (-margin.top) +')'); paulo@89: } paulo@89: paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: // Main Chart Component(s) paulo@89: scatter paulo@89: .width(availableWidth) paulo@89: .height(availableHeight) paulo@89: .color(data.map(function(d,i) { paulo@89: d.color = d.color || color(d, i); paulo@89: return d.color; paulo@89: }).filter(function(d,i) { return !data[i].disabled })); paulo@89: paulo@89: wrap.select('.nv-scatterWrap') paulo@89: .datum(data.filter(function(d) { return !d.disabled })) paulo@89: .call(scatter); paulo@89: paulo@89: paulo@89: wrap.select('.nv-regressionLinesWrap') paulo@89: .attr('clip-path', 'url(#nv-edge-clip-' + scatter.id() + ')'); paulo@89: paulo@89: var regWrap = wrap.select('.nv-regressionLinesWrap').selectAll('.nv-regLines') paulo@89: .data(function (d) { paulo@89: return d; paulo@89: }); paulo@89: paulo@89: regWrap.enter().append('g').attr('class', 'nv-regLines'); paulo@89: paulo@89: var regLine = regWrap.selectAll('.nv-regLine') paulo@89: .data(function (d) { paulo@89: return [d] paulo@89: }); paulo@89: paulo@89: regLine.enter() paulo@89: .append('line').attr('class', 'nv-regLine') paulo@89: .style('stroke-opacity', 0); paulo@89: paulo@89: // don't add lines unless we have slope and intercept to use paulo@89: regLine.filter(function(d) { paulo@89: return d.intercept && d.slope; paulo@89: }) paulo@89: .watchTransition(renderWatch, 'scatterPlusLineChart: regline') paulo@89: .attr('x1', x.range()[0]) paulo@89: .attr('x2', x.range()[1]) paulo@89: .attr('y1', function (d, i) { paulo@89: return y(x.domain()[0] * d.slope + d.intercept) paulo@89: }) paulo@89: .attr('y2', function (d, i) { paulo@89: return y(x.domain()[1] * d.slope + d.intercept) paulo@89: }) paulo@89: .style('stroke', function (d, i, j) { paulo@89: return color(d, j) paulo@89: }) paulo@89: .style('stroke-opacity', function (d, i) { paulo@89: return (d.disabled || typeof d.slope === 'undefined' || typeof d.intercept === 'undefined') ? 0 : 1 paulo@89: }); paulo@89: paulo@89: // Setup Axes paulo@89: if (showXAxis) { paulo@89: xAxis paulo@89: .scale(x) paulo@89: ._ticks( nv.utils.calcTicksX(availableWidth/100, data) ) paulo@89: .tickSize( -availableHeight , 0); paulo@89: paulo@89: g.select('.nv-x.nv-axis') paulo@89: .attr('transform', 'translate(0,' + y.range()[0] + ')') paulo@89: .call(xAxis); paulo@89: } paulo@89: paulo@89: if (showYAxis) { paulo@89: yAxis paulo@89: .scale(y) paulo@89: ._ticks( nv.utils.calcTicksY(availableHeight/36, data) ) paulo@89: .tickSize( -availableWidth, 0); paulo@89: paulo@89: g.select('.nv-y.nv-axis') paulo@89: .call(yAxis); paulo@89: } paulo@89: paulo@89: paulo@89: if (showDistX) { paulo@89: distX paulo@89: .getData(scatter.x()) paulo@89: .scale(x) paulo@89: .width(availableWidth) paulo@89: .color(data.map(function(d,i) { paulo@89: return d.color || color(d, i); paulo@89: }).filter(function(d,i) { return !data[i].disabled })); paulo@89: gEnter.select('.nv-distWrap').append('g') paulo@89: .attr('class', 'nv-distributionX'); paulo@89: g.select('.nv-distributionX') paulo@89: .attr('transform', 'translate(0,' + y.range()[0] + ')') paulo@89: .datum(data.filter(function(d) { return !d.disabled })) paulo@89: .call(distX); paulo@89: } paulo@89: paulo@89: if (showDistY) { paulo@89: distY paulo@89: .getData(scatter.y()) paulo@89: .scale(y) paulo@89: .width(availableHeight) paulo@89: .color(data.map(function(d,i) { paulo@89: return d.color || color(d, i); paulo@89: }).filter(function(d,i) { return !data[i].disabled })); paulo@89: gEnter.select('.nv-distWrap').append('g') paulo@89: .attr('class', 'nv-distributionY'); paulo@89: g.select('.nv-distributionY') paulo@89: .attr('transform', 'translate(' + (rightAlignYAxis ? availableWidth : -distY.size() ) + ',0)') paulo@89: .datum(data.filter(function(d) { return !d.disabled })) paulo@89: .call(distY); paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (in chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: legend.dispatch.on('stateChange', function(newState) { paulo@89: for (var key in newState) paulo@89: state[key] = newState[key]; paulo@89: dispatch.stateChange(state); paulo@89: chart.update(); paulo@89: }); paulo@89: paulo@89: // Update chart from a state object passed to event handler paulo@89: dispatch.on('changeState', function(e) { paulo@89: if (typeof e.disabled !== 'undefined') { paulo@89: data.forEach(function(series,i) { paulo@89: series.disabled = e.disabled[i]; paulo@89: }); paulo@89: state.disabled = e.disabled; paulo@89: } paulo@89: chart.update(); paulo@89: }); paulo@89: paulo@89: // mouseover needs availableHeight so we just keep scatter mouse events inside the chart block paulo@89: scatter.dispatch.on('elementMouseout.tooltip', function(evt) { paulo@89: tooltip.hidden(true); paulo@89: container.select('.nv-chart-' + scatter.id() + ' .nv-series-' + evt.seriesIndex + ' .nv-distx-' + evt.pointIndex) paulo@89: .attr('y1', 0); paulo@89: container.select('.nv-chart-' + scatter.id() + ' .nv-series-' + evt.seriesIndex + ' .nv-disty-' + evt.pointIndex) paulo@89: .attr('x2', distY.size()); paulo@89: }); paulo@89: paulo@89: scatter.dispatch.on('elementMouseover.tooltip', function(evt) { paulo@89: container.select('.nv-series-' + evt.seriesIndex + ' .nv-distx-' + evt.pointIndex) paulo@89: .attr('y1', evt.pos.top - availableHeight - margin.top); paulo@89: container.select('.nv-series-' + evt.seriesIndex + ' .nv-disty-' + evt.pointIndex) paulo@89: .attr('x2', evt.pos.left + distX.size() - margin.left); paulo@89: tooltip.position(evt.pos).data(evt).hidden(false); paulo@89: }); paulo@89: paulo@89: //store old scales for use in transitions on update paulo@89: x0 = x.copy(); paulo@89: y0 = y.copy(); paulo@89: paulo@89: }); paulo@89: paulo@89: renderWatch.renderEnd('scatter with line immediate'); paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: // expose chart's sub-components paulo@89: chart.dispatch = dispatch; paulo@89: chart.scatter = scatter; paulo@89: chart.legend = legend; paulo@89: chart.xAxis = xAxis; paulo@89: chart.yAxis = yAxis; paulo@89: chart.distX = distX; paulo@89: chart.distY = distY; paulo@89: chart.tooltip = tooltip; paulo@89: paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: container: {get: function(){return container;}, set: function(_){container=_;}}, paulo@89: showDistX: {get: function(){return showDistX;}, set: function(_){showDistX=_;}}, paulo@89: showDistY: {get: function(){return showDistY;}, set: function(_){showDistY=_;}}, paulo@89: showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}}, paulo@89: showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}}, paulo@89: showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}}, paulo@89: defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}}, paulo@89: noData: {get: function(){return noData;}, set: function(_){noData=_;}}, paulo@89: duration: {get: function(){return duration;}, set: function(_){duration=_;}}, paulo@89: paulo@89: // deprecated options paulo@89: tooltips: {get: function(){return tooltip.enabled();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead'); paulo@89: tooltip.enabled(!!_); paulo@89: }}, paulo@89: tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead'); paulo@89: tooltip.contentGenerator(_); paulo@89: }}, paulo@89: tooltipXContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltipContent', 'This option is removed, put values into main tooltip.'); paulo@89: }}, paulo@89: tooltipYContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltipContent', 'This option is removed, put values into main tooltip.'); paulo@89: }}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){ paulo@89: rightAlignYAxis = _; paulo@89: yAxis.orient( (_) ? 'right' : 'left'); paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: legend.color(color); paulo@89: distX.color(color); paulo@89: distY.color(color); paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.inheritOptions(chart, scatter); paulo@89: nv.utils.initOptions(chart); paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: nv.models.sparkline = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var margin = {top: 2, right: 0, bottom: 2, left: 0} paulo@89: , width = 400 paulo@89: , height = 32 paulo@89: , container = null paulo@89: , animate = true paulo@89: , x = d3.scale.linear() paulo@89: , y = d3.scale.linear() paulo@89: , getX = function(d) { return d.x } paulo@89: , getY = function(d) { return d.y } paulo@89: , color = nv.utils.getColor(['#000']) paulo@89: , xDomain paulo@89: , yDomain paulo@89: , xRange paulo@89: , yRange paulo@89: ; paulo@89: paulo@89: function chart(selection) { paulo@89: selection.each(function(data) { paulo@89: var availableWidth = width - margin.left - margin.right, paulo@89: availableHeight = height - margin.top - margin.bottom; paulo@89: paulo@89: container = d3.select(this); paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: // Setup Scales paulo@89: x .domain(xDomain || d3.extent(data, getX )) paulo@89: .range(xRange || [0, availableWidth]); paulo@89: paulo@89: y .domain(yDomain || d3.extent(data, getY )) paulo@89: .range(yRange || [availableHeight, 0]); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-sparkline').data([data]); paulo@89: var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-sparkline'); paulo@89: var gEnter = wrapEnter.append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') paulo@89: paulo@89: var paths = wrap.selectAll('path') paulo@89: .data(function(d) { return [d] }); paulo@89: paths.enter().append('path'); paulo@89: paths.exit().remove(); paulo@89: paths paulo@89: .style('stroke', function(d,i) { return d.color || color(d, i) }) paulo@89: .attr('d', d3.svg.line() paulo@89: .x(function(d,i) { return x(getX(d,i)) }) paulo@89: .y(function(d,i) { return y(getY(d,i)) }) paulo@89: ); paulo@89: paulo@89: // TODO: Add CURRENT data point (Need Min, Mac, Current / Most recent) paulo@89: var points = wrap.selectAll('circle.nv-point') paulo@89: .data(function(data) { paulo@89: var yValues = data.map(function(d, i) { return getY(d,i); }); paulo@89: function pointIndex(index) { paulo@89: if (index != -1) { paulo@89: var result = data[index]; paulo@89: result.pointIndex = index; paulo@89: return result; paulo@89: } else { paulo@89: return null; paulo@89: } paulo@89: } paulo@89: var maxPoint = pointIndex(yValues.lastIndexOf(y.domain()[1])), paulo@89: minPoint = pointIndex(yValues.indexOf(y.domain()[0])), paulo@89: currentPoint = pointIndex(yValues.length - 1); paulo@89: return [minPoint, maxPoint, currentPoint].filter(function (d) {return d != null;}); paulo@89: }); paulo@89: points.enter().append('circle'); paulo@89: points.exit().remove(); paulo@89: points paulo@89: .attr('cx', function(d,i) { return x(getX(d,d.pointIndex)) }) paulo@89: .attr('cy', function(d,i) { return y(getY(d,d.pointIndex)) }) paulo@89: .attr('r', 2) paulo@89: .attr('class', function(d,i) { paulo@89: return getX(d, d.pointIndex) == x.domain()[1] ? 'nv-point nv-currentValue' : paulo@89: getY(d, d.pointIndex) == y.domain()[0] ? 'nv-point nv-minValue' : 'nv-point nv-maxValue' paulo@89: }); paulo@89: }); paulo@89: paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}}, paulo@89: yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}}, paulo@89: xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}}, paulo@89: yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}}, paulo@89: xScale: {get: function(){return x;}, set: function(_){x=_;}}, paulo@89: yScale: {get: function(){return y;}, set: function(_){y=_;}}, paulo@89: animate: {get: function(){return animate;}, set: function(_){animate=_;}}, paulo@89: paulo@89: //functor options paulo@89: x: {get: function(){return getX;}, set: function(_){getX=d3.functor(_);}}, paulo@89: y: {get: function(){return getY;}, set: function(_){getY=d3.functor(_);}}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.initOptions(chart); paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: nv.models.sparklinePlus = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var sparkline = nv.models.sparkline(); paulo@89: paulo@89: var margin = {top: 15, right: 100, bottom: 10, left: 50} paulo@89: , width = null paulo@89: , height = null paulo@89: , x paulo@89: , y paulo@89: , index = [] paulo@89: , paused = false paulo@89: , xTickFormat = d3.format(',r') paulo@89: , yTickFormat = d3.format(',.2f') paulo@89: , showLastValue = true paulo@89: , alignValue = true paulo@89: , rightAlignValue = false paulo@89: , noData = null paulo@89: ; paulo@89: paulo@89: function chart(selection) { paulo@89: selection.each(function(data) { paulo@89: var container = d3.select(this); paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: var availableWidth = nv.utils.availableWidth(width, container, margin), paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: paulo@89: chart.update = function() { container.call(chart); }; paulo@89: chart.container = this; paulo@89: paulo@89: // Display No Data message if there's nothing to show. paulo@89: if (!data || !data.length) { paulo@89: nv.utils.noData(chart, container) paulo@89: return chart; paulo@89: } else { paulo@89: container.selectAll('.nv-noData').remove(); paulo@89: } paulo@89: paulo@89: var currentValue = sparkline.y()(data[data.length-1], data.length-1); paulo@89: paulo@89: // Setup Scales paulo@89: x = sparkline.xScale(); paulo@89: y = sparkline.yScale(); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-sparklineplus').data([data]); paulo@89: var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-sparklineplus'); paulo@89: var gEnter = wrapEnter.append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-sparklineWrap'); paulo@89: gEnter.append('g').attr('class', 'nv-valueWrap'); paulo@89: gEnter.append('g').attr('class', 'nv-hoverArea'); paulo@89: paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: // Main Chart Component(s) paulo@89: var sparklineWrap = g.select('.nv-sparklineWrap'); paulo@89: paulo@89: sparkline.width(availableWidth).height(availableHeight); paulo@89: sparklineWrap.call(sparkline); paulo@89: paulo@89: if (showLastValue) { paulo@89: var valueWrap = g.select('.nv-valueWrap'); paulo@89: var value = valueWrap.selectAll('.nv-currentValue') paulo@89: .data([currentValue]); paulo@89: paulo@89: value.enter().append('text').attr('class', 'nv-currentValue') paulo@89: .attr('dx', rightAlignValue ? -8 : 8) paulo@89: .attr('dy', '.9em') paulo@89: .style('text-anchor', rightAlignValue ? 'end' : 'start'); paulo@89: paulo@89: value paulo@89: .attr('x', availableWidth + (rightAlignValue ? margin.right : 0)) paulo@89: .attr('y', alignValue ? function (d) { paulo@89: return y(d) paulo@89: } : 0) paulo@89: .style('fill', sparkline.color()(data[data.length - 1], data.length - 1)) paulo@89: .text(yTickFormat(currentValue)); paulo@89: } paulo@89: paulo@89: gEnter.select('.nv-hoverArea').append('rect') paulo@89: .on('mousemove', sparklineHover) paulo@89: .on('click', function() { paused = !paused }) paulo@89: .on('mouseout', function() { index = []; updateValueLine(); }); paulo@89: paulo@89: g.select('.nv-hoverArea rect') paulo@89: .attr('transform', function(d) { return 'translate(' + -margin.left + ',' + -margin.top + ')' }) paulo@89: .attr('width', availableWidth + margin.left + margin.right) paulo@89: .attr('height', availableHeight + margin.top); paulo@89: paulo@89: //index is currently global (within the chart), may or may not keep it that way paulo@89: function updateValueLine() { paulo@89: if (paused) return; paulo@89: paulo@89: var hoverValue = g.selectAll('.nv-hoverValue').data(index); paulo@89: paulo@89: var hoverEnter = hoverValue.enter() paulo@89: .append('g').attr('class', 'nv-hoverValue') paulo@89: .style('stroke-opacity', 0) paulo@89: .style('fill-opacity', 0); paulo@89: paulo@89: hoverValue.exit() paulo@89: .transition().duration(250) paulo@89: .style('stroke-opacity', 0) paulo@89: .style('fill-opacity', 0) paulo@89: .remove(); paulo@89: paulo@89: hoverValue paulo@89: .attr('transform', function(d) { return 'translate(' + x(sparkline.x()(data[d],d)) + ',0)' }) paulo@89: .transition().duration(250) paulo@89: .style('stroke-opacity', 1) paulo@89: .style('fill-opacity', 1); paulo@89: paulo@89: if (!index.length) return; paulo@89: paulo@89: hoverEnter.append('line') paulo@89: .attr('x1', 0) paulo@89: .attr('y1', -margin.top) paulo@89: .attr('x2', 0) paulo@89: .attr('y2', availableHeight); paulo@89: paulo@89: hoverEnter.append('text').attr('class', 'nv-xValue') paulo@89: .attr('x', -6) paulo@89: .attr('y', -margin.top) paulo@89: .attr('text-anchor', 'end') paulo@89: .attr('dy', '.9em'); paulo@89: paulo@89: g.select('.nv-hoverValue .nv-xValue') paulo@89: .text(xTickFormat(sparkline.x()(data[index[0]], index[0]))); paulo@89: paulo@89: hoverEnter.append('text').attr('class', 'nv-yValue') paulo@89: .attr('x', 6) paulo@89: .attr('y', -margin.top) paulo@89: .attr('text-anchor', 'start') paulo@89: .attr('dy', '.9em'); paulo@89: paulo@89: g.select('.nv-hoverValue .nv-yValue') paulo@89: .text(yTickFormat(sparkline.y()(data[index[0]], index[0]))); paulo@89: } paulo@89: paulo@89: function sparklineHover() { paulo@89: if (paused) return; paulo@89: paulo@89: var pos = d3.mouse(this)[0] - margin.left; paulo@89: paulo@89: function getClosestIndex(data, x) { paulo@89: var distance = Math.abs(sparkline.x()(data[0], 0) - x); paulo@89: var closestIndex = 0; paulo@89: for (var i = 0; i < data.length; i++){ paulo@89: if (Math.abs(sparkline.x()(data[i], i) - x) < distance) { paulo@89: distance = Math.abs(sparkline.x()(data[i], i) - x); paulo@89: closestIndex = i; paulo@89: } paulo@89: } paulo@89: return closestIndex; paulo@89: } paulo@89: paulo@89: index = [getClosestIndex(data, Math.round(x.invert(pos)))]; paulo@89: updateValueLine(); paulo@89: } paulo@89: paulo@89: }); paulo@89: paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: // expose chart's sub-components paulo@89: chart.sparkline = sparkline; paulo@89: paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: xTickFormat: {get: function(){return xTickFormat;}, set: function(_){xTickFormat=_;}}, paulo@89: yTickFormat: {get: function(){return yTickFormat;}, set: function(_){yTickFormat=_;}}, paulo@89: showLastValue: {get: function(){return showLastValue;}, set: function(_){showLastValue=_;}}, paulo@89: alignValue: {get: function(){return alignValue;}, set: function(_){alignValue=_;}}, paulo@89: rightAlignValue: {get: function(){return rightAlignValue;}, set: function(_){rightAlignValue=_;}}, paulo@89: noData: {get: function(){return noData;}, set: function(_){noData=_;}}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.inheritOptions(chart, sparkline); paulo@89: nv.utils.initOptions(chart); paulo@89: paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: nv.models.stackedArea = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var margin = {top: 0, right: 0, bottom: 0, left: 0} paulo@89: , width = 960 paulo@89: , height = 500 paulo@89: , color = nv.utils.defaultColor() // a function that computes the color paulo@89: , id = Math.floor(Math.random() * 100000) //Create semi-unique ID incase user doesn't selet one paulo@89: , container = null paulo@89: , getX = function(d) { return d.x } // accessor to get the x value from a data point paulo@89: , getY = function(d) { return d.y } // accessor to get the y value from a data point paulo@89: , style = 'stack' paulo@89: , offset = 'zero' paulo@89: , order = 'default' paulo@89: , interpolate = 'linear' // controls the line interpolation paulo@89: , clipEdge = false // if true, masks lines within x and y scale paulo@89: , x //can be accessed via chart.xScale() paulo@89: , y //can be accessed via chart.yScale() paulo@89: , scatter = nv.models.scatter() paulo@89: , duration = 250 paulo@89: , dispatch = d3.dispatch('areaClick', 'areaMouseover', 'areaMouseout','renderEnd', 'elementClick', 'elementMouseover', 'elementMouseout') paulo@89: ; paulo@89: paulo@89: scatter paulo@89: .pointSize(2.2) // default size paulo@89: .pointDomain([2.2, 2.2]) // all the same size by default paulo@89: ; paulo@89: paulo@89: /************************************ paulo@89: * offset: paulo@89: * 'wiggle' (stream) paulo@89: * 'zero' (stacked) paulo@89: * 'expand' (normalize to 100%) paulo@89: * 'silhouette' (simple centered) paulo@89: * paulo@89: * order: paulo@89: * 'inside-out' (stream) paulo@89: * 'default' (input order) paulo@89: ************************************/ paulo@89: paulo@89: var renderWatch = nv.utils.renderWatch(dispatch, duration); paulo@89: paulo@89: function chart(selection) { paulo@89: renderWatch.reset(); paulo@89: renderWatch.models(scatter); paulo@89: selection.each(function(data) { paulo@89: var availableWidth = width - margin.left - margin.right, paulo@89: availableHeight = height - margin.top - margin.bottom; paulo@89: paulo@89: container = d3.select(this); paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: // Setup Scales paulo@89: x = scatter.xScale(); paulo@89: y = scatter.yScale(); paulo@89: paulo@89: var dataRaw = data; paulo@89: // Injecting point index into each point because d3.layout.stack().out does not give index paulo@89: data.forEach(function(aseries, i) { paulo@89: aseries.seriesIndex = i; paulo@89: aseries.values = aseries.values.map(function(d, j) { paulo@89: d.index = j; paulo@89: d.seriesIndex = i; paulo@89: return d; paulo@89: }); paulo@89: }); paulo@89: paulo@89: var dataFiltered = data.filter(function(series) { paulo@89: return !series.disabled; paulo@89: }); paulo@89: paulo@89: data = d3.layout.stack() paulo@89: .order(order) paulo@89: .offset(offset) paulo@89: .values(function(d) { return d.values }) //TODO: make values customizeable in EVERY model in this fashion paulo@89: .x(getX) paulo@89: .y(getY) paulo@89: .out(function(d, y0, y) { paulo@89: d.display = { paulo@89: y: y, paulo@89: y0: y0 paulo@89: }; paulo@89: }) paulo@89: (dataFiltered); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-stackedarea').data([data]); paulo@89: var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-stackedarea'); paulo@89: var defsEnter = wrapEnter.append('defs'); paulo@89: var gEnter = wrapEnter.append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-areaWrap'); paulo@89: gEnter.append('g').attr('class', 'nv-scatterWrap'); paulo@89: paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: // If the user has not specified forceY, make sure 0 is included in the domain paulo@89: // Otherwise, use user-specified values for forceY paulo@89: if (scatter.forceY().length == 0) { paulo@89: scatter.forceY().push(0); paulo@89: } paulo@89: paulo@89: scatter paulo@89: .width(availableWidth) paulo@89: .height(availableHeight) paulo@89: .x(getX) paulo@89: .y(function(d) { return d.display.y + d.display.y0 }) paulo@89: .forceY([0]) paulo@89: .color(data.map(function(d,i) { paulo@89: return d.color || color(d, d.seriesIndex); paulo@89: })); paulo@89: paulo@89: var scatterWrap = g.select('.nv-scatterWrap') paulo@89: .datum(data); paulo@89: paulo@89: scatterWrap.call(scatter); paulo@89: paulo@89: defsEnter.append('clipPath') paulo@89: .attr('id', 'nv-edge-clip-' + id) paulo@89: .append('rect'); paulo@89: paulo@89: wrap.select('#nv-edge-clip-' + id + ' rect') paulo@89: .attr('width', availableWidth) paulo@89: .attr('height', availableHeight); paulo@89: paulo@89: g.attr('clip-path', clipEdge ? 'url(#nv-edge-clip-' + id + ')' : ''); paulo@89: paulo@89: var area = d3.svg.area() paulo@89: .x(function(d,i) { return x(getX(d,i)) }) paulo@89: .y0(function(d) { paulo@89: return y(d.display.y0) paulo@89: }) paulo@89: .y1(function(d) { paulo@89: return y(d.display.y + d.display.y0) paulo@89: }) paulo@89: .interpolate(interpolate); paulo@89: paulo@89: var zeroArea = d3.svg.area() paulo@89: .x(function(d,i) { return x(getX(d,i)) }) paulo@89: .y0(function(d) { return y(d.display.y0) }) paulo@89: .y1(function(d) { return y(d.display.y0) }); paulo@89: paulo@89: var path = g.select('.nv-areaWrap').selectAll('path.nv-area') paulo@89: .data(function(d) { return d }); paulo@89: paulo@89: path.enter().append('path').attr('class', function(d,i) { return 'nv-area nv-area-' + i }) paulo@89: .attr('d', function(d,i){ paulo@89: return zeroArea(d.values, d.seriesIndex); paulo@89: }) paulo@89: .on('mouseover', function(d,i) { paulo@89: d3.select(this).classed('hover', true); paulo@89: dispatch.areaMouseover({ paulo@89: point: d, paulo@89: series: d.key, paulo@89: pos: [d3.event.pageX, d3.event.pageY], paulo@89: seriesIndex: d.seriesIndex paulo@89: }); paulo@89: }) paulo@89: .on('mouseout', function(d,i) { paulo@89: d3.select(this).classed('hover', false); paulo@89: dispatch.areaMouseout({ paulo@89: point: d, paulo@89: series: d.key, paulo@89: pos: [d3.event.pageX, d3.event.pageY], paulo@89: seriesIndex: d.seriesIndex paulo@89: }); paulo@89: }) paulo@89: .on('click', function(d,i) { paulo@89: d3.select(this).classed('hover', false); paulo@89: dispatch.areaClick({ paulo@89: point: d, paulo@89: series: d.key, paulo@89: pos: [d3.event.pageX, d3.event.pageY], paulo@89: seriesIndex: d.seriesIndex paulo@89: }); paulo@89: }); paulo@89: paulo@89: path.exit().remove(); paulo@89: path.style('fill', function(d,i){ paulo@89: return d.color || color(d, d.seriesIndex) paulo@89: }) paulo@89: .style('stroke', function(d,i){ return d.color || color(d, d.seriesIndex) }); paulo@89: path.watchTransition(renderWatch,'stackedArea path') paulo@89: .attr('d', function(d,i) { paulo@89: return area(d.values,i) paulo@89: }); paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (in chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: scatter.dispatch.on('elementMouseover.area', function(e) { paulo@89: g.select('.nv-chart-' + id + ' .nv-area-' + e.seriesIndex).classed('hover', true); paulo@89: }); paulo@89: scatter.dispatch.on('elementMouseout.area', function(e) { paulo@89: g.select('.nv-chart-' + id + ' .nv-area-' + e.seriesIndex).classed('hover', false); paulo@89: }); paulo@89: paulo@89: //Special offset functions paulo@89: chart.d3_stackedOffset_stackPercent = function(stackData) { paulo@89: var n = stackData.length, //How many series paulo@89: m = stackData[0].length, //how many points per series paulo@89: i, paulo@89: j, paulo@89: o, paulo@89: y0 = []; paulo@89: paulo@89: for (j = 0; j < m; ++j) { //Looping through all points paulo@89: for (i = 0, o = 0; i < dataRaw.length; i++) { //looping through all series paulo@89: o += getY(dataRaw[i].values[j]); //total y value of all series at a certian point in time. paulo@89: } paulo@89: paulo@89: if (o) for (i = 0; i < n; i++) { //(total y value of all series at point in time i) != 0 paulo@89: stackData[i][j][1] /= o; paulo@89: } else { //(total y value of all series at point in time i) == 0 paulo@89: for (i = 0; i < n; i++) { paulo@89: stackData[i][j][1] = 0; paulo@89: } paulo@89: } paulo@89: } paulo@89: for (j = 0; j < m; ++j) y0[j] = 0; paulo@89: return y0; paulo@89: }; paulo@89: paulo@89: }); paulo@89: paulo@89: renderWatch.renderEnd('stackedArea immediate'); paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Global getters and setters paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: chart.dispatch = dispatch; paulo@89: chart.scatter = scatter; paulo@89: paulo@89: scatter.dispatch.on('elementClick', function(){ dispatch.elementClick.apply(this, arguments); }); paulo@89: scatter.dispatch.on('elementMouseover', function(){ dispatch.elementMouseover.apply(this, arguments); }); paulo@89: scatter.dispatch.on('elementMouseout', function(){ dispatch.elementMouseout.apply(this, arguments); }); paulo@89: paulo@89: chart.interpolate = function(_) { paulo@89: if (!arguments.length) return interpolate; paulo@89: interpolate = _; paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: chart.duration = function(_) { paulo@89: if (!arguments.length) return duration; paulo@89: duration = _; paulo@89: renderWatch.reset(duration); paulo@89: scatter.duration(duration); paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: chart.dispatch = dispatch; paulo@89: chart.scatter = scatter; paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: clipEdge: {get: function(){return clipEdge;}, set: function(_){clipEdge=_;}}, paulo@89: offset: {get: function(){return offset;}, set: function(_){offset=_;}}, paulo@89: order: {get: function(){return order;}, set: function(_){order=_;}}, paulo@89: interpolate: {get: function(){return interpolate;}, set: function(_){interpolate=_;}}, paulo@89: paulo@89: // simple functor options paulo@89: x: {get: function(){return getX;}, set: function(_){getX = d3.functor(_);}}, paulo@89: y: {get: function(){return getY;}, set: function(_){getY = d3.functor(_);}}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: }}, paulo@89: style: {get: function(){return style;}, set: function(_){ paulo@89: style = _; paulo@89: switch (style) { paulo@89: case 'stack': paulo@89: chart.offset('zero'); paulo@89: chart.order('default'); paulo@89: break; paulo@89: case 'stream': paulo@89: chart.offset('wiggle'); paulo@89: chart.order('inside-out'); paulo@89: break; paulo@89: case 'stream-center': paulo@89: chart.offset('silhouette'); paulo@89: chart.order('inside-out'); paulo@89: break; paulo@89: case 'expand': paulo@89: chart.offset('expand'); paulo@89: chart.order('default'); paulo@89: break; paulo@89: case 'stack_percent': paulo@89: chart.offset(chart.d3_stackedOffset_stackPercent); paulo@89: chart.order('default'); paulo@89: break; paulo@89: } paulo@89: }}, paulo@89: duration: {get: function(){return duration;}, set: function(_){ paulo@89: duration = _; paulo@89: renderWatch.reset(duration); paulo@89: scatter.duration(duration); paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.inheritOptions(chart, scatter); paulo@89: nv.utils.initOptions(chart); paulo@89: paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: nv.models.stackedAreaChart = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var stacked = nv.models.stackedArea() paulo@89: , xAxis = nv.models.axis() paulo@89: , yAxis = nv.models.axis() paulo@89: , legend = nv.models.legend() paulo@89: , controls = nv.models.legend() paulo@89: , interactiveLayer = nv.interactiveGuideline() paulo@89: , tooltip = nv.models.tooltip() paulo@89: ; paulo@89: paulo@89: var margin = {top: 30, right: 25, bottom: 50, left: 60} paulo@89: , width = null paulo@89: , height = null paulo@89: , color = nv.utils.defaultColor() paulo@89: , showControls = true paulo@89: , showLegend = true paulo@89: , showXAxis = true paulo@89: , showYAxis = true paulo@89: , rightAlignYAxis = false paulo@89: , useInteractiveGuideline = false paulo@89: , x //can be accessed via chart.xScale() paulo@89: , y //can be accessed via chart.yScale() paulo@89: , state = nv.utils.state() paulo@89: , defaultState = null paulo@89: , noData = null paulo@89: , dispatch = d3.dispatch('stateChange', 'changeState','renderEnd') paulo@89: , controlWidth = 250 paulo@89: , controlOptions = ['Stacked','Stream','Expanded'] paulo@89: , controlLabels = {} paulo@89: , duration = 250 paulo@89: ; paulo@89: paulo@89: state.style = stacked.style(); paulo@89: xAxis.orient('bottom').tickPadding(7); paulo@89: yAxis.orient((rightAlignYAxis) ? 'right' : 'left'); paulo@89: paulo@89: tooltip paulo@89: .headerFormatter(function(d, i) { paulo@89: return xAxis.tickFormat()(d, i); paulo@89: }) paulo@89: .valueFormatter(function(d, i) { paulo@89: return yAxis.tickFormat()(d, i); paulo@89: }); paulo@89: paulo@89: interactiveLayer.tooltip paulo@89: .headerFormatter(function(d, i) { paulo@89: return xAxis.tickFormat()(d, i); paulo@89: }) paulo@89: .valueFormatter(function(d, i) { paulo@89: return yAxis.tickFormat()(d, i); paulo@89: }); paulo@89: paulo@89: var oldYTickFormat = null, paulo@89: oldValueFormatter = null; paulo@89: paulo@89: controls.updateState(false); paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var renderWatch = nv.utils.renderWatch(dispatch); paulo@89: var style = stacked.style(); paulo@89: paulo@89: var stateGetter = function(data) { paulo@89: return function(){ paulo@89: return { paulo@89: active: data.map(function(d) { return !d.disabled }), paulo@89: style: stacked.style() paulo@89: }; paulo@89: } paulo@89: }; paulo@89: paulo@89: var stateSetter = function(data) { paulo@89: return function(state) { paulo@89: if (state.style !== undefined) paulo@89: style = state.style; paulo@89: if (state.active !== undefined) paulo@89: data.forEach(function(series,i) { paulo@89: series.disabled = !state.active[i]; paulo@89: }); paulo@89: } paulo@89: }; paulo@89: paulo@89: var percentFormatter = d3.format('%'); paulo@89: paulo@89: function chart(selection) { paulo@89: renderWatch.reset(); paulo@89: renderWatch.models(stacked); paulo@89: if (showXAxis) renderWatch.models(xAxis); paulo@89: if (showYAxis) renderWatch.models(yAxis); paulo@89: paulo@89: selection.each(function(data) { paulo@89: var container = d3.select(this), paulo@89: that = this; paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: var availableWidth = nv.utils.availableWidth(width, container, margin), paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: paulo@89: chart.update = function() { container.transition().duration(duration).call(chart); }; paulo@89: chart.container = this; paulo@89: paulo@89: state paulo@89: .setter(stateSetter(data), chart.update) paulo@89: .getter(stateGetter(data)) paulo@89: .update(); paulo@89: paulo@89: // DEPRECATED set state.disabled paulo@89: state.disabled = data.map(function(d) { return !!d.disabled }); paulo@89: paulo@89: if (!defaultState) { paulo@89: var key; paulo@89: defaultState = {}; paulo@89: for (key in state) { paulo@89: if (state[key] instanceof Array) paulo@89: defaultState[key] = state[key].slice(0); paulo@89: else paulo@89: defaultState[key] = state[key]; paulo@89: } paulo@89: } paulo@89: paulo@89: // Display No Data message if there's nothing to show. paulo@89: if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) { paulo@89: nv.utils.noData(chart, container) paulo@89: return chart; paulo@89: } else { paulo@89: container.selectAll('.nv-noData').remove(); paulo@89: } paulo@89: paulo@89: // Setup Scales paulo@89: x = stacked.xScale(); paulo@89: y = stacked.yScale(); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-stackedAreaChart').data([data]); paulo@89: var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-stackedAreaChart').append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append("rect").style("opacity",0); paulo@89: gEnter.append('g').attr('class', 'nv-x nv-axis'); paulo@89: gEnter.append('g').attr('class', 'nv-y nv-axis'); paulo@89: gEnter.append('g').attr('class', 'nv-stackedWrap'); paulo@89: gEnter.append('g').attr('class', 'nv-legendWrap'); paulo@89: gEnter.append('g').attr('class', 'nv-controlsWrap'); paulo@89: gEnter.append('g').attr('class', 'nv-interactive'); paulo@89: paulo@89: g.select("rect").attr("width",availableWidth).attr("height",availableHeight); paulo@89: paulo@89: // Legend paulo@89: if (showLegend) { paulo@89: var legendWidth = (showControls) ? availableWidth - controlWidth : availableWidth; paulo@89: paulo@89: legend.width(legendWidth); paulo@89: g.select('.nv-legendWrap').datum(data).call(legend); paulo@89: paulo@89: if ( margin.top != legend.height()) { paulo@89: margin.top = legend.height(); paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: } paulo@89: paulo@89: g.select('.nv-legendWrap') paulo@89: .attr('transform', 'translate(' + (availableWidth-legendWidth) + ',' + (-margin.top) +')'); paulo@89: } paulo@89: paulo@89: // Controls paulo@89: if (showControls) { paulo@89: var controlsData = [ paulo@89: { paulo@89: key: controlLabels.stacked || 'Stacked', paulo@89: metaKey: 'Stacked', paulo@89: disabled: stacked.style() != 'stack', paulo@89: style: 'stack' paulo@89: }, paulo@89: { paulo@89: key: controlLabels.stream || 'Stream', paulo@89: metaKey: 'Stream', paulo@89: disabled: stacked.style() != 'stream', paulo@89: style: 'stream' paulo@89: }, paulo@89: { paulo@89: key: controlLabels.expanded || 'Expanded', paulo@89: metaKey: 'Expanded', paulo@89: disabled: stacked.style() != 'expand', paulo@89: style: 'expand' paulo@89: }, paulo@89: { paulo@89: key: controlLabels.stack_percent || 'Stack %', paulo@89: metaKey: 'Stack_Percent', paulo@89: disabled: stacked.style() != 'stack_percent', paulo@89: style: 'stack_percent' paulo@89: } paulo@89: ]; paulo@89: paulo@89: controlWidth = (controlOptions.length/3) * 260; paulo@89: controlsData = controlsData.filter(function(d) { paulo@89: return controlOptions.indexOf(d.metaKey) !== -1; paulo@89: }); paulo@89: paulo@89: controls paulo@89: .width( controlWidth ) paulo@89: .color(['#444', '#444', '#444']); paulo@89: paulo@89: g.select('.nv-controlsWrap') paulo@89: .datum(controlsData) paulo@89: .call(controls); paulo@89: paulo@89: if ( margin.top != Math.max(controls.height(), legend.height()) ) { paulo@89: margin.top = Math.max(controls.height(), legend.height()); paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: } paulo@89: paulo@89: g.select('.nv-controlsWrap') paulo@89: .attr('transform', 'translate(0,' + (-margin.top) +')'); paulo@89: } paulo@89: paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: if (rightAlignYAxis) { paulo@89: g.select(".nv-y.nv-axis") paulo@89: .attr("transform", "translate(" + availableWidth + ",0)"); paulo@89: } paulo@89: paulo@89: //Set up interactive layer paulo@89: if (useInteractiveGuideline) { paulo@89: interactiveLayer paulo@89: .width(availableWidth) paulo@89: .height(availableHeight) paulo@89: .margin({left: margin.left, top: margin.top}) paulo@89: .svgContainer(container) paulo@89: .xScale(x); paulo@89: wrap.select(".nv-interactive").call(interactiveLayer); paulo@89: } paulo@89: paulo@89: stacked paulo@89: .width(availableWidth) paulo@89: .height(availableHeight); paulo@89: paulo@89: var stackedWrap = g.select('.nv-stackedWrap') paulo@89: .datum(data); paulo@89: paulo@89: stackedWrap.transition().call(stacked); paulo@89: paulo@89: // Setup Axes paulo@89: if (showXAxis) { paulo@89: xAxis.scale(x) paulo@89: ._ticks( nv.utils.calcTicksX(availableWidth/100, data) ) paulo@89: .tickSize( -availableHeight, 0); paulo@89: paulo@89: g.select('.nv-x.nv-axis') paulo@89: .attr('transform', 'translate(0,' + availableHeight + ')'); paulo@89: paulo@89: g.select('.nv-x.nv-axis') paulo@89: .transition().duration(0) paulo@89: .call(xAxis); paulo@89: } paulo@89: paulo@89: if (showYAxis) { paulo@89: var ticks; paulo@89: if (stacked.offset() === 'wiggle') { paulo@89: ticks = 0; paulo@89: } paulo@89: else { paulo@89: ticks = nv.utils.calcTicksY(availableHeight/36, data); paulo@89: } paulo@89: yAxis.scale(y) paulo@89: ._ticks(ticks) paulo@89: .tickSize(-availableWidth, 0); paulo@89: paulo@89: if (stacked.style() === 'expand' || stacked.style() === 'stack_percent') { paulo@89: var currentFormat = yAxis.tickFormat(); paulo@89: paulo@89: if ( !oldYTickFormat || currentFormat !== percentFormatter ) paulo@89: oldYTickFormat = currentFormat; paulo@89: paulo@89: //Forces the yAxis to use percentage in 'expand' mode. paulo@89: yAxis.tickFormat(percentFormatter); paulo@89: } paulo@89: else { paulo@89: if (oldYTickFormat) { paulo@89: yAxis.tickFormat(oldYTickFormat); paulo@89: oldYTickFormat = null; paulo@89: } paulo@89: } paulo@89: paulo@89: g.select('.nv-y.nv-axis') paulo@89: .transition().duration(0) paulo@89: .call(yAxis); paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (in chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: stacked.dispatch.on('areaClick.toggle', function(e) { paulo@89: if (data.filter(function(d) { return !d.disabled }).length === 1) paulo@89: data.forEach(function(d) { paulo@89: d.disabled = false; paulo@89: }); paulo@89: else paulo@89: data.forEach(function(d,i) { paulo@89: d.disabled = (i != e.seriesIndex); paulo@89: }); paulo@89: paulo@89: state.disabled = data.map(function(d) { return !!d.disabled }); paulo@89: dispatch.stateChange(state); paulo@89: paulo@89: chart.update(); paulo@89: }); paulo@89: paulo@89: legend.dispatch.on('stateChange', function(newState) { paulo@89: for (var key in newState) paulo@89: state[key] = newState[key]; paulo@89: dispatch.stateChange(state); paulo@89: chart.update(); paulo@89: }); paulo@89: paulo@89: controls.dispatch.on('legendClick', function(d,i) { paulo@89: if (!d.disabled) return; paulo@89: paulo@89: controlsData = controlsData.map(function(s) { paulo@89: s.disabled = true; paulo@89: return s; paulo@89: }); paulo@89: d.disabled = false; paulo@89: paulo@89: stacked.style(d.style); paulo@89: paulo@89: paulo@89: state.style = stacked.style(); paulo@89: dispatch.stateChange(state); paulo@89: paulo@89: chart.update(); paulo@89: }); paulo@89: paulo@89: interactiveLayer.dispatch.on('elementMousemove', function(e) { paulo@89: stacked.clearHighlights(); paulo@89: var singlePoint, pointIndex, pointXLocation, allData = []; paulo@89: data paulo@89: .filter(function(series, i) { paulo@89: series.seriesIndex = i; paulo@89: return !series.disabled; paulo@89: }) paulo@89: .forEach(function(series,i) { paulo@89: pointIndex = nv.interactiveBisect(series.values, e.pointXValue, chart.x()); paulo@89: var point = series.values[pointIndex]; paulo@89: var pointYValue = chart.y()(point, pointIndex); paulo@89: if (pointYValue != null) { paulo@89: stacked.highlightPoint(i, pointIndex, true); paulo@89: } paulo@89: if (typeof point === 'undefined') return; paulo@89: if (typeof singlePoint === 'undefined') singlePoint = point; paulo@89: if (typeof pointXLocation === 'undefined') pointXLocation = chart.xScale()(chart.x()(point,pointIndex)); paulo@89: paulo@89: //If we are in 'expand' mode, use the stacked percent value instead of raw value. paulo@89: var tooltipValue = (stacked.style() == 'expand') ? point.display.y : chart.y()(point,pointIndex); paulo@89: allData.push({ paulo@89: key: series.key, paulo@89: value: tooltipValue, paulo@89: color: color(series,series.seriesIndex), paulo@89: stackedValue: point.display paulo@89: }); paulo@89: }); paulo@89: paulo@89: allData.reverse(); paulo@89: paulo@89: //Highlight the tooltip entry based on which stack the mouse is closest to. paulo@89: if (allData.length > 2) { paulo@89: var yValue = chart.yScale().invert(e.mouseY); paulo@89: var yDistMax = Infinity, indexToHighlight = null; paulo@89: allData.forEach(function(series,i) { paulo@89: paulo@89: //To handle situation where the stacked area chart is negative, we need to use absolute values paulo@89: //when checking if the mouse Y value is within the stack area. paulo@89: yValue = Math.abs(yValue); paulo@89: var stackedY0 = Math.abs(series.stackedValue.y0); paulo@89: var stackedY = Math.abs(series.stackedValue.y); paulo@89: if ( yValue >= stackedY0 && yValue <= (stackedY + stackedY0)) paulo@89: { paulo@89: indexToHighlight = i; paulo@89: return; paulo@89: } paulo@89: }); paulo@89: if (indexToHighlight != null) paulo@89: allData[indexToHighlight].highlight = true; paulo@89: } paulo@89: paulo@89: var xValue = xAxis.tickFormat()(chart.x()(singlePoint,pointIndex)); paulo@89: paulo@89: var valueFormatter = interactiveLayer.tooltip.valueFormatter(); paulo@89: // Keeps track of the tooltip valueFormatter if the chart changes to expanded view paulo@89: if (stacked.style() === 'expand' || stacked.style() === 'stack_percent') { paulo@89: if ( !oldValueFormatter ) { paulo@89: oldValueFormatter = valueFormatter; paulo@89: } paulo@89: //Forces the tooltip to use percentage in 'expand' mode. paulo@89: valueFormatter = d3.format(".1%"); paulo@89: } paulo@89: else { paulo@89: if (oldValueFormatter) { paulo@89: valueFormatter = oldValueFormatter; paulo@89: oldValueFormatter = null; paulo@89: } paulo@89: } paulo@89: paulo@89: interactiveLayer.tooltip paulo@89: .position({left: pointXLocation + margin.left, top: e.mouseY + margin.top}) paulo@89: .chartContainer(that.parentNode) paulo@89: .valueFormatter(valueFormatter) paulo@89: .data( paulo@89: { paulo@89: value: xValue, paulo@89: series: allData paulo@89: } paulo@89: )(); paulo@89: paulo@89: interactiveLayer.renderGuideLine(pointXLocation); paulo@89: paulo@89: }); paulo@89: paulo@89: interactiveLayer.dispatch.on("elementMouseout",function(e) { paulo@89: stacked.clearHighlights(); paulo@89: }); paulo@89: paulo@89: // Update chart from a state object passed to event handler paulo@89: dispatch.on('changeState', function(e) { paulo@89: paulo@89: if (typeof e.disabled !== 'undefined' && data.length === e.disabled.length) { paulo@89: data.forEach(function(series,i) { paulo@89: series.disabled = e.disabled[i]; paulo@89: }); paulo@89: paulo@89: state.disabled = e.disabled; paulo@89: } paulo@89: paulo@89: if (typeof e.style !== 'undefined') { paulo@89: stacked.style(e.style); paulo@89: style = e.style; paulo@89: } paulo@89: paulo@89: chart.update(); paulo@89: }); paulo@89: paulo@89: }); paulo@89: paulo@89: renderWatch.renderEnd('stacked Area chart immediate'); paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (out of chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: stacked.dispatch.on('elementMouseover.tooltip', function(evt) { paulo@89: evt.point['x'] = stacked.x()(evt.point); paulo@89: evt.point['y'] = stacked.y()(evt.point); paulo@89: tooltip.data(evt).position(evt.pos).hidden(false); paulo@89: }); paulo@89: paulo@89: stacked.dispatch.on('elementMouseout.tooltip', function(evt) { paulo@89: tooltip.hidden(true) paulo@89: }); paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: // expose chart's sub-components paulo@89: chart.dispatch = dispatch; paulo@89: chart.stacked = stacked; paulo@89: chart.legend = legend; paulo@89: chart.controls = controls; paulo@89: chart.xAxis = xAxis; paulo@89: chart.yAxis = yAxis; paulo@89: chart.interactiveLayer = interactiveLayer; paulo@89: chart.tooltip = tooltip; paulo@89: paulo@89: chart.dispatch = dispatch; paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}}, paulo@89: showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}}, paulo@89: showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}}, paulo@89: defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}}, paulo@89: noData: {get: function(){return noData;}, set: function(_){noData=_;}}, paulo@89: showControls: {get: function(){return showControls;}, set: function(_){showControls=_;}}, paulo@89: controlLabels: {get: function(){return controlLabels;}, set: function(_){controlLabels=_;}}, paulo@89: controlOptions: {get: function(){return controlOptions;}, set: function(_){controlOptions=_;}}, paulo@89: paulo@89: // deprecated options paulo@89: tooltips: {get: function(){return tooltip.enabled();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltips', 'use chart.tooltip.enabled() instead'); paulo@89: tooltip.enabled(!!_); paulo@89: }}, paulo@89: tooltipContent: {get: function(){return tooltip.contentGenerator();}, set: function(_){ paulo@89: // deprecated after 1.7.1 paulo@89: nv.deprecated('tooltipContent', 'use chart.tooltip.contentGenerator() instead'); paulo@89: tooltip.contentGenerator(_); paulo@89: }}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }}, paulo@89: duration: {get: function(){return duration;}, set: function(_){ paulo@89: duration = _; paulo@89: renderWatch.reset(duration); paulo@89: stacked.duration(duration); paulo@89: xAxis.duration(duration); paulo@89: yAxis.duration(duration); paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = nv.utils.getColor(_); paulo@89: legend.color(color); paulo@89: stacked.color(color); paulo@89: }}, paulo@89: rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){ paulo@89: rightAlignYAxis = _; paulo@89: yAxis.orient( rightAlignYAxis ? 'right' : 'left'); paulo@89: }}, paulo@89: useInteractiveGuideline: {get: function(){return useInteractiveGuideline;}, set: function(_){ paulo@89: useInteractiveGuideline = !!_; paulo@89: chart.interactive(!_); paulo@89: chart.useVoronoi(!_); paulo@89: stacked.scatter.interactive(!_); paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.inheritOptions(chart, stacked); paulo@89: nv.utils.initOptions(chart); paulo@89: paulo@89: return chart; paulo@89: }; paulo@89: // based on http://bl.ocks.org/kerryrodden/477c1bfb081b783f80ad paulo@89: nv.models.sunburst = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var margin = {top: 0, right: 0, bottom: 0, left: 0} paulo@89: , width = null paulo@89: , height = null paulo@89: , mode = "count" paulo@89: , modes = {count: function(d) { return 1; }, size: function(d) { return d.size }} paulo@89: , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one paulo@89: , container = null paulo@89: , color = nv.utils.defaultColor() paulo@89: , duration = 500 paulo@89: , dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMousemove', 'elementMouseover', 'elementMouseout', 'renderEnd') paulo@89: ; paulo@89: paulo@89: var x = d3.scale.linear().range([0, 2 * Math.PI]); paulo@89: var y = d3.scale.sqrt(); paulo@89: paulo@89: var partition = d3.layout.partition() paulo@89: .sort(null) paulo@89: .value(function(d) { return 1; }); paulo@89: paulo@89: var arc = d3.svg.arc() paulo@89: .startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); }) paulo@89: .endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); }) paulo@89: .innerRadius(function(d) { return Math.max(0, y(d.y)); }) paulo@89: .outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)); }); paulo@89: paulo@89: // Keep track of the current and previous node being displayed as the root. paulo@89: var node, prevNode; paulo@89: // Keep track of the root node paulo@89: var rootNode; paulo@89: paulo@89: //============================================================ paulo@89: // chart function paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var renderWatch = nv.utils.renderWatch(dispatch); paulo@89: paulo@89: function chart(selection) { paulo@89: renderWatch.reset(); paulo@89: selection.each(function(data) { paulo@89: container = d3.select(this); paulo@89: var availableWidth = nv.utils.availableWidth(width, container, margin); paulo@89: var availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: var radius = Math.min(availableWidth, availableHeight) / 2; paulo@89: var path; paulo@89: paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('.nv-wrap.nv-sunburst').data(data); paulo@89: var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-sunburst nv-chart-' + id); paulo@89: paulo@89: var g = wrapEnter.selectAll('nv-sunburst'); paulo@89: paulo@89: wrap.attr('transform', 'translate(' + availableWidth / 2 + ',' + availableHeight / 2 + ')'); paulo@89: paulo@89: container.on('click', function (d, i) { paulo@89: dispatch.chartClick({ paulo@89: data: d, paulo@89: index: i, paulo@89: pos: d3.event, paulo@89: id: id paulo@89: }); paulo@89: }); paulo@89: paulo@89: y.range([0, radius]); paulo@89: paulo@89: node = node || data; paulo@89: rootNode = data[0]; paulo@89: partition.value(modes[mode] || modes["count"]); paulo@89: path = g.data(partition.nodes).enter() paulo@89: .append("path") paulo@89: .attr("d", arc) paulo@89: .style("fill", function (d) { paulo@89: return color((d.children ? d : d.parent).name); paulo@89: }) paulo@89: .style("stroke", "#FFF") paulo@89: .on("click", function(d) { paulo@89: if (prevNode !== node && node !== d) prevNode = node; paulo@89: node = d; paulo@89: path.transition() paulo@89: .duration(duration) paulo@89: .attrTween("d", arcTweenZoom(d)); paulo@89: }) paulo@89: .each(stash) paulo@89: .on("dblclick", function(d) { paulo@89: if (prevNode.parent == d) { paulo@89: path.transition() paulo@89: .duration(duration) paulo@89: .attrTween("d", arcTweenZoom(rootNode)); paulo@89: } paulo@89: }) paulo@89: .each(stash) paulo@89: .on('mouseover', function(d,i){ paulo@89: d3.select(this).classed('hover', true).style('opacity', 0.8); paulo@89: dispatch.elementMouseover({ paulo@89: data: d, paulo@89: color: d3.select(this).style("fill") paulo@89: }); paulo@89: }) paulo@89: .on('mouseout', function(d,i){ paulo@89: d3.select(this).classed('hover', false).style('opacity', 1); paulo@89: dispatch.elementMouseout({ paulo@89: data: d paulo@89: }); paulo@89: }) paulo@89: .on('mousemove', function(d,i){ paulo@89: dispatch.elementMousemove({ paulo@89: data: d paulo@89: }); paulo@89: }); paulo@89: paulo@89: paulo@89: paulo@89: // Setup for switching data: stash the old values for transition. paulo@89: function stash(d) { paulo@89: d.x0 = d.x; paulo@89: d.dx0 = d.dx; paulo@89: } paulo@89: paulo@89: // When switching data: interpolate the arcs in data space. paulo@89: function arcTweenData(a, i) { paulo@89: var oi = d3.interpolate({x: a.x0, dx: a.dx0}, a); paulo@89: paulo@89: function tween(t) { paulo@89: var b = oi(t); paulo@89: a.x0 = b.x; paulo@89: a.dx0 = b.dx; paulo@89: return arc(b); paulo@89: } paulo@89: paulo@89: if (i == 0) { paulo@89: // If we are on the first arc, adjust the x domain to match the root node paulo@89: // at the current zoom level. (We only need to do this once.) paulo@89: var xd = d3.interpolate(x.domain(), [node.x, node.x + node.dx]); paulo@89: return function (t) { paulo@89: x.domain(xd(t)); paulo@89: return tween(t); paulo@89: }; paulo@89: } else { paulo@89: return tween; paulo@89: } paulo@89: } paulo@89: paulo@89: // When zooming: interpolate the scales. paulo@89: function arcTweenZoom(d) { paulo@89: var xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]), paulo@89: yd = d3.interpolate(y.domain(), [d.y, 1]), paulo@89: yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]); paulo@89: return function (d, i) { paulo@89: return i paulo@89: ? function (t) { paulo@89: return arc(d); paulo@89: } paulo@89: : function (t) { paulo@89: x.domain(xd(t)); paulo@89: y.domain(yd(t)).range(yr(t)); paulo@89: return arc(d); paulo@89: }; paulo@89: }; paulo@89: } paulo@89: paulo@89: }); paulo@89: paulo@89: renderWatch.renderEnd('sunburst immediate'); paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: chart.dispatch = dispatch; paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: width: {get: function(){return width;}, set: function(_){width=_;}}, paulo@89: height: {get: function(){return height;}, set: function(_){height=_;}}, paulo@89: mode: {get: function(){return mode;}, set: function(_){mode=_;}}, paulo@89: id: {get: function(){return id;}, set: function(_){id=_;}}, paulo@89: duration: {get: function(){return duration;}, set: function(_){duration=_;}}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top != undefined ? _.top : margin.top; paulo@89: margin.right = _.right != undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom != undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left != undefined ? _.left : margin.left; paulo@89: }}, paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color=nv.utils.getColor(_); paulo@89: }} paulo@89: }); paulo@89: paulo@89: nv.utils.initOptions(chart); paulo@89: return chart; paulo@89: }; paulo@89: nv.models.sunburstChart = function() { paulo@89: "use strict"; paulo@89: paulo@89: //============================================================ paulo@89: // Public Variables with Default Settings paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var sunburst = nv.models.sunburst(); paulo@89: var tooltip = nv.models.tooltip(); paulo@89: paulo@89: var margin = {top: 30, right: 20, bottom: 20, left: 20} paulo@89: , width = null paulo@89: , height = null paulo@89: , color = nv.utils.defaultColor() paulo@89: , id = Math.round(Math.random() * 100000) paulo@89: , defaultState = null paulo@89: , noData = null paulo@89: , duration = 250 paulo@89: , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'stateChange', 'changeState','renderEnd') paulo@89: ; paulo@89: paulo@89: //============================================================ paulo@89: // Private Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: var renderWatch = nv.utils.renderWatch(dispatch); paulo@89: tooltip.headerEnabled(false).duration(0).valueFormatter(function(d, i) { paulo@89: return d; paulo@89: }); paulo@89: paulo@89: //============================================================ paulo@89: // Chart function paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: function chart(selection) { paulo@89: renderWatch.reset(); paulo@89: renderWatch.models(sunburst); paulo@89: paulo@89: selection.each(function(data) { paulo@89: var container = d3.select(this); paulo@89: nv.utils.initSVG(container); paulo@89: paulo@89: var that = this; paulo@89: var availableWidth = nv.utils.availableWidth(width, container, margin), paulo@89: availableHeight = nv.utils.availableHeight(height, container, margin); paulo@89: paulo@89: chart.update = function() { paulo@89: if (duration === 0) paulo@89: container.call(chart); paulo@89: else paulo@89: container.transition().duration(duration).call(chart) paulo@89: }; paulo@89: chart.container = this; paulo@89: paulo@89: // Display No Data message if there's nothing to show. paulo@89: if (!data || !data.length) { paulo@89: nv.utils.noData(chart, container); paulo@89: return chart; paulo@89: } else { paulo@89: container.selectAll('.nv-noData').remove(); paulo@89: } paulo@89: paulo@89: // Setup containers and skeleton of chart paulo@89: var wrap = container.selectAll('g.nv-wrap.nv-sunburstChart').data(data); paulo@89: var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-sunburstChart').append('g'); paulo@89: var g = wrap.select('g'); paulo@89: paulo@89: gEnter.append('g').attr('class', 'nv-sunburstWrap'); paulo@89: paulo@89: wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); paulo@89: paulo@89: // Main Chart Component(s) paulo@89: sunburst.width(availableWidth).height(availableHeight); paulo@89: var sunWrap = g.select('.nv-sunburstWrap').datum(data); paulo@89: d3.transition(sunWrap).call(sunburst); paulo@89: paulo@89: }); paulo@89: paulo@89: renderWatch.renderEnd('sunburstChart immediate'); paulo@89: return chart; paulo@89: } paulo@89: paulo@89: //============================================================ paulo@89: // Event Handling/Dispatching (out of chart's scope) paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: sunburst.dispatch.on('elementMouseover.tooltip', function(evt) { paulo@89: evt['series'] = { paulo@89: key: evt.data.name, paulo@89: value: evt.data.size, paulo@89: color: evt.color paulo@89: }; paulo@89: tooltip.data(evt).hidden(false); paulo@89: }); paulo@89: paulo@89: sunburst.dispatch.on('elementMouseout.tooltip', function(evt) { paulo@89: tooltip.hidden(true); paulo@89: }); paulo@89: paulo@89: sunburst.dispatch.on('elementMousemove.tooltip', function(evt) { paulo@89: tooltip.position({top: d3.event.pageY, left: d3.event.pageX})(); paulo@89: }); paulo@89: paulo@89: //============================================================ paulo@89: // Expose Public Variables paulo@89: //------------------------------------------------------------ paulo@89: paulo@89: // expose chart's sub-components paulo@89: chart.dispatch = dispatch; paulo@89: chart.sunburst = sunburst; paulo@89: chart.tooltip = tooltip; paulo@89: chart.options = nv.utils.optionsFunc.bind(chart); paulo@89: paulo@89: // use Object get/set functionality to map between vars and chart functions paulo@89: chart._options = Object.create({}, { paulo@89: // simple options, just get/set the necessary values paulo@89: noData: {get: function(){return noData;}, set: function(_){noData=_;}}, paulo@89: defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}}, paulo@89: paulo@89: // options that require extra logic in the setter paulo@89: color: {get: function(){return color;}, set: function(_){ paulo@89: color = _; paulo@89: sunburst.color(color); paulo@89: }}, paulo@89: duration: {get: function(){return duration;}, set: function(_){ paulo@89: duration = _; paulo@89: renderWatch.reset(duration); paulo@89: sunburst.duration(duration); paulo@89: }}, paulo@89: margin: {get: function(){return margin;}, set: function(_){ paulo@89: margin.top = _.top !== undefined ? _.top : margin.top; paulo@89: margin.right = _.right !== undefined ? _.right : margin.right; paulo@89: margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; paulo@89: margin.left = _.left !== undefined ? _.left : margin.left; paulo@89: }} paulo@89: }); paulo@89: nv.utils.inheritOptions(chart, sunburst); paulo@89: nv.utils.initOptions(chart); paulo@89: return chart; paulo@89: }; paulo@89: paulo@89: nv.version = "1.8.1"; paulo@89: })();