OCA reporting engine fork for dev and update.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

5984 lines
242 KiB

  1. /* nvd3 version 1.8.2-dev (https://github.com/novus/nvd3) 2016-02-08 */
  2. (function(){
  3. // set up main nv object
  4. var nv = {};
  5. // the major global objects under the nv namespace
  6. nv.dev = false; //set false when in production
  7. nv.tooltip = nv.tooltip || {}; // For the tooltip system
  8. nv.utils = nv.utils || {}; // Utility subsystem
  9. nv.models = nv.models || {}; //stores all the possible models/components
  10. nv.charts = {}; //stores all the ready to use charts
  11. nv.logs = {}; //stores some statistics and potential error messages
  12. nv.dom = {}; //DOM manipulation functions
  13. // Node/CommonJS - require D3
  14. if (typeof(module) !== 'undefined' && typeof(exports) !== 'undefined' && typeof(d3) == 'undefined') {
  15. d3 = require('d3');
  16. }
  17. nv.dispatch = d3.dispatch('render_start', 'render_end');
  18. // Function bind polyfill
  19. // Needed ONLY for phantomJS as it's missing until version 2.0 which is unreleased as of this comment
  20. // https://github.com/ariya/phantomjs/issues/10522
  21. // http://kangax.github.io/compat-table/es5/#Function.prototype.bind
  22. // phantomJS is used for running the test suite
  23. if (!Function.prototype.bind) {
  24. Function.prototype.bind = function (oThis) {
  25. if (typeof this !== "function") {
  26. // closest thing possible to the ECMAScript 5 internal IsCallable function
  27. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  28. }
  29. var aArgs = Array.prototype.slice.call(arguments, 1),
  30. fToBind = this,
  31. fNOP = function () {},
  32. fBound = function () {
  33. return fToBind.apply(this instanceof fNOP && oThis
  34. ? this
  35. : oThis,
  36. aArgs.concat(Array.prototype.slice.call(arguments)));
  37. };
  38. fNOP.prototype = this.prototype;
  39. fBound.prototype = new fNOP();
  40. return fBound;
  41. };
  42. }
  43. // Development render timers - disabled if dev = false
  44. if (nv.dev) {
  45. nv.dispatch.on('render_start', function(e) {
  46. nv.logs.startTime = +new Date();
  47. });
  48. nv.dispatch.on('render_end', function(e) {
  49. nv.logs.endTime = +new Date();
  50. nv.logs.totalTime = nv.logs.endTime - nv.logs.startTime;
  51. nv.log('total', nv.logs.totalTime); // used for development, to keep track of graph generation times
  52. });
  53. }
  54. // Logs all arguments, and returns the last so you can test things in place
  55. // Note: in IE8 console.log is an object not a function, and if modernizr is used
  56. // then calling Function.prototype.bind with with anything other than a function
  57. // causes a TypeError to be thrown.
  58. nv.log = function() {
  59. if (nv.dev && window.console && console.log && console.log.apply)
  60. console.log.apply(console, arguments);
  61. else if (nv.dev && window.console && typeof console.log == "function" && Function.prototype.bind) {
  62. var log = Function.prototype.bind.call(console.log, console);
  63. log.apply(console, arguments);
  64. }
  65. return arguments[arguments.length - 1];
  66. };
  67. // print console warning, should be used by deprecated functions
  68. nv.deprecated = function(name, info) {
  69. if (console && console.warn) {
  70. console.warn('nvd3 warning: `' + name + '` has been deprecated. ', info || '');
  71. }
  72. };
  73. // The nv.render function is used to queue up chart rendering
  74. // in non-blocking async functions.
  75. // When all queued charts are done rendering, nv.dispatch.render_end is invoked.
  76. nv.render = function render(step) {
  77. // number of graphs to generate in each timeout loop
  78. step = step || 1;
  79. nv.render.active = true;
  80. nv.dispatch.render_start();
  81. var renderLoop = function() {
  82. var chart, graph;
  83. for (var i = 0; i < step && (graph = nv.render.queue[i]); i++) {
  84. chart = graph.generate();
  85. if (typeof graph.callback == typeof(Function)) graph.callback(chart);
  86. }
  87. nv.render.queue.splice(0, i);
  88. if (nv.render.queue.length) {
  89. setTimeout(renderLoop);
  90. }
  91. else {
  92. nv.dispatch.render_end();
  93. nv.render.active = false;
  94. }
  95. };
  96. setTimeout(renderLoop);
  97. };
  98. nv.render.active = false;
  99. nv.render.queue = [];
  100. /*
  101. Adds a chart to the async rendering queue. This method can take arguments in two forms:
  102. nv.addGraph({
  103. generate: <Function>
  104. callback: <Function>
  105. })
  106. or
  107. nv.addGraph(<generate Function>, <callback Function>)
  108. The generate function should contain code that creates the NVD3 model, sets options
  109. on it, adds data to an SVG element, and invokes the chart model. The generate function
  110. should return the chart model. See examples/lineChart.html for a usage example.
  111. The callback function is optional, and it is called when the generate function completes.
  112. */
  113. nv.addGraph = function(obj) {
  114. if (typeof arguments[0] === typeof(Function)) {
  115. obj = {generate: arguments[0], callback: arguments[1]};
  116. }
  117. nv.render.queue.push(obj);
  118. if (!nv.render.active) {
  119. nv.render();
  120. }
  121. };
  122. // Node/CommonJS exports
  123. if (typeof(module) !== 'undefined' && typeof(exports) !== 'undefined') {
  124. module.exports = nv;
  125. }
  126. if (typeof(window) !== 'undefined') {
  127. window.nv = nv;
  128. }
  129. /* Facade for queueing DOM write operations
  130. * with Fastdom (https://github.com/wilsonpage/fastdom)
  131. * if available.
  132. * This could easily be extended to support alternate
  133. * implementations in the future.
  134. */
  135. nv.dom.write = function(callback) {
  136. if (window.fastdom !== undefined) {
  137. return fastdom.write(callback);
  138. }
  139. return callback();
  140. };
  141. /* Facade for queueing DOM read operations
  142. * with Fastdom (https://github.com/wilsonpage/fastdom)
  143. * if available.
  144. * This could easily be extended to support alternate
  145. * implementations in the future.
  146. */
  147. nv.dom.read = function(callback) {
  148. if (window.fastdom !== undefined) {
  149. return fastdom.read(callback);
  150. }
  151. return callback();
  152. };/* Utility class to handle creation of an interactive layer.
  153. This places a rectangle on top of the chart. When you mouse move over it, it sends a dispatch
  154. containing the X-coordinate. It can also render a vertical line where the mouse is located.
  155. dispatch.elementMousemove is the important event to latch onto. It is fired whenever the mouse moves over
  156. the rectangle. The dispatch is given one object which contains the mouseX/Y location.
  157. It also has 'pointXValue', which is the conversion of mouseX to the x-axis scale.
  158. */
  159. nv.interactiveGuideline = function() {
  160. "use strict";
  161. var margin = { left: 0, top: 0 } //Pass the chart's top and left magins. Used to calculate the mouseX/Y.
  162. , width = null
  163. , height = null
  164. , xScale = d3.scale.linear()
  165. , dispatch = d3.dispatch('elementMousemove', 'elementMouseout', 'elementClick', 'elementDblclick', 'elementMouseDown', 'elementMouseUp')
  166. , showGuideLine = true
  167. , svgContainer = null // Must pass the chart's svg, we'll use its mousemove event.
  168. , tooltip = nv.models.tooltip()
  169. , isMSIE = "ActiveXObject" in window // Checkt if IE by looking for activeX.
  170. ;
  171. tooltip
  172. .duration(0)
  173. .hideDelay(0)
  174. .hidden(false);
  175. function layer(selection) {
  176. selection.each(function(data) {
  177. var container = d3.select(this);
  178. var availableWidth = (width || 960), availableHeight = (height || 400);
  179. var wrap = container.selectAll("g.nv-wrap.nv-interactiveLineLayer")
  180. .data([data]);
  181. var wrapEnter = wrap.enter()
  182. .append("g").attr("class", " nv-wrap nv-interactiveLineLayer");
  183. wrapEnter.append("g").attr("class","nv-interactiveGuideLine");
  184. if (!svgContainer) {
  185. return;
  186. }
  187. function mouseHandler() {
  188. var d3mouse = d3.mouse(this);
  189. var mouseX = d3mouse[0];
  190. var mouseY = d3mouse[1];
  191. var subtractMargin = true;
  192. var mouseOutAnyReason = false;
  193. if (isMSIE) {
  194. /*
  195. D3.js (or maybe SVG.getScreenCTM) has a nasty bug in Internet Explorer 10.
  196. d3.mouse() returns incorrect X,Y mouse coordinates when mouse moving
  197. over a rect in IE 10.
  198. However, d3.event.offsetX/Y also returns the mouse coordinates
  199. relative to the triggering <rect>. So we use offsetX/Y on IE.
  200. */
  201. mouseX = d3.event.offsetX;
  202. mouseY = d3.event.offsetY;
  203. /*
  204. On IE, if you attach a mouse event listener to the <svg> container,
  205. it will actually trigger it for all the child elements (like <path>, <circle>, etc).
  206. When this happens on IE, the offsetX/Y is set to where ever the child element
  207. is located.
  208. As a result, we do NOT need to subtract margins to figure out the mouse X/Y
  209. position under this scenario. Removing the line below *will* cause
  210. the interactive layer to not work right on IE.
  211. */
  212. if(d3.event.target.tagName !== "svg") {
  213. subtractMargin = false;
  214. }
  215. if (d3.event.target.className.baseVal.match("nv-legend")) {
  216. mouseOutAnyReason = true;
  217. }
  218. }
  219. if(subtractMargin) {
  220. mouseX -= margin.left;
  221. mouseY -= margin.top;
  222. }
  223. /* If mouseX/Y is outside of the chart's bounds,
  224. trigger a mouseOut event.
  225. */
  226. if (d3.event.type === 'mouseout'
  227. || mouseX < 0 || mouseY < 0
  228. || mouseX > availableWidth || mouseY > availableHeight
  229. || (d3.event.relatedTarget && d3.event.relatedTarget.ownerSVGElement === undefined)
  230. || mouseOutAnyReason
  231. ) {
  232. if (isMSIE) {
  233. if (d3.event.relatedTarget
  234. && d3.event.relatedTarget.ownerSVGElement === undefined
  235. && (d3.event.relatedTarget.className === undefined
  236. || d3.event.relatedTarget.className.match(tooltip.nvPointerEventsClass))) {
  237. return;
  238. }
  239. }
  240. dispatch.elementMouseout({
  241. mouseX: mouseX,
  242. mouseY: mouseY
  243. });
  244. layer.renderGuideLine(null); //hide the guideline
  245. tooltip.hidden(true);
  246. return;
  247. } else {
  248. tooltip.hidden(false);
  249. }
  250. var scaleIsOrdinal = typeof xScale.rangeBands === 'function';
  251. var pointXValue = undefined;
  252. // Ordinal scale has no invert method
  253. if (scaleIsOrdinal) {
  254. var elementIndex = d3.bisect(xScale.range(), mouseX) - 1;
  255. // Check if mouseX is in the range band
  256. if (xScale.range()[elementIndex] + xScale.rangeBand() >= mouseX) {
  257. pointXValue = xScale.domain()[d3.bisect(xScale.range(), mouseX) - 1];
  258. }
  259. else {
  260. dispatch.elementMouseout({
  261. mouseX: mouseX,
  262. mouseY: mouseY
  263. });
  264. layer.renderGuideLine(null); //hide the guideline
  265. tooltip.hidden(true);
  266. return;
  267. }
  268. }
  269. else {
  270. pointXValue = xScale.invert(mouseX);
  271. }
  272. dispatch.elementMousemove({
  273. mouseX: mouseX,
  274. mouseY: mouseY,
  275. pointXValue: pointXValue
  276. });
  277. //If user double clicks the layer, fire a elementDblclick
  278. if (d3.event.type === "dblclick") {
  279. dispatch.elementDblclick({
  280. mouseX: mouseX,
  281. mouseY: mouseY,
  282. pointXValue: pointXValue
  283. });
  284. }
  285. // if user single clicks the layer, fire elementClick
  286. if (d3.event.type === 'click') {
  287. dispatch.elementClick({
  288. mouseX: mouseX,
  289. mouseY: mouseY,
  290. pointXValue: pointXValue
  291. });
  292. }
  293. // if user presses mouse down the layer, fire elementMouseDown
  294. if (d3.event.type === 'mousedown') {
  295. dispatch.elementMouseDown({
  296. mouseX: mouseX,
  297. mouseY: mouseY,
  298. pointXValue: pointXValue
  299. });
  300. }
  301. // if user presses mouse down the layer, fire elementMouseUp
  302. if (d3.event.type === 'mouseup') {
  303. dispatch.elementMouseUp({
  304. mouseX: mouseX,
  305. mouseY: mouseY,
  306. pointXValue: pointXValue
  307. });
  308. }
  309. }
  310. svgContainer
  311. .on("touchmove",mouseHandler)
  312. .on("mousemove",mouseHandler, true)
  313. .on("mouseout" ,mouseHandler,true)
  314. .on("mousedown" ,mouseHandler,true)
  315. .on("mouseup" ,mouseHandler,true)
  316. .on("dblclick" ,mouseHandler)
  317. .on("click", mouseHandler)
  318. ;
  319. layer.guideLine = null;
  320. //Draws a vertical guideline at the given X postion.
  321. layer.renderGuideLine = function(x) {
  322. if (!showGuideLine) return;
  323. if (layer.guideLine && layer.guideLine.attr("x1") === x) return;
  324. nv.dom.write(function() {
  325. var line = wrap.select(".nv-interactiveGuideLine")
  326. .selectAll("line")
  327. .data((x != null) ? [nv.utils.NaNtoZero(x)] : [], String);
  328. line.enter()
  329. .append("line")
  330. .attr("class", "nv-guideline")
  331. .attr("x1", function(d) { return d;})
  332. .attr("x2", function(d) { return d;})
  333. .attr("y1", availableHeight)
  334. .attr("y2",0);
  335. line.exit().remove();
  336. });
  337. }
  338. });
  339. }
  340. layer.dispatch = dispatch;
  341. layer.tooltip = tooltip;
  342. layer.margin = function(_) {
  343. if (!arguments.length) return margin;
  344. margin.top = typeof _.top != 'undefined' ? _.top : margin.top;
  345. margin.left = typeof _.left != 'undefined' ? _.left : margin.left;
  346. return layer;
  347. };
  348. layer.width = function(_) {
  349. if (!arguments.length) return width;
  350. width = _;
  351. return layer;
  352. };
  353. layer.height = function(_) {
  354. if (!arguments.length) return height;
  355. height = _;
  356. return layer;
  357. };
  358. layer.xScale = function(_) {
  359. if (!arguments.length) return xScale;
  360. xScale = _;
  361. return layer;
  362. };
  363. layer.showGuideLine = function(_) {
  364. if (!arguments.length) return showGuideLine;
  365. showGuideLine = _;
  366. return layer;
  367. };
  368. layer.svgContainer = function(_) {
  369. if (!arguments.length) return svgContainer;
  370. svgContainer = _;
  371. return layer;
  372. };
  373. return layer;
  374. };
  375. /* Utility class that uses d3.bisect to find the index in a given array, where a search value can be inserted.
  376. This is different from normal bisectLeft; this function finds the nearest index to insert the search value.
  377. For instance, lets say your array is [1,2,3,5,10,30], and you search for 28.
  378. Normal d3.bisectLeft will return 4, because 28 is inserted after the number 10. But interactiveBisect will return 5
  379. because 28 is closer to 30 than 10.
  380. Unit tests can be found in: interactiveBisectTest.html
  381. Has the following known issues:
  382. * Will not work if the data points move backwards (ie, 10,9,8,7, etc) or if the data points are in random order.
  383. * Won't work if there are duplicate x coordinate values.
  384. */
  385. nv.interactiveBisect = function (values, searchVal, xAccessor) {
  386. "use strict";
  387. if (! (values instanceof Array)) {
  388. return null;
  389. }
  390. var _xAccessor;
  391. if (typeof xAccessor !== 'function') {
  392. _xAccessor = function(d) {
  393. return d.x;
  394. }
  395. } else {
  396. _xAccessor = xAccessor;
  397. }
  398. var _cmp = function(d, v) {
  399. // Accessors are no longer passed the index of the element along with
  400. // the element itself when invoked by d3.bisector.
  401. //
  402. // Starting at D3 v3.4.4, d3.bisector() started inspecting the
  403. // function passed to determine if it should consider it an accessor
  404. // or a comparator. This meant that accessors that take two arguments
  405. // (expecting an index as the second parameter) are treated as
  406. // comparators where the second argument is the search value against
  407. // which the first argument is compared.
  408. return _xAccessor(d) - v;
  409. };
  410. var bisect = d3.bisector(_cmp).left;
  411. var index = d3.max([0, bisect(values,searchVal) - 1]);
  412. var currentValue = _xAccessor(values[index]);
  413. if (typeof currentValue === 'undefined') {
  414. currentValue = index;
  415. }
  416. if (currentValue === searchVal) {
  417. return index; //found exact match
  418. }
  419. var nextIndex = d3.min([index+1, values.length - 1]);
  420. var nextValue = _xAccessor(values[nextIndex]);
  421. if (typeof nextValue === 'undefined') {
  422. nextValue = nextIndex;
  423. }
  424. if (Math.abs(nextValue - searchVal) >= Math.abs(currentValue - searchVal)) {
  425. return index;
  426. } else {
  427. return nextIndex
  428. }
  429. };
  430. /*
  431. Returns the index in the array "values" that is closest to searchVal.
  432. Only returns an index if searchVal is within some "threshold".
  433. Otherwise, returns null.
  434. */
  435. nv.nearestValueIndex = function (values, searchVal, threshold) {
  436. "use strict";
  437. var yDistMax = Infinity, indexToHighlight = null;
  438. values.forEach(function(d,i) {
  439. var delta = Math.abs(searchVal - d);
  440. if ( d != null && delta <= yDistMax && delta < threshold) {
  441. yDistMax = delta;
  442. indexToHighlight = i;
  443. }
  444. });
  445. return indexToHighlight;
  446. };
  447. /* Model which can be instantiated to handle tooltip rendering.
  448. Example usage:
  449. var tip = nv.models.tooltip().gravity('w').distance(23)
  450. .data(myDataObject);
  451. tip(); //just invoke the returned function to render tooltip.
  452. */
  453. nv.models.tooltip = function() {
  454. "use strict";
  455. /*
  456. Tooltip data. If data is given in the proper format, a consistent tooltip is generated.
  457. Example Format of data:
  458. {
  459. key: "Date",
  460. value: "August 2009",
  461. series: [
  462. {key: "Series 1", value: "Value 1", color: "#000"},
  463. {key: "Series 2", value: "Value 2", color: "#00f"}
  464. ]
  465. }
  466. */
  467. var id = "nvtooltip-" + Math.floor(Math.random() * 100000) // Generates a unique id when you create a new tooltip() object.
  468. , data = null
  469. , gravity = 'w' // Can be 'n','s','e','w'. Determines how tooltip is positioned.
  470. , distance = 25 // Distance to offset tooltip from the mouse location.
  471. , snapDistance = 0 // Tolerance allowed before tooltip is moved from its current position (creates 'snapping' effect)
  472. , classes = null // Attaches additional CSS classes to the tooltip DIV that is created.
  473. , chartContainer = null // Parent dom element of the SVG that holds the chart.
  474. , hidden = true // Start off hidden, toggle with hide/show functions below.
  475. , hideDelay = 200 // Delay (in ms) before the tooltip hides after calling hide().
  476. , tooltip = null // d3 select of the tooltip div.
  477. , lastPosition = { left: null, top: null } // Last position the tooltip was in.
  478. , enabled = true // True -> tooltips are rendered. False -> don't render tooltips.
  479. , duration = 100 // Tooltip movement duration, in ms.
  480. , headerEnabled = true // If is to show the tooltip header.
  481. , nvPointerEventsClass = "nv-pointer-events-none" // CSS class to specify whether element should not have mouse events.
  482. ;
  483. /*
  484. Function that returns the position (relative to the viewport) the tooltip should be placed in.
  485. Should return: {
  486. left: <leftPos>,
  487. top: <topPos>
  488. }
  489. */
  490. var position = function() {
  491. return {
  492. left: d3.event !== null ? d3.event.clientX : 0,
  493. top: d3.event !== null ? d3.event.clientY : 0
  494. };
  495. };
  496. // Format function for the tooltip values column.
  497. var valueFormatter = function(d, i) {
  498. return d;
  499. };
  500. // Format function for the tooltip header value.
  501. var headerFormatter = function(d) {
  502. return d;
  503. };
  504. var keyFormatter = function(d, i) {
  505. return d;
  506. };
  507. // By default, the tooltip model renders a beautiful table inside a DIV.
  508. // You can override this function if a custom tooltip is desired.
  509. var contentGenerator = function(d) {
  510. if (d === null) {
  511. return '';
  512. }
  513. var table = d3.select(document.createElement("table"));
  514. if (headerEnabled) {
  515. var theadEnter = table.selectAll("thead")
  516. .data([d])
  517. .enter().append("thead");
  518. theadEnter.append("tr")
  519. .append("td")
  520. .attr("colspan", 3)
  521. .append("strong")
  522. .classed("x-value", true)
  523. .html(headerFormatter(d.value));
  524. }
  525. var tbodyEnter = table.selectAll("tbody")
  526. .data([d])
  527. .enter().append("tbody");
  528. var trowEnter = tbodyEnter.selectAll("tr")
  529. .data(function(p) { return p.series})
  530. .enter()
  531. .append("tr")
  532. .classed("highlight", function(p) { return p.highlight});
  533. trowEnter.append("td")
  534. .classed("legend-color-guide",true)
  535. .append("div")
  536. .style("background-color", function(p) { return p.color});
  537. trowEnter.append("td")
  538. .classed("key",true)
  539. .classed("total",function(p) { return !!p.total})
  540. .html(function(p, i) { return keyFormatter(p.key, i)});
  541. trowEnter.append("td")
  542. .classed("value",true)
  543. .html(function(p, i) { return valueFormatter(p.value, i) });
  544. trowEnter.selectAll("td").each(function(p) {
  545. if (p.highlight) {
  546. var opacityScale = d3.scale.linear().domain([0,1]).range(["#fff",p.color]);
  547. var opacity = 0.6;
  548. d3.select(this)
  549. .style("border-bottom-color", opacityScale(opacity))
  550. .style("border-top-color", opacityScale(opacity))
  551. ;
  552. }
  553. });
  554. var html = table.node().outerHTML;
  555. if (d.footer !== undefined)
  556. html += "<div class='footer'>" + d.footer + "</div>";
  557. return html;
  558. };
  559. var dataSeriesExists = function(d) {
  560. if (d && d.series) {
  561. if (nv.utils.isArray(d.series)) {
  562. return true;
  563. }
  564. // if object, it's okay just convert to array of the object
  565. if (nv.utils.isObject(d.series)) {
  566. d.series = [d.series];
  567. return true;
  568. }
  569. }
  570. return false;
  571. };
  572. // Calculates the gravity offset of the tooltip. Parameter is position of tooltip
  573. // relative to the viewport.
  574. var calcGravityOffset = function(pos) {
  575. var height = tooltip.node().offsetHeight,
  576. width = tooltip.node().offsetWidth,
  577. clientWidth = document.documentElement.clientWidth, // Don't want scrollbars.
  578. clientHeight = document.documentElement.clientHeight, // Don't want scrollbars.
  579. left, top, tmp;
  580. // calculate position based on gravity
  581. switch (gravity) {
  582. case 'e':
  583. left = - width - distance;
  584. top = - (height / 2);
  585. if(pos.left + left < 0) left = distance;
  586. if((tmp = pos.top + top) < 0) top -= tmp;
  587. if((tmp = pos.top + top + height) > clientHeight) top -= tmp - clientHeight;
  588. break;
  589. case 'w':
  590. left = distance;
  591. top = - (height / 2);
  592. if (pos.left + left + width > clientWidth) left = - width - distance;
  593. if ((tmp = pos.top + top) < 0) top -= tmp;
  594. if ((tmp = pos.top + top + height) > clientHeight) top -= tmp - clientHeight;
  595. break;
  596. case 'n':
  597. left = - (width / 2) - 5; // - 5 is an approximation of the mouse's height.
  598. top = distance;
  599. if (pos.top + top + height > clientHeight) top = - height - distance;
  600. if ((tmp = pos.left + left) < 0) left -= tmp;
  601. if ((tmp = pos.left + left + width) > clientWidth) left -= tmp - clientWidth;
  602. break;
  603. case 's':
  604. left = - (width / 2);
  605. top = - height - distance;
  606. if (pos.top + top < 0) top = distance;
  607. if ((tmp = pos.left + left) < 0) left -= tmp;
  608. if ((tmp = pos.left + left + width) > clientWidth) left -= tmp - clientWidth;
  609. break;
  610. case 'center':
  611. left = - (width / 2);
  612. top = - (height / 2);
  613. break;
  614. default:
  615. left = 0;
  616. top = 0;
  617. break;
  618. }
  619. return { 'left': left, 'top': top };
  620. };
  621. /*
  622. Positions the tooltip in the correct place, as given by the position() function.
  623. */
  624. var positionTooltip = function() {
  625. nv.dom.read(function() {
  626. var pos = position(),
  627. gravityOffset = calcGravityOffset(pos),
  628. left = pos.left + gravityOffset.left,
  629. top = pos.top + gravityOffset.top;
  630. // delay hiding a bit to avoid flickering
  631. if (hidden) {
  632. tooltip
  633. .interrupt()
  634. .transition()
  635. .delay(hideDelay)
  636. .duration(0)
  637. .style('opacity', 0);
  638. } else {
  639. // using tooltip.style('transform') returns values un-usable for tween
  640. var old_translate = 'translate(' + lastPosition.left + 'px, ' + lastPosition.top + 'px)';
  641. var new_translate = 'translate(' + left + 'px, ' + top + 'px)';
  642. var translateInterpolator = d3.interpolateString(old_translate, new_translate);
  643. var is_hidden = tooltip.style('opacity') < 0.1;
  644. tooltip
  645. .interrupt() // cancel running transitions
  646. .transition()
  647. .duration(is_hidden ? 0 : duration)
  648. // using tween since some versions of d3 can't auto-tween a translate on a div
  649. .styleTween('transform', function (d) {
  650. return translateInterpolator;
  651. }, 'important')
  652. // Safari has its own `-webkit-transform` and does not support `transform`
  653. .styleTween('-webkit-transform', function (d) {
  654. return translateInterpolator;
  655. })
  656. .style('-ms-transform', new_translate)
  657. .style('opacity', 1);
  658. }
  659. lastPosition.left = left;
  660. lastPosition.top = top;
  661. });
  662. };
  663. // Creates new tooltip container, or uses existing one on DOM.
  664. function initTooltip() {
  665. if (!tooltip || !tooltip.node()) {
  666. var container = chartContainer ? chartContainer : document.body;
  667. // Create new tooltip div if it doesn't exist on DOM.
  668. var data = [1];
  669. tooltip = d3.select(container).selectAll('.nvtooltip').data(data);
  670. tooltip.enter().append('div')
  671. .attr("class", "nvtooltip " + (classes ? classes : "xy-tooltip"))
  672. .attr("id", id)
  673. .style("top", 0).style("left", 0)
  674. .style('opacity', 0)
  675. .style('position', 'fixed')
  676. .selectAll("div, table, td, tr").classed(nvPointerEventsClass, true)
  677. .classed(nvPointerEventsClass, true);
  678. tooltip.exit().remove()
  679. }
  680. }
  681. // Draw the tooltip onto the DOM.
  682. function nvtooltip() {
  683. if (!enabled) return;
  684. if (!dataSeriesExists(data)) return;
  685. nv.dom.write(function () {
  686. initTooltip();
  687. // Generate data and set it into tooltip.
  688. // Bonus - If you override contentGenerator and return falsey you can use something like
  689. // React or Knockout to bind the data for your tooltip.
  690. var newContent = contentGenerator(data);
  691. if (newContent) {
  692. tooltip.node().innerHTML = newContent;
  693. }
  694. positionTooltip();
  695. });
  696. return nvtooltip;
  697. }
  698. nvtooltip.nvPointerEventsClass = nvPointerEventsClass;
  699. nvtooltip.options = nv.utils.optionsFunc.bind(nvtooltip);
  700. nvtooltip._options = Object.create({}, {
  701. // simple read/write options
  702. duration: {get: function(){return duration;}, set: function(_){duration=_;}},
  703. gravity: {get: function(){return gravity;}, set: function(_){gravity=_;}},
  704. distance: {get: function(){return distance;}, set: function(_){distance=_;}},
  705. snapDistance: {get: function(){return snapDistance;}, set: function(_){snapDistance=_;}},
  706. classes: {get: function(){return classes;}, set: function(_){classes=_;}},
  707. chartContainer: {get: function(){return chartContainer;}, set: function(_){chartContainer=_;}},
  708. enabled: {get: function(){return enabled;}, set: function(_){enabled=_;}},
  709. hideDelay: {get: function(){return hideDelay;}, set: function(_){hideDelay=_;}},
  710. contentGenerator: {get: function(){return contentGenerator;}, set: function(_){contentGenerator=_;}},
  711. valueFormatter: {get: function(){return valueFormatter;}, set: function(_){valueFormatter=_;}},
  712. headerFormatter: {get: function(){return headerFormatter;}, set: function(_){headerFormatter=_;}},
  713. keyFormatter: {get: function(){return keyFormatter;}, set: function(_){keyFormatter=_;}},
  714. headerEnabled: {get: function(){return headerEnabled;}, set: function(_){headerEnabled=_;}},
  715. position: {get: function(){return position;}, set: function(_){position=_;}},
  716. // Deprecated options
  717. fixedTop: {get: function(){return null;}, set: function(_){
  718. // deprecated after 1.8.1
  719. nv.deprecated('fixedTop', 'feature removed after 1.8.1');
  720. }},
  721. offset: {get: function(){return {left: 0, top: 0};}, set: function(_){
  722. // deprecated after 1.8.1
  723. nv.deprecated('offset', 'use chart.tooltip.distance() instead');
  724. }},
  725. // options with extra logic
  726. hidden: {get: function(){return hidden;}, set: function(_){
  727. if (hidden != _) {
  728. hidden = !!_;
  729. nvtooltip();
  730. }
  731. }},
  732. data: {get: function(){return data;}, set: function(_){
  733. // if showing a single data point, adjust data format with that
  734. if (_.point) {
  735. _.value = _.point.x;
  736. _.series = _.series || {};
  737. _.series.value = _.point.y;
  738. _.series.color = _.point.color || _.series.color;
  739. }
  740. data = _;
  741. }},
  742. // read only properties
  743. node: {get: function(){return tooltip.node();}, set: function(_){}},
  744. id: {get: function(){return id;}, set: function(_){}}
  745. });
  746. nv.utils.initOptions(nvtooltip);
  747. return nvtooltip;
  748. };
  749. /*
  750. Gets the browser window size
  751. Returns object with height and width properties
  752. */
  753. nv.utils.windowSize = function() {
  754. // Sane defaults
  755. var size = {width: 640, height: 480};
  756. // Most recent browsers use
  757. if (window.innerWidth && window.innerHeight) {
  758. size.width = window.innerWidth;
  759. size.height = window.innerHeight;
  760. return (size);
  761. }
  762. // IE can use depending on mode it is in
  763. if (document.compatMode=='CSS1Compat' &&
  764. document.documentElement &&
  765. document.documentElement.offsetWidth ) {
  766. size.width = document.documentElement.offsetWidth;
  767. size.height = document.documentElement.offsetHeight;
  768. return (size);
  769. }
  770. // Earlier IE uses Doc.body
  771. if (document.body && document.body.offsetWidth) {
  772. size.width = document.body.offsetWidth;
  773. size.height = document.body.offsetHeight;
  774. return (size);
  775. }
  776. return (size);
  777. };
  778. /* handle dumb browser quirks... isinstance breaks if you use frames
  779. typeof returns 'object' for null, NaN is a number, etc.
  780. */
  781. nv.utils.isArray = Array.isArray;
  782. nv.utils.isObject = function(a) {
  783. return a !== null && typeof a === 'object';
  784. };
  785. nv.utils.isFunction = function(a) {
  786. return typeof a === 'function';
  787. };
  788. nv.utils.isDate = function(a) {
  789. return toString.call(a) === '[object Date]';
  790. };
  791. nv.utils.isNumber = function(a) {
  792. return !isNaN(a) && typeof a === 'number';
  793. };
  794. /*
  795. Binds callback function to run when window is resized
  796. */
  797. nv.utils.windowResize = function(handler) {
  798. if (window.addEventListener) {
  799. window.addEventListener('resize', handler);
  800. } else {
  801. nv.log("ERROR: Failed to bind to window.resize with: ", handler);
  802. }
  803. // return object with clear function to remove the single added callback.
  804. return {
  805. callback: handler,
  806. clear: function() {
  807. window.removeEventListener('resize', handler);
  808. }
  809. }
  810. };
  811. /*
  812. Backwards compatible way to implement more d3-like coloring of graphs.
  813. Can take in nothing, an array, or a function/scale
  814. To use a normal scale, get the range and pass that because we must be able
  815. to take two arguments and use the index to keep backward compatibility
  816. */
  817. nv.utils.getColor = function(color) {
  818. //if you pass in nothing, get default colors back
  819. if (color === undefined) {
  820. return nv.utils.defaultColor();
  821. //if passed an array, turn it into a color scale
  822. } else if(nv.utils.isArray(color)) {
  823. var color_scale = d3.scale.ordinal().range(color);
  824. return function(d, i) {
  825. var key = i === undefined ? d : i;
  826. return d.color || color_scale(key);
  827. };
  828. //if passed a function or scale, return it, or whatever it may be
  829. //external libs, such as angularjs-nvd3-directives use this
  830. } else {
  831. //can't really help it if someone passes rubbish as color
  832. return color;
  833. }
  834. };
  835. /*
  836. Default color chooser uses a color scale of 20 colors from D3
  837. https://github.com/mbostock/d3/wiki/Ordinal-Scales#categorical-colors
  838. */
  839. nv.utils.defaultColor = function() {
  840. // get range of the scale so we'll turn it into our own function.
  841. return nv.utils.getColor(d3.scale.category20().range());
  842. };
  843. /*
  844. Returns a color function that takes the result of 'getKey' for each series and
  845. looks for a corresponding color from the dictionary
  846. */
  847. nv.utils.customTheme = function(dictionary, getKey, defaultColors) {
  848. // use default series.key if getKey is undefined
  849. getKey = getKey || function(series) { return series.key };
  850. defaultColors = defaultColors || d3.scale.category20().range();
  851. // start at end of default color list and walk back to index 0
  852. var defIndex = defaultColors.length;
  853. return function(series, index) {
  854. var key = getKey(series);
  855. if (nv.utils.isFunction(dictionary[key])) {
  856. return dictionary[key]();
  857. } else if (dictionary[key] !== undefined) {
  858. return dictionary[key];
  859. } else {
  860. // no match in dictionary, use a default color
  861. if (!defIndex) {
  862. // used all the default colors, start over
  863. defIndex = defaultColors.length;
  864. }
  865. defIndex = defIndex - 1;
  866. return defaultColors[defIndex];
  867. }
  868. };
  869. };
  870. /*
  871. From the PJAX example on d3js.org, while this is not really directly needed
  872. it's a very cool method for doing pjax, I may expand upon it a little bit,
  873. open to suggestions on anything that may be useful
  874. */
  875. nv.utils.pjax = function(links, content) {
  876. var load = function(href) {
  877. d3.html(href, function(fragment) {
  878. var target = d3.select(content).node();
  879. target.parentNode.replaceChild(
  880. d3.select(fragment).select(content).node(),
  881. target);
  882. nv.utils.pjax(links, content);
  883. });
  884. };
  885. d3.selectAll(links).on("click", function() {
  886. history.pushState(this.href, this.textContent, this.href);
  887. load(this.href);
  888. d3.event.preventDefault();
  889. });
  890. d3.select(window).on("popstate", function() {
  891. if (d3.event.state) {
  892. load(d3.event.state);
  893. }
  894. });
  895. };
  896. /*
  897. For when we want to approximate the width in pixels for an SVG:text element.
  898. Most common instance is when the element is in a display:none; container.
  899. Forumla is : text.length * font-size * constant_factor
  900. */
  901. nv.utils.calcApproxTextWidth = function (svgTextElem) {
  902. if (nv.utils.isFunction(svgTextElem.style) && nv.utils.isFunction(svgTextElem.text)) {
  903. var fontSize = parseInt(svgTextElem.style("font-size").replace("px",""), 10);
  904. var textLength = svgTextElem.text().length;
  905. return nv.utils.NaNtoZero(textLength * fontSize * 0.5);
  906. }
  907. return 0;
  908. };
  909. /*
  910. Numbers that are undefined, null or NaN, convert them to zeros.
  911. */
  912. nv.utils.NaNtoZero = function(n) {
  913. if (!nv.utils.isNumber(n)
  914. || isNaN(n)
  915. || n === null
  916. || n === Infinity
  917. || n === -Infinity) {
  918. return 0;
  919. }
  920. return n;
  921. };
  922. /*
  923. Add a way to watch for d3 transition ends to d3
  924. */
  925. d3.selection.prototype.watchTransition = function(renderWatch){
  926. var args = [this].concat([].slice.call(arguments, 1));
  927. return renderWatch.transition.apply(renderWatch, args);
  928. };
  929. /*
  930. Helper object to watch when d3 has rendered something
  931. */
  932. nv.utils.renderWatch = function(dispatch, duration) {
  933. if (!(this instanceof nv.utils.renderWatch)) {
  934. return new nv.utils.renderWatch(dispatch, duration);
  935. }
  936. var _duration = duration !== undefined ? duration : 250;
  937. var renderStack = [];
  938. var self = this;
  939. this.models = function(models) {
  940. models = [].slice.call(arguments, 0);
  941. models.forEach(function(model){
  942. model.__rendered = false;
  943. (function(m){
  944. m.dispatch.on('renderEnd', function(arg){
  945. m.__rendered = true;
  946. self.renderEnd('model');
  947. });
  948. })(model);
  949. if (renderStack.indexOf(model) < 0) {
  950. renderStack.push(model);
  951. }
  952. });
  953. return this;
  954. };
  955. this.reset = function(duration) {
  956. if (duration !== undefined) {
  957. _duration = duration;
  958. }
  959. renderStack = [];
  960. };
  961. this.transition = function(selection, args, duration) {
  962. args = arguments.length > 1 ? [].slice.call(arguments, 1) : [];
  963. if (args.length > 1) {
  964. duration = args.pop();
  965. } else {
  966. duration = _duration !== undefined ? _duration : 250;
  967. }
  968. selection.__rendered = false;
  969. if (renderStack.indexOf(selection) < 0) {
  970. renderStack.push(selection);
  971. }
  972. if (duration === 0) {
  973. selection.__rendered = true;
  974. selection.delay = function() { return this; };
  975. selection.duration = function() { return this; };
  976. return selection;
  977. } else {
  978. if (selection.length === 0) {
  979. selection.__rendered = true;
  980. } else if (selection.every( function(d){ return !d.length; } )) {
  981. selection.__rendered = true;
  982. } else {
  983. selection.__rendered = false;
  984. }
  985. var n = 0;
  986. return selection
  987. .transition()
  988. .duration(duration)
  989. .each(function(){ ++n; })
  990. .each('end', function(d, i) {
  991. if (--n === 0) {
  992. selection.__rendered = true;
  993. self.renderEnd.apply(this, args);
  994. }
  995. });
  996. }
  997. };
  998. this.renderEnd = function() {
  999. if (renderStack.every( function(d){ return d.__rendered; } )) {
  1000. renderStack.forEach( function(d){ d.__rendered = false; });
  1001. dispatch.renderEnd.apply(this, arguments);
  1002. }
  1003. }
  1004. };
  1005. /*
  1006. Takes multiple objects and combines them into the first one (dst)
  1007. example: nv.utils.deepExtend({a: 1}, {a: 2, b: 3}, {c: 4});
  1008. gives: {a: 2, b: 3, c: 4}
  1009. */
  1010. nv.utils.deepExtend = function(dst){
  1011. var sources = arguments.length > 1 ? [].slice.call(arguments, 1) : [];
  1012. sources.forEach(function(source) {
  1013. for (var key in source) {
  1014. var isArray = nv.utils.isArray(dst[key]);
  1015. var isObject = nv.utils.isObject(dst[key]);
  1016. var srcObj = nv.utils.isObject(source[key]);
  1017. if (isObject && !isArray && srcObj) {
  1018. nv.utils.deepExtend(dst[key], source[key]);
  1019. } else {
  1020. dst[key] = source[key];
  1021. }
  1022. }
  1023. });
  1024. };
  1025. /*
  1026. state utility object, used to track d3 states in the models
  1027. */
  1028. nv.utils.state = function(){
  1029. if (!(this instanceof nv.utils.state)) {
  1030. return new nv.utils.state();
  1031. }
  1032. var state = {};
  1033. var _self = this;
  1034. var _setState = function(){};
  1035. var _getState = function(){ return {}; };
  1036. var init = null;
  1037. var changed = null;
  1038. this.dispatch = d3.dispatch('change', 'set');
  1039. this.dispatch.on('set', function(state){
  1040. _setState(state, true);
  1041. });
  1042. this.getter = function(fn){
  1043. _getState = fn;
  1044. return this;
  1045. };
  1046. this.setter = function(fn, callback) {
  1047. if (!callback) {
  1048. callback = function(){};
  1049. }
  1050. _setState = function(state, update){
  1051. fn(state);
  1052. if (update) {
  1053. callback();
  1054. }
  1055. };
  1056. return this;
  1057. };
  1058. this.init = function(state){
  1059. init = init || {};
  1060. nv.utils.deepExtend(init, state);
  1061. };
  1062. var _set = function(){
  1063. var settings = _getState();
  1064. if (JSON.stringify(settings) === JSON.stringify(state)) {
  1065. return false;
  1066. }
  1067. for (var key in settings) {
  1068. if (state[key] === undefined) {
  1069. state[key] = {};
  1070. }
  1071. state[key] = settings[key];
  1072. changed = true;
  1073. }
  1074. return true;
  1075. };
  1076. this.update = function(){
  1077. if (init) {
  1078. _setState(init, false);
  1079. init = null;
  1080. }
  1081. if (_set.call(this)) {
  1082. this.dispatch.change(state);
  1083. }
  1084. };
  1085. };
  1086. /*
  1087. Snippet of code you can insert into each nv.models.* to give you the ability to
  1088. do things like:
  1089. chart.options({
  1090. showXAxis: true,
  1091. tooltips: true
  1092. });
  1093. To enable in the chart:
  1094. chart.options = nv.utils.optionsFunc.bind(chart);
  1095. */
  1096. nv.utils.optionsFunc = function(args) {
  1097. if (args) {
  1098. d3.map(args).forEach((function(key,value) {
  1099. if (nv.utils.isFunction(this[key])) {
  1100. this[key](value);
  1101. }
  1102. }).bind(this));
  1103. }
  1104. return this;
  1105. };
  1106. /*
  1107. numTicks: requested number of ticks
  1108. data: the chart data
  1109. returns the number of ticks to actually use on X axis, based on chart data
  1110. to avoid duplicate ticks with the same value
  1111. */
  1112. nv.utils.calcTicksX = function(numTicks, data) {
  1113. // find max number of values from all data streams
  1114. var numValues = 1;
  1115. var i = 0;
  1116. for (i; i < data.length; i += 1) {
  1117. var stream_len = data[i] && data[i].values ? data[i].values.length : 0;
  1118. numValues = stream_len > numValues ? stream_len : numValues;
  1119. }
  1120. nv.log("Requested number of ticks: ", numTicks);
  1121. nv.log("Calculated max values to be: ", numValues);
  1122. // make sure we don't have more ticks than values to avoid duplicates
  1123. numTicks = numTicks > numValues ? numTicks = numValues - 1 : numTicks;
  1124. // make sure we have at least one tick
  1125. numTicks = numTicks < 1 ? 1 : numTicks;
  1126. // make sure it's an integer
  1127. numTicks = Math.floor(numTicks);
  1128. nv.log("Calculating tick count as: ", numTicks);
  1129. return numTicks;
  1130. };
  1131. /*
  1132. returns number of ticks to actually use on Y axis, based on chart data
  1133. */
  1134. nv.utils.calcTicksY = function(numTicks, data) {
  1135. // currently uses the same logic but we can adjust here if needed later
  1136. return nv.utils.calcTicksX(numTicks, data);
  1137. };
  1138. /*
  1139. Add a particular option from an options object onto chart
  1140. Options exposed on a chart are a getter/setter function that returns chart
  1141. on set to mimic typical d3 option chaining, e.g. svg.option1('a').option2('b');
  1142. option objects should be generated via Object.create() to provide
  1143. the option of manipulating data via get/set functions.
  1144. */
  1145. nv.utils.initOption = function(chart, name) {
  1146. // if it's a call option, just call it directly, otherwise do get/set
  1147. if (chart._calls && chart._calls[name]) {
  1148. chart[name] = chart._calls[name];
  1149. } else {
  1150. chart[name] = function (_) {
  1151. if (!arguments.length) return chart._options[name];
  1152. chart._overrides[name] = true;
  1153. chart._options[name] = _;
  1154. return chart;
  1155. };
  1156. // calling the option as _option will ignore if set by option already
  1157. // so nvd3 can set options internally but the stop if set manually
  1158. chart['_' + name] = function(_) {
  1159. if (!arguments.length) return chart._options[name];
  1160. if (!chart._overrides[name]) {
  1161. chart._options[name] = _;
  1162. }
  1163. return chart;
  1164. }
  1165. }
  1166. };
  1167. /*
  1168. Add all options in an options object to the chart
  1169. */
  1170. nv.utils.initOptions = function(chart) {
  1171. chart._overrides = chart._overrides || {};
  1172. var ops = Object.getOwnPropertyNames(chart._options || {});
  1173. var calls = Object.getOwnPropertyNames(chart._calls || {});
  1174. ops = ops.concat(calls);
  1175. for (var i in ops) {
  1176. nv.utils.initOption(chart, ops[i]);
  1177. }
  1178. };
  1179. /*
  1180. Inherit options from a D3 object
  1181. d3.rebind makes calling the function on target actually call it on source
  1182. Also use _d3options so we can track what we inherit for documentation and chained inheritance
  1183. */
  1184. nv.utils.inheritOptionsD3 = function(target, d3_source, oplist) {
  1185. target._d3options = oplist.concat(target._d3options || []);
  1186. oplist.unshift(d3_source);
  1187. oplist.unshift(target);
  1188. d3.rebind.apply(this, oplist);
  1189. };
  1190. /*
  1191. Remove duplicates from an array
  1192. */
  1193. nv.utils.arrayUnique = function(a) {
  1194. return a.sort().filter(function(item, pos) {
  1195. return !pos || item != a[pos - 1];
  1196. });
  1197. };
  1198. /*
  1199. Keeps a list of custom symbols to draw from in addition to d3.svg.symbol
  1200. Necessary since d3 doesn't let you extend its list -_-
  1201. Add new symbols by doing nv.utils.symbols.set('name', function(size){...});
  1202. */
  1203. nv.utils.symbolMap = d3.map();
  1204. /*
  1205. Replaces d3.svg.symbol so that we can look both there and our own map
  1206. */
  1207. nv.utils.symbol = function() {
  1208. var type,
  1209. size = 64;
  1210. function symbol(d,i) {
  1211. var t = type.call(this,d,i);
  1212. var s = size.call(this,d,i);
  1213. if (d3.svg.symbolTypes.indexOf(t) !== -1) {
  1214. return d3.svg.symbol().type(t).size(s)();
  1215. } else {
  1216. return nv.utils.symbolMap.get(t)(s);
  1217. }
  1218. }
  1219. symbol.type = function(_) {
  1220. if (!arguments.length) return type;
  1221. type = d3.functor(_);
  1222. return symbol;
  1223. };
  1224. symbol.size = function(_) {
  1225. if (!arguments.length) return size;
  1226. size = d3.functor(_);
  1227. return symbol;
  1228. };
  1229. return symbol;
  1230. };
  1231. /*
  1232. Inherit option getter/setter functions from source to target
  1233. d3.rebind makes calling the function on target actually call it on source
  1234. Also track via _inherited and _d3options so we can track what we inherit
  1235. for documentation generation purposes and chained inheritance
  1236. */
  1237. nv.utils.inheritOptions = function(target, source) {
  1238. // inherit all the things
  1239. var ops = Object.getOwnPropertyNames(source._options || {});
  1240. var calls = Object.getOwnPropertyNames(source._calls || {});
  1241. var inherited = source._inherited || [];
  1242. var d3ops = source._d3options || [];
  1243. var args = ops.concat(calls).concat(inherited).concat(d3ops);
  1244. args.unshift(source);
  1245. args.unshift(target);
  1246. d3.rebind.apply(this, args);
  1247. // pass along the lists to keep track of them, don't allow duplicates
  1248. target._inherited = nv.utils.arrayUnique(ops.concat(calls).concat(inherited).concat(ops).concat(target._inherited || []));
  1249. target._d3options = nv.utils.arrayUnique(d3ops.concat(target._d3options || []));
  1250. };
  1251. /*
  1252. Runs common initialize code on the svg before the chart builds
  1253. */
  1254. nv.utils.initSVG = function(svg) {
  1255. svg.classed({'nvd3-svg':true});
  1256. };
  1257. /*
  1258. Sanitize and provide default for the container height.
  1259. */
  1260. nv.utils.sanitizeHeight = function(height, container) {
  1261. return (height || parseInt(container.style('height'), 10) || 400);
  1262. };
  1263. /*
  1264. Sanitize and provide default for the container width.
  1265. */
  1266. nv.utils.sanitizeWidth = function(width, container) {
  1267. return (width || parseInt(container.style('width'), 10) || 960);
  1268. };
  1269. /*
  1270. Calculate the available height for a chart.
  1271. */
  1272. nv.utils.availableHeight = function(height, container, margin) {
  1273. return Math.max(0,nv.utils.sanitizeHeight(height, container) - margin.top - margin.bottom);
  1274. };
  1275. /*
  1276. Calculate the available width for a chart.
  1277. */
  1278. nv.utils.availableWidth = function(width, container, margin) {
  1279. return Math.max(0,nv.utils.sanitizeWidth(width, container) - margin.left - margin.right);
  1280. };
  1281. /*
  1282. Clear any rendered chart components and display a chart's 'noData' message
  1283. */
  1284. nv.utils.noData = function(chart, container) {
  1285. var opt = chart.options(),
  1286. margin = opt.margin(),
  1287. noData = opt.noData(),
  1288. data = (noData == null) ? ["No Data Available."] : [noData],
  1289. height = nv.utils.availableHeight(null, container, margin),
  1290. width = nv.utils.availableWidth(null, container, margin),
  1291. x = margin.left + width/2,
  1292. y = margin.top + height/2;
  1293. //Remove any previously created chart components
  1294. container.selectAll('g').remove();
  1295. var noDataText = container.selectAll('.nv-noData').data(data);
  1296. noDataText.enter().append('text')
  1297. .attr('class', 'nvd3 nv-noData')
  1298. .attr('dy', '-.7em')
  1299. .style('text-anchor', 'middle');
  1300. noDataText
  1301. .attr('x', x)
  1302. .attr('y', y)
  1303. .text(function(t){ return t; });
  1304. };
  1305. /*
  1306. Wrap long labels.
  1307. */
  1308. nv.utils.wrapTicks = function (text, width) {
  1309. text.each(function() {
  1310. var text = d3.select(this),
  1311. words = text.text().split(/\s+/).reverse(),
  1312. word,
  1313. line = [],
  1314. lineNumber = 0,
  1315. lineHeight = 1.1,
  1316. y = text.attr("y"),
  1317. dy = parseFloat(text.attr("dy")),
  1318. tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
  1319. while (word = words.pop()) {
  1320. line.push(word);
  1321. tspan.text(line.join(" "));
  1322. if (tspan.node().getComputedTextLength() > width) {
  1323. line.pop();
  1324. tspan.text(line.join(" "));
  1325. line = [word];
  1326. tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
  1327. }
  1328. }
  1329. });
  1330. };
  1331. /*
  1332. Check equality of 2 array
  1333. */
  1334. nv.utils.arrayEquals = function (array1, array2) {
  1335. if (array1 === array2)
  1336. return true;
  1337. if (!array1 || !array2)
  1338. return false;
  1339. // compare lengths - can save a lot of time
  1340. if (array1.length != array2.length)
  1341. return false;
  1342. for (var i = 0,
  1343. l = array1.length; i < l; i++) {
  1344. // Check if we have nested arrays
  1345. if (array1[i] instanceof Array && array2[i] instanceof Array) {
  1346. // recurse into the nested arrays
  1347. if (!nv.arrayEquals(array1[i], array2[i]))
  1348. return false;
  1349. } else if (array1[i] != array2[i]) {
  1350. // Warning - two different object instances will never be equal: {x:20} != {x:20}
  1351. return false;
  1352. }
  1353. }
  1354. return true;
  1355. };nv.models.axis = function() {
  1356. "use strict";
  1357. //============================================================
  1358. // Public Variables with Default Settings
  1359. //------------------------------------------------------------
  1360. var axis = d3.svg.axis();
  1361. var scale = d3.scale.linear();
  1362. var margin = {top: 0, right: 0, bottom: 0, left: 0}
  1363. , width = 75 //only used for tickLabel currently
  1364. , height = 60 //only used for tickLabel currently
  1365. , axisLabelText = null
  1366. , showMaxMin = true //TODO: showMaxMin should be disabled on all ordinal scaled axes
  1367. , rotateLabels = 0
  1368. , rotateYLabel = true
  1369. , staggerLabels = false
  1370. , isOrdinal = false
  1371. , ticks = null
  1372. , axisLabelDistance = 0
  1373. , fontSize = undefined
  1374. , duration = 250
  1375. , dispatch = d3.dispatch('renderEnd')
  1376. ;
  1377. axis
  1378. .scale(scale)
  1379. .orient('bottom')
  1380. .tickFormat(function(d) { return d })
  1381. ;
  1382. //============================================================
  1383. // Private Variables
  1384. //------------------------------------------------------------
  1385. var scale0;
  1386. var renderWatch = nv.utils.renderWatch(dispatch, duration);
  1387. function chart(selection) {
  1388. renderWatch.reset();
  1389. selection.each(function(data) {
  1390. var container = d3.select(this);
  1391. nv.utils.initSVG(container);
  1392. // Setup containers and skeleton of chart
  1393. var wrap = container.selectAll('g.nv-wrap.nv-axis').data([data]);
  1394. var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-axis');
  1395. var gEnter = wrapEnter.append('g');
  1396. var g = wrap.select('g');
  1397. if (ticks !== null)
  1398. axis.ticks(ticks);
  1399. else if (axis.orient() == 'top' || axis.orient() == 'bottom')
  1400. axis.ticks(Math.abs(scale.range()[1] - scale.range()[0]) / 100);
  1401. //TODO: consider calculating width/height based on whether or not label is added, for reference in charts using this component
  1402. g.watchTransition(renderWatch, 'axis').call(axis);
  1403. scale0 = scale0 || axis.scale();
  1404. var fmt = axis.tickFormat();
  1405. if (fmt == null) {
  1406. fmt = scale0.tickFormat();
  1407. }
  1408. var axisLabel = g.selectAll('text.nv-axislabel')
  1409. .data([axisLabelText || null]);
  1410. axisLabel.exit().remove();
  1411. //only skip when fontSize is undefined so it can be cleared with a null or blank string
  1412. if (fontSize !== undefined) {
  1413. g.selectAll('g').select("text").style('font-size', fontSize);
  1414. }
  1415. var xLabelMargin;
  1416. var axisMaxMin;
  1417. var w;
  1418. switch (axis.orient()) {
  1419. case 'top':
  1420. axisLabel.enter().append('text').attr('class', 'nv-axislabel');
  1421. w = 0;
  1422. if (scale.range().length === 1) {
  1423. w = isOrdinal ? scale.range()[0] * 2 + scale.rangeBand() : 0;
  1424. } else if (scale.range().length === 2) {
  1425. w = isOrdinal ? scale.range()[0] + scale.range()[1] + scale.rangeBand() : scale.range()[1];
  1426. } else if ( scale.range().length > 2){
  1427. w = scale.range()[scale.range().length-1]+(scale.range()[1]-scale.range()[0]);
  1428. };
  1429. axisLabel
  1430. .attr('text-anchor', 'middle')
  1431. .attr('y', 0)
  1432. .attr('x', w/2);
  1433. if (showMaxMin) {
  1434. axisMaxMin = wrap.selectAll('g.nv-axisMaxMin')
  1435. .data(scale.domain());
  1436. axisMaxMin.enter().append('g').attr('class',function(d,i){
  1437. return ['nv-axisMaxMin','nv-axisMaxMin-x',(i == 0 ? 'nv-axisMin-x':'nv-axisMax-x')].join(' ')
  1438. }).append('text');
  1439. axisMaxMin.exit().remove();
  1440. axisMaxMin
  1441. .attr('transform', function(d,i) {
  1442. return 'translate(' + nv.utils.NaNtoZero(scale(d)) + ',0)'
  1443. })
  1444. .select('text')
  1445. .attr('dy', '-0.5em')
  1446. .attr('y', -axis.tickPadding())
  1447. .attr('text-anchor', 'middle')
  1448. .text(function(d,i) {
  1449. var v = fmt(d);
  1450. return ('' + v).match('NaN') ? '' : v;
  1451. });
  1452. axisMaxMin.watchTransition(renderWatch, 'min-max top')
  1453. .attr('transform', function(d,i) {
  1454. return 'translate(' + nv.utils.NaNtoZero(scale.range()[i]) + ',0)'
  1455. });
  1456. }
  1457. break;
  1458. case 'bottom':
  1459. xLabelMargin = axisLabelDistance + 36;
  1460. var maxTextWidth = 30;
  1461. var textHeight = 0;
  1462. var xTicks = g.selectAll('g').select("text");
  1463. var rotateLabelsRule = '';
  1464. if (rotateLabels%360) {
  1465. //Reset transform on ticks so textHeight can be calculated correctly
  1466. xTicks.attr('transform', '');
  1467. //Calculate the longest xTick width
  1468. xTicks.each(function(d,i){
  1469. var box = this.getBoundingClientRect();
  1470. var width = box.width;
  1471. textHeight = box.height;
  1472. if(width > maxTextWidth) maxTextWidth = width;
  1473. });
  1474. rotateLabelsRule = 'rotate(' + rotateLabels + ' 0,' + (textHeight/2 + axis.tickPadding()) + ')';
  1475. //Convert to radians before calculating sin. Add 30 to margin for healthy padding.
  1476. var sin = Math.abs(Math.sin(rotateLabels*Math.PI/180));
  1477. xLabelMargin = (sin ? sin*maxTextWidth : maxTextWidth)+30;
  1478. //Rotate all xTicks
  1479. xTicks
  1480. .attr('transform', rotateLabelsRule)
  1481. .style('text-anchor', rotateLabels%360 > 0 ? 'start' : 'end');
  1482. } else {
  1483. if (staggerLabels) {
  1484. xTicks
  1485. .attr('transform', function(d,i) {
  1486. return 'translate(0,' + (i % 2 == 0 ? '0' : '12') + ')'
  1487. });
  1488. } else {
  1489. xTicks.attr('transform', "translate(0,0)");
  1490. }
  1491. }
  1492. axisLabel.enter().append('text').attr('class', 'nv-axislabel');
  1493. w = 0;
  1494. if (scale.range().length === 1) {
  1495. w = isOrdinal ? scale.range()[0] * 2 + scale.rangeBand() : 0;
  1496. } else if (scale.range().length === 2) {
  1497. w = isOrdinal ? scale.range()[0] + scale.range()[1] + scale.rangeBand() : scale.range()[1];
  1498. } else if ( scale.range().length > 2){
  1499. w = scale.range()[scale.range().length-1]+(scale.range()[1]-scale.range()[0]);
  1500. };
  1501. axisLabel
  1502. .attr('text-anchor', 'middle')
  1503. .attr('y', xLabelMargin)
  1504. .attr('x', w/2);
  1505. if (showMaxMin) {
  1506. //if (showMaxMin && !isOrdinal) {
  1507. axisMaxMin = wrap.selectAll('g.nv-axisMaxMin')
  1508. //.data(scale.domain())
  1509. .data([scale.domain()[0], scale.domain()[scale.domain().length - 1]]);
  1510. axisMaxMin.enter().append('g').attr('class',function(d,i){
  1511. return ['nv-axisMaxMin','nv-axisMaxMin-x',(i == 0 ? 'nv-axisMin-x':'nv-axisMax-x')].join(' ')
  1512. }).append('text');
  1513. axisMaxMin.exit().remove();
  1514. axisMaxMin
  1515. .attr('transform', function(d,i) {
  1516. return 'translate(' + nv.utils.NaNtoZero((scale(d) + (isOrdinal ? scale.rangeBand() / 2 : 0))) + ',0)'
  1517. })
  1518. .select('text')
  1519. .attr('dy', '.71em')
  1520. .attr('y', axis.tickPadding())
  1521. .attr('transform', rotateLabelsRule)
  1522. .style('text-anchor', rotateLabels ? (rotateLabels%360 > 0 ? 'start' : 'end') : 'middle')
  1523. .text(function(d,i) {
  1524. var v = fmt(d);
  1525. return ('' + v).match('NaN') ? '' : v;
  1526. });
  1527. axisMaxMin.watchTransition(renderWatch, 'min-max bottom')
  1528. .attr('transform', function(d,i) {
  1529. return 'translate(' + nv.utils.NaNtoZero((scale(d) + (isOrdinal ? scale.rangeBand() / 2 : 0))) + ',0)'
  1530. });
  1531. }
  1532. break;
  1533. case 'right':
  1534. axisLabel.enter().append('text').attr('class', 'nv-axislabel');
  1535. axisLabel
  1536. .style('text-anchor', rotateYLabel ? 'middle' : 'begin')
  1537. .attr('transform', rotateYLabel ? 'rotate(90)' : '')
  1538. .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
  1539. .attr('x', rotateYLabel ? (d3.max(scale.range()) / 2) : axis.tickPadding());
  1540. if (showMaxMin) {
  1541. axisMaxMin = wrap.selectAll('g.nv-axisMaxMin')
  1542. .data(scale.domain());
  1543. axisMaxMin.enter().append('g').attr('class',function(d,i){
  1544. return ['nv-axisMaxMin','nv-axisMaxMin-y',(i == 0 ? 'nv-axisMin-y':'nv-axisMax-y')].join(' ')
  1545. }).append('text')
  1546. .style('opacity', 0);
  1547. axisMaxMin.exit().remove();
  1548. axisMaxMin
  1549. .attr('transform', function(d,i) {
  1550. return 'translate(0,' + nv.utils.NaNtoZero(scale(d)) + ')'
  1551. })
  1552. .select('text')
  1553. .attr('dy', '.32em')
  1554. .attr('y', 0)
  1555. .attr('x', axis.tickPadding())
  1556. .style('text-anchor', 'start')
  1557. .text(function(d, i) {
  1558. var v = fmt(d);
  1559. return ('' + v).match('NaN') ? '' : v;
  1560. });
  1561. axisMaxMin.watchTransition(renderWatch, 'min-max right')
  1562. .attr('transform', function(d,i) {
  1563. return 'translate(0,' + nv.utils.NaNtoZero(scale.range()[i]) + ')'
  1564. })
  1565. .select('text')
  1566. .style('opacity', 1);
  1567. }
  1568. break;
  1569. case 'left':
  1570. /*
  1571. //For dynamically placing the label. Can be used with dynamically-sized chart axis margins
  1572. var yTicks = g.selectAll('g').select("text");
  1573. yTicks.each(function(d,i){
  1574. var labelPadding = this.getBoundingClientRect().width + axis.tickPadding() + 16;
  1575. if(labelPadding > width) width = labelPadding;
  1576. });
  1577. */
  1578. axisLabel.enter().append('text').attr('class', 'nv-axislabel');
  1579. axisLabel
  1580. .style('text-anchor', rotateYLabel ? 'middle' : 'end')
  1581. .attr('transform', rotateYLabel ? 'rotate(-90)' : '')
  1582. .attr('y', rotateYLabel ? (-Math.max(margin.left, width) + 25 - (axisLabelDistance || 0)) : -10)
  1583. .attr('x', rotateYLabel ? (-d3.max(scale.range()) / 2) : -axis.tickPadding());
  1584. if (showMaxMin) {
  1585. axisMaxMin = wrap.selectAll('g.nv-axisMaxMin')
  1586. .data(scale.domain());
  1587. axisMaxMin.enter().append('g').attr('class',function(d,i){
  1588. return ['nv-axisMaxMin','nv-axisMaxMin-y',(i == 0 ? 'nv-axisMin-y':'nv-axisMax-y')].join(' ')
  1589. }).append('text')
  1590. .style('opacity', 0);
  1591. axisMaxMin.exit().remove();
  1592. axisMaxMin
  1593. .attr('transform', function(d,i) {
  1594. return 'translate(0,' + nv.utils.NaNtoZero(scale0(d)) + ')'
  1595. })
  1596. .select('text')
  1597. .attr('dy', '.32em')
  1598. .attr('y', 0)
  1599. .attr('x', -axis.tickPadding())
  1600. .attr('text-anchor', 'end')
  1601. .text(function(d,i) {
  1602. var v = fmt(d);
  1603. return ('' + v).match('NaN') ? '' : v;
  1604. });
  1605. axisMaxMin.watchTransition(renderWatch, 'min-max right')
  1606. .attr('transform', function(d,i) {
  1607. return 'translate(0,' + nv.utils.NaNtoZero(scale.range()[i]) + ')'
  1608. })
  1609. .select('text')
  1610. .style('opacity', 1);
  1611. }
  1612. break;
  1613. }
  1614. axisLabel.text(function(d) { return d });
  1615. if (showMaxMin && (axis.orient() === 'left' || axis.orient() === 'right')) {
  1616. //check if max and min overlap other values, if so, hide the values that overlap
  1617. g.selectAll('g') // the g's wrapping each tick
  1618. .each(function(d,i) {
  1619. d3.select(this).select('text').attr('opacity', 1);
  1620. 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!
  1621. if (d > 1e-10 || d < -1e-10) // accounts for minor floating point errors... though could be problematic if the scale is EXTREMELY SMALL
  1622. d3.select(this).attr('opacity', 0);
  1623. d3.select(this).select('text').attr('opacity', 0); // Don't remove the ZERO line!!
  1624. }
  1625. });
  1626. //if Max and Min = 0 only show min, Issue #281
  1627. if (scale.domain()[0] == scale.domain()[1] && scale.domain()[0] == 0) {
  1628. wrap.selectAll('g.nv-axisMaxMin').style('opacity', function (d, i) {
  1629. return !i ? 1 : 0
  1630. });
  1631. }
  1632. }
  1633. if (showMaxMin && (axis.orient() === 'top' || axis.orient() === 'bottom')) {
  1634. var maxMinRange = [];
  1635. wrap.selectAll('g.nv-axisMaxMin')
  1636. .each(function(d,i) {
  1637. try {
  1638. if (i) // i== 1, max position
  1639. 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)
  1640. else // i==0, min position
  1641. maxMinRange.push(scale(d) + this.getBoundingClientRect().width + 4)
  1642. }catch (err) {
  1643. if (i) // i== 1, max position
  1644. 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)
  1645. else // i==0, min position
  1646. maxMinRange.push(scale(d) + 4);
  1647. }
  1648. });
  1649. // the g's wrapping each tick
  1650. g.selectAll('g').each(function(d, i) {
  1651. if (scale(d) < maxMinRange[0] || scale(d) > maxMinRange[1]) {
  1652. if (d > 1e-10 || d < -1e-10) // accounts for minor floating point errors... though could be problematic if the scale is EXTREMELY SMALL
  1653. d3.select(this).remove();
  1654. else
  1655. d3.select(this).select('text').remove(); // Don't remove the ZERO line!!
  1656. }
  1657. });
  1658. }
  1659. //Highlight zero tick line
  1660. g.selectAll('.tick')
  1661. .filter(function (d) {
  1662. /*
  1663. The filter needs to return only ticks at or near zero.
  1664. Numbers like 0.00001 need to count as zero as well,
  1665. and the arithmetic trick below solves that.
  1666. */
  1667. return !parseFloat(Math.round(d * 100000) / 1000000) && (d !== undefined)
  1668. })
  1669. .classed('zero', true);
  1670. //store old scales for use in transitions on update
  1671. scale0 = scale.copy();
  1672. });
  1673. renderWatch.renderEnd('axis immediate');
  1674. return chart;
  1675. }
  1676. //============================================================
  1677. // Expose Public Variables
  1678. //------------------------------------------------------------
  1679. // expose chart's sub-components
  1680. chart.axis = axis;
  1681. chart.dispatch = dispatch;
  1682. chart.options = nv.utils.optionsFunc.bind(chart);
  1683. chart._options = Object.create({}, {
  1684. // simple options, just get/set the necessary values
  1685. axisLabelDistance: {get: function(){return axisLabelDistance;}, set: function(_){axisLabelDistance=_;}},
  1686. staggerLabels: {get: function(){return staggerLabels;}, set: function(_){staggerLabels=_;}},
  1687. rotateLabels: {get: function(){return rotateLabels;}, set: function(_){rotateLabels=_;}},
  1688. rotateYLabel: {get: function(){return rotateYLabel;}, set: function(_){rotateYLabel=_;}},
  1689. showMaxMin: {get: function(){return showMaxMin;}, set: function(_){showMaxMin=_;}},
  1690. axisLabel: {get: function(){return axisLabelText;}, set: function(_){axisLabelText=_;}},
  1691. height: {get: function(){return height;}, set: function(_){height=_;}},
  1692. ticks: {get: function(){return ticks;}, set: function(_){ticks=_;}},
  1693. width: {get: function(){return width;}, set: function(_){width=_;}},
  1694. fontSize: {get: function(){return fontSize;}, set: function(_){fontSize=_;}},
  1695. // options that require extra logic in the setter
  1696. margin: {get: function(){return margin;}, set: function(_){
  1697. margin.top = _.top !== undefined ? _.top : margin.top;
  1698. margin.right = _.right !== undefined ? _.right : margin.right;
  1699. margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
  1700. margin.left = _.left !== undefined ? _.left : margin.left;
  1701. }},
  1702. duration: {get: function(){return duration;}, set: function(_){
  1703. duration=_;
  1704. renderWatch.reset(duration);
  1705. }},
  1706. scale: {get: function(){return scale;}, set: function(_){
  1707. scale = _;
  1708. axis.scale(scale);
  1709. isOrdinal = typeof scale.rangeBands === 'function';
  1710. nv.utils.inheritOptionsD3(chart, scale, ['domain', 'range', 'rangeBand', 'rangeBands']);
  1711. }}
  1712. });
  1713. nv.utils.initOptions(chart);
  1714. nv.utils.inheritOptionsD3(chart, axis, ['orient', 'tickValues', 'tickSubdivide', 'tickSize', 'tickPadding', 'tickFormat']);
  1715. nv.utils.inheritOptionsD3(chart, scale, ['domain', 'range', 'rangeBand', 'rangeBands']);
  1716. return chart;
  1717. };
  1718. nv.models.scatter = function() {
  1719. "use strict";
  1720. //============================================================
  1721. // Public Variables with Default Settings
  1722. //------------------------------------------------------------
  1723. var margin = {top: 0, right: 0, bottom: 0, left: 0}
  1724. , width = null
  1725. , height = null
  1726. , color = nv.utils.defaultColor() // chooses color
  1727. , id = Math.floor(Math.random() * 100000) //Create semi-unique ID incase user doesn't select one
  1728. , container = null
  1729. , x = d3.scale.linear()
  1730. , y = d3.scale.linear()
  1731. , z = d3.scale.linear() //linear because d3.svg.shape.size is treated as area
  1732. , getX = function(d) { return d.x } // accessor to get the x value
  1733. , getY = function(d) { return d.y } // accessor to get the y value
  1734. , getSize = function(d) { return d.size || 1} // accessor to get the point size
  1735. , getShape = function(d) { return d.shape || 'circle' } // accessor to get point shape
  1736. , forceX = [] // List of numbers to Force into the X scale (ie. 0, or a max / min, etc.)
  1737. , forceY = [] // List of numbers to Force into the Y scale
  1738. , forceSize = [] // List of numbers to Force into the Size scale
  1739. , interactive = true // If true, plots a voronoi overlay for advanced point intersection
  1740. , pointActive = function(d) { return !d.notActive } // any points that return false will be filtered out
  1741. , padData = false // If true, adds half a data points width to front and back, for lining up a line chart with a bar chart
  1742. , padDataOuter = .1 //outerPadding to imitate ordinal scale outer padding
  1743. , clipEdge = false // if true, masks points within x and y scale
  1744. , clipVoronoi = true // if true, masks each point with a circle... can turn off to slightly increase performance
  1745. , showVoronoi = false // display the voronoi areas
  1746. , clipRadius = function() { return 25 } // function to get the radius for voronoi point clips
  1747. , xDomain = null // Override x domain (skips the calculation from data)
  1748. , yDomain = null // Override y domain
  1749. , xRange = null // Override x range
  1750. , yRange = null // Override y range
  1751. , sizeDomain = null // Override point size domain
  1752. , sizeRange = null
  1753. , singlePoint = false
  1754. , dispatch = d3.dispatch('elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'renderEnd')
  1755. , useVoronoi = true
  1756. , duration = 250
  1757. , interactiveUpdateDelay = 300
  1758. , showLabels = false
  1759. ;
  1760. //============================================================
  1761. // Private Variables
  1762. //------------------------------------------------------------
  1763. var x0, y0, z0 // used to store previous scales
  1764. , timeoutID
  1765. , needsUpdate = false // Flag for when the points are visually updating, but the interactive layer is behind, to disable tooltips
  1766. , renderWatch = nv.utils.renderWatch(dispatch, duration)
  1767. , _sizeRange_def = [16, 256]
  1768. , _caches
  1769. ;
  1770. function getCache(d) {
  1771. var cache, i;
  1772. cache = _caches = _caches || {};
  1773. i = d[0].series;
  1774. cache = cache[i] = cache[i] || {};
  1775. i = d[1];
  1776. cache = cache[i] = cache[i] || {};
  1777. return cache;
  1778. }
  1779. function getDiffs(d) {
  1780. var i, key,
  1781. point = d[0],
  1782. cache = getCache(d),
  1783. diffs = false;
  1784. for (i = 1; i < arguments.length; i ++) {
  1785. key = arguments[i];
  1786. if (cache[key] !== point[key] || !cache.hasOwnProperty(key)) {
  1787. cache[key] = point[key];
  1788. diffs = true;
  1789. }
  1790. }
  1791. return diffs;
  1792. }
  1793. function chart(selection) {
  1794. renderWatch.reset();
  1795. selection.each(function(data) {
  1796. container = d3.select(this);
  1797. var availableWidth = nv.utils.availableWidth(width, container, margin),
  1798. availableHeight = nv.utils.availableHeight(height, container, margin);
  1799. nv.utils.initSVG(container);
  1800. //add series index to each data point for reference
  1801. data.forEach(function(series, i) {
  1802. series.values.forEach(function(point) {
  1803. point.series = i;
  1804. });
  1805. });
  1806. // Setup Scales
  1807. var logScale = chart.yScale().name === d3.scale.log().name ? true : false;
  1808. // remap and flatten the data for use in calculating the scales' domains
  1809. 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
  1810. d3.merge(
  1811. data.map(function(d) {
  1812. return d.values.map(function(d,i) {
  1813. return { x: getX(d,i), y: getY(d,i), size: getSize(d,i) }
  1814. })
  1815. })
  1816. );
  1817. x .domain(xDomain || d3.extent(seriesData.map(function(d) { return d.x; }).concat(forceX)))
  1818. if (padData && data[0])
  1819. x.range(xRange || [(availableWidth * padDataOuter + availableWidth) / (2 *data[0].values.length), availableWidth - availableWidth * (1 + padDataOuter) / (2 * data[0].values.length) ]);
  1820. //x.range([availableWidth * .5 / data[0].values.length, availableWidth * (data[0].values.length - .5) / data[0].values.length ]);
  1821. else
  1822. x.range(xRange || [0, availableWidth]);
  1823. if (logScale) {
  1824. var min = d3.min(seriesData.map(function(d) { if (d.y !== 0) return d.y; }));
  1825. y.clamp(true)
  1826. .domain(yDomain || d3.extent(seriesData.map(function(d) {
  1827. if (d.y !== 0) return d.y;
  1828. else return min * 0.1;
  1829. }).concat(forceY)))
  1830. .range(yRange || [availableHeight, 0]);
  1831. } else {
  1832. y.domain(yDomain || d3.extent(seriesData.map(function (d) { return d.y;}).concat(forceY)))
  1833. .range(yRange || [availableHeight, 0]);
  1834. }
  1835. z .domain(sizeDomain || d3.extent(seriesData.map(function(d) { return d.size }).concat(forceSize)))
  1836. .range(sizeRange || _sizeRange_def);
  1837. // If scale's domain don't have a range, slightly adjust to make one... so a chart can show a single data point
  1838. singlePoint = x.domain()[0] === x.domain()[1] || y.domain()[0] === y.domain()[1];
  1839. if (x.domain()[0] === x.domain()[1])
  1840. x.domain()[0] ?
  1841. x.domain([x.domain()[0] - x.domain()[0] * 0.01, x.domain()[1] + x.domain()[1] * 0.01])
  1842. : x.domain([-1,1]);
  1843. if (y.domain()[0] === y.domain()[1])
  1844. y.domain()[0] ?
  1845. y.domain([y.domain()[0] - y.domain()[0] * 0.01, y.domain()[1] + y.domain()[1] * 0.01])
  1846. : y.domain([-1,1]);
  1847. if ( isNaN(x.domain()[0])) {
  1848. x.domain([-1,1]);
  1849. }
  1850. if ( isNaN(y.domain()[0])) {
  1851. y.domain([-1,1]);
  1852. }
  1853. x0 = x0 || x;
  1854. y0 = y0 || y;
  1855. z0 = z0 || z;
  1856. var scaleDiff = x(1) !== x0(1) || y(1) !== y0(1) || z(1) !== z0(1);
  1857. // Setup containers and skeleton of chart
  1858. var wrap = container.selectAll('g.nv-wrap.nv-scatter').data([data]);
  1859. var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-scatter nv-chart-' + id);
  1860. var defsEnter = wrapEnter.append('defs');
  1861. var gEnter = wrapEnter.append('g');
  1862. var g = wrap.select('g');
  1863. wrap.classed('nv-single-point', singlePoint);
  1864. gEnter.append('g').attr('class', 'nv-groups');
  1865. gEnter.append('g').attr('class', 'nv-point-paths');
  1866. wrapEnter.append('g').attr('class', 'nv-point-clips');
  1867. wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
  1868. defsEnter.append('clipPath')
  1869. .attr('id', 'nv-edge-clip-' + id)
  1870. .append('rect');
  1871. wrap.select('#nv-edge-clip-' + id + ' rect')
  1872. .attr('width', availableWidth)
  1873. .attr('height', (availableHeight > 0) ? availableHeight : 0);
  1874. g.attr('clip-path', clipEdge ? 'url(#nv-edge-clip-' + id + ')' : '');
  1875. function updateInteractiveLayer() {
  1876. // Always clear needs-update flag regardless of whether or not
  1877. // we will actually do anything (avoids needless invocations).
  1878. needsUpdate = false;
  1879. if (!interactive) return false;
  1880. // inject series and point index for reference into voronoi
  1881. if (useVoronoi === true) {
  1882. var vertices = d3.merge(data.map(function(group, groupIndex) {
  1883. return group.values
  1884. .map(function(point, pointIndex) {
  1885. // *Adding noise to make duplicates very unlikely
  1886. // *Injecting series and point index for reference
  1887. /* *Adding a 'jitter' to the points, because there's an issue in d3.geom.voronoi.
  1888. */
  1889. var pX = getX(point,pointIndex);
  1890. var pY = getY(point,pointIndex);
  1891. return [nv.utils.NaNtoZero(x(pX))+ Math.random() * 1e-4,
  1892. nv.utils.NaNtoZero(y(pY))+ Math.random() * 1e-4,
  1893. groupIndex,
  1894. pointIndex, point]; //temp hack to add noise until I think of a better way so there are no duplicates
  1895. })
  1896. .filter(function(pointArray, pointIndex) {
  1897. return pointActive(pointArray[4], pointIndex); // Issue #237.. move filter to after map, so pointIndex is correct!
  1898. })
  1899. })
  1900. );
  1901. if (vertices.length == 0) return false; // No active points, we're done
  1902. if (vertices.length < 3) {
  1903. // Issue #283 - Adding 2 dummy points to the voronoi b/c voronoi requires min 3 points to work
  1904. vertices.push([x.range()[0] - 20, y.range()[0] - 20, null, null]);
  1905. vertices.push([x.range()[1] + 20, y.range()[1] + 20, null, null]);
  1906. vertices.push([x.range()[0] - 20, y.range()[0] + 20, null, null]);
  1907. vertices.push([x.range()[1] + 20, y.range()[1] - 20, null, null]);
  1908. }
  1909. // keep voronoi sections from going more than 10 outside of graph
  1910. // to avoid overlap with other things like legend etc
  1911. var bounds = d3.geom.polygon([
  1912. [-10,-10],
  1913. [-10,height + 10],
  1914. [width + 10,height + 10],
  1915. [width + 10,-10]
  1916. ]);
  1917. var voronoi = d3.geom.voronoi(vertices).map(function(d, i) {
  1918. return {
  1919. 'data': bounds.clip(d),
  1920. 'series': vertices[i][2],
  1921. 'point': vertices[i][3]
  1922. }
  1923. });
  1924. // nuke all voronoi paths on reload and recreate them
  1925. wrap.select('.nv-point-paths').selectAll('path').remove();
  1926. var pointPaths = wrap.select('.nv-point-paths').selectAll('path').data(voronoi);
  1927. var vPointPaths = pointPaths
  1928. .enter().append("svg:path")
  1929. .attr("d", function(d) {
  1930. if (!d || !d.data || d.data.length === 0)
  1931. return 'M 0 0';
  1932. else
  1933. return "M" + d.data.join(",") + "Z";
  1934. })
  1935. .attr("id", function(d,i) {
  1936. return "nv-path-"+i; })
  1937. .attr("clip-path", function(d,i) { return "url(#nv-clip-"+id+"-"+i+")"; })
  1938. ;
  1939. // good for debugging point hover issues
  1940. if (showVoronoi) {
  1941. vPointPaths.style("fill", d3.rgb(230, 230, 230))
  1942. .style('fill-opacity', 0.4)
  1943. .style('stroke-opacity', 1)
  1944. .style("stroke", d3.rgb(200,200,200));
  1945. }
  1946. if (clipVoronoi) {
  1947. // voronoi sections are already set to clip,
  1948. // just create the circles with the IDs they expect
  1949. wrap.select('.nv-point-clips').selectAll('*').remove(); // must do * since it has sub-dom
  1950. var pointClips = wrap.select('.nv-point-clips').selectAll('clipPath').data(vertices);
  1951. var vPointClips = pointClips
  1952. .enter().append("svg:clipPath")
  1953. .attr("id", function(d, i) { return "nv-clip-"+id+"-"+i;})
  1954. .append("svg:circle")
  1955. .attr('cx', function(d) { return d[0]; })
  1956. .attr('cy', function(d) { return d[1]; })
  1957. .attr('r', clipRadius);
  1958. }
  1959. var mouseEventCallback = function(d, mDispatch) {
  1960. if (needsUpdate) return 0;
  1961. var series = data[d.series];
  1962. if (series === undefined) return;
  1963. var point = series.values[d.point];
  1964. point['color'] = color(series, d.series);
  1965. // standardize attributes for tooltip.
  1966. point['x'] = getX(point);
  1967. point['y'] = getY(point);
  1968. // can't just get box of event node since it's actually a voronoi polygon
  1969. var box = container.node().getBoundingClientRect();
  1970. var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
  1971. var scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
  1972. var pos = {
  1973. left: x(getX(point, d.point)) + box.left + scrollLeft + margin.left + 10,
  1974. top: y(getY(point, d.point)) + box.top + scrollTop + margin.top + 10
  1975. };
  1976. mDispatch({
  1977. point: point,
  1978. series: series,
  1979. pos: pos,
  1980. relativePos: [x(getX(point, d.point)) + margin.left, y(getY(point, d.point)) + margin.top],
  1981. seriesIndex: d.series,
  1982. pointIndex: d.point
  1983. });
  1984. };
  1985. pointPaths
  1986. .on('click', function(d) {
  1987. mouseEventCallback(d, dispatch.elementClick);
  1988. })
  1989. .on('dblclick', function(d) {
  1990. mouseEventCallback(d, dispatch.elementDblClick);
  1991. })
  1992. .on('mouseover', function(d) {
  1993. mouseEventCallback(d, dispatch.elementMouseover);
  1994. })
  1995. .on('mouseout', function(d, i) {
  1996. mouseEventCallback(d, dispatch.elementMouseout);
  1997. });
  1998. } else {
  1999. // add event handlers to points instead voronoi paths
  2000. wrap.select('.nv-groups').selectAll('.nv-group')
  2001. .selectAll('.nv-point')
  2002. //.data(dataWithPoints)
  2003. //.style('pointer-events', 'auto') // recativate events, disabled by css
  2004. .on('click', function(d,i) {
  2005. //nv.log('test', d, i);
  2006. if (needsUpdate || !data[d.series]) return 0; //check if this is a dummy point
  2007. var series = data[d.series],
  2008. point = series.values[i];
  2009. dispatch.elementClick({
  2010. point: point,
  2011. series: series,
  2012. pos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top], //TODO: make this pos base on the page
  2013. relativePos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top],
  2014. seriesIndex: d.series,
  2015. pointIndex: i
  2016. });
  2017. })
  2018. .on('dblclick', function(d,i) {
  2019. if (needsUpdate || !data[d.series]) return 0; //check if this is a dummy point
  2020. var series = data[d.series],
  2021. point = series.values[i];
  2022. dispatch.elementDblClick({
  2023. point: point,
  2024. series: series,
  2025. pos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top],//TODO: make this pos base on the page
  2026. relativePos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top],
  2027. seriesIndex: d.series,
  2028. pointIndex: i
  2029. });
  2030. })
  2031. .on('mouseover', function(d,i) {
  2032. if (needsUpdate || !data[d.series]) return 0; //check if this is a dummy point
  2033. var series = data[d.series],
  2034. point = series.values[i];
  2035. dispatch.elementMouseover({
  2036. point: point,
  2037. series: series,
  2038. pos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top],//TODO: make this pos base on the page
  2039. relativePos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top],
  2040. seriesIndex: d.series,
  2041. pointIndex: i,
  2042. color: color(d, i)
  2043. });
  2044. })
  2045. .on('mouseout', function(d,i) {
  2046. if (needsUpdate || !data[d.series]) return 0; //check if this is a dummy point
  2047. var series = data[d.series],
  2048. point = series.values[i];
  2049. dispatch.elementMouseout({
  2050. point: point,
  2051. series: series,
  2052. pos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top],//TODO: make this pos base on the page
  2053. relativePos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top],
  2054. seriesIndex: d.series,
  2055. pointIndex: i,
  2056. color: color(d, i)
  2057. });
  2058. });
  2059. }
  2060. }
  2061. needsUpdate = true;
  2062. var groups = wrap.select('.nv-groups').selectAll('.nv-group')
  2063. .data(function(d) { return d }, function(d) { return d.key });
  2064. groups.enter().append('g')
  2065. .style('stroke-opacity', 1e-6)
  2066. .style('fill-opacity', 1e-6);
  2067. groups.exit()
  2068. .remove();
  2069. groups
  2070. .attr('class', function(d,i) {
  2071. return (d.classed || '') + ' nv-group nv-series-' + i;
  2072. })
  2073. .classed('nv-noninteractive', !interactive)
  2074. .classed('hover', function(d) { return d.hover });
  2075. groups.watchTransition(renderWatch, 'scatter: groups')
  2076. .style('fill', function(d,i) { return color(d, i) })
  2077. .style('stroke', function(d,i) { return color(d, i) })
  2078. .style('stroke-opacity', 1)
  2079. .style('fill-opacity', .5);
  2080. // create the points, maintaining their IDs from the original data set
  2081. var points = groups.selectAll('path.nv-point')
  2082. .data(function(d) {
  2083. return d.values.map(
  2084. function (point, pointIndex) {
  2085. return [point, pointIndex]
  2086. }).filter(
  2087. function(pointArray, pointIndex) {
  2088. return pointActive(pointArray[0], pointIndex)
  2089. })
  2090. });
  2091. points.enter().append('path')
  2092. .attr('class', function (d) {
  2093. return 'nv-point nv-point-' + d[1];
  2094. })
  2095. .style('fill', function (d) { return d.color })
  2096. .style('stroke', function (d) { return d.color })
  2097. .attr('transform', function(d) {
  2098. return 'translate(' + nv.utils.NaNtoZero(x0(getX(d[0],d[1]))) + ',' + nv.utils.NaNtoZero(y0(getY(d[0],d[1]))) + ')'
  2099. })
  2100. .attr('d',
  2101. nv.utils.symbol()
  2102. .type(function(d) { return getShape(d[0]); })
  2103. .size(function(d) { return z(getSize(d[0],d[1])) })
  2104. );
  2105. points.exit().remove();
  2106. groups.exit().selectAll('path.nv-point')
  2107. .watchTransition(renderWatch, 'scatter exit')
  2108. .attr('transform', function(d) {
  2109. return 'translate(' + nv.utils.NaNtoZero(x(getX(d[0],d[1]))) + ',' + nv.utils.NaNtoZero(y(getY(d[0],d[1]))) + ')'
  2110. })
  2111. .remove();
  2112. points.filter(function (d) { return scaleDiff || getDiffs(d, 'x', 'y'); })
  2113. .watchTransition(renderWatch, 'scatter points')
  2114. .attr('transform', function(d) {
  2115. //nv.log(d, getX(d[0],d[1]), x(getX(d[0],d[1])));
  2116. return 'translate(' + nv.utils.NaNtoZero(x(getX(d[0],d[1]))) + ',' + nv.utils.NaNtoZero(y(getY(d[0],d[1]))) + ')'
  2117. });
  2118. points.filter(function (d) { return scaleDiff || getDiffs(d, 'shape', 'size'); })
  2119. .watchTransition(renderWatch, 'scatter points')
  2120. .attr('d',
  2121. nv.utils.symbol()
  2122. .type(function(d) { return getShape(d[0]); })
  2123. .size(function(d) { return z(getSize(d[0],d[1])) })
  2124. );
  2125. // add label a label to scatter chart
  2126. if(showLabels)
  2127. {
  2128. var titles = groups.selectAll('.nv-label')
  2129. .data(function(d) {
  2130. return d.values.map(
  2131. function (point, pointIndex) {
  2132. return [point, pointIndex]
  2133. }).filter(
  2134. function(pointArray, pointIndex) {
  2135. return pointActive(pointArray[0], pointIndex)
  2136. })
  2137. });
  2138. titles.enter().append('text')
  2139. .style('fill', function (d,i) {
  2140. return d.color })
  2141. .style('stroke-opacity', 0)
  2142. .style('fill-opacity', 1)
  2143. .attr('transform', function(d) {
  2144. var dx = nv.utils.NaNtoZero(x0(getX(d[0],d[1]))) + Math.sqrt(z(getSize(d[0],d[1]))/Math.PI) + 2;
  2145. return 'translate(' + dx + ',' + nv.utils.NaNtoZero(y0(getY(d[0],d[1]))) + ')';
  2146. })
  2147. .text(function(d,i){
  2148. return d[0].label;});
  2149. titles.exit().remove();
  2150. groups.exit().selectAll('path.nv-label')
  2151. .watchTransition(renderWatch, 'scatter exit')
  2152. .attr('transform', function(d) {
  2153. var dx = nv.utils.NaNtoZero(x(getX(d[0],d[1])))+ Math.sqrt(z(getSize(d[0],d[1]))/Math.PI)+2;
  2154. return 'translate(' + dx + ',' + nv.utils.NaNtoZero(y(getY(d[0],d[1]))) + ')';
  2155. })
  2156. .remove();
  2157. titles.each(function(d) {
  2158. d3.select(this)
  2159. .classed('nv-label', true)
  2160. .classed('nv-label-' + d[1], false)
  2161. .classed('hover',false);
  2162. });
  2163. titles.watchTransition(renderWatch, 'scatter labels')
  2164. .attr('transform', function(d) {
  2165. var dx = nv.utils.NaNtoZero(x(getX(d[0],d[1])))+ Math.sqrt(z(getSize(d[0],d[1]))/Math.PI)+2;
  2166. return 'translate(' + dx + ',' + nv.utils.NaNtoZero(y(getY(d[0],d[1]))) + ')'
  2167. });
  2168. }
  2169. // Delay updating the invisible interactive layer for smoother animation
  2170. if( interactiveUpdateDelay )
  2171. {
  2172. clearTimeout(timeoutID); // stop repeat calls to updateInteractiveLayer
  2173. timeoutID = setTimeout(updateInteractiveLayer, interactiveUpdateDelay );
  2174. }
  2175. else
  2176. {
  2177. updateInteractiveLayer();
  2178. }
  2179. //store old scales for use in transitions on update
  2180. x0 = x.copy();
  2181. y0 = y.copy();
  2182. z0 = z.copy();
  2183. });
  2184. renderWatch.renderEnd('scatter immediate');
  2185. return chart;
  2186. }
  2187. //============================================================
  2188. // Expose Public Variables
  2189. //------------------------------------------------------------
  2190. chart.dispatch = dispatch;
  2191. chart.options = nv.utils.optionsFunc.bind(chart);
  2192. // utility function calls provided by this chart
  2193. chart._calls = new function() {
  2194. this.clearHighlights = function () {
  2195. nv.dom.write(function() {
  2196. container.selectAll(".nv-point.hover").classed("hover", false);
  2197. });
  2198. return null;
  2199. };
  2200. this.highlightPoint = function (seriesIndex, pointIndex, isHoverOver) {
  2201. nv.dom.write(function() {
  2202. container.select('.nv-groups')
  2203. .selectAll(".nv-series-" + seriesIndex)
  2204. .selectAll(".nv-point-" + pointIndex)
  2205. .classed("hover", isHoverOver);
  2206. });
  2207. };
  2208. };
  2209. // trigger calls from events too
  2210. dispatch.on('elementMouseover.point', function(d) {
  2211. if (interactive) chart._calls.highlightPoint(d.seriesIndex,d.pointIndex,true);
  2212. });
  2213. dispatch.on('elementMouseout.point', function(d) {
  2214. if (interactive) chart._calls.highlightPoint(d.seriesIndex,d.pointIndex,false);
  2215. });
  2216. chart._options = Object.create({}, {
  2217. // simple options, just get/set the necessary values
  2218. width: {get: function(){return width;}, set: function(_){width=_;}},
  2219. height: {get: function(){return height;}, set: function(_){height=_;}},
  2220. xScale: {get: function(){return x;}, set: function(_){x=_;}},
  2221. yScale: {get: function(){return y;}, set: function(_){y=_;}},
  2222. pointScale: {get: function(){return z;}, set: function(_){z=_;}},
  2223. xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}},
  2224. yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}},
  2225. pointDomain: {get: function(){return sizeDomain;}, set: function(_){sizeDomain=_;}},
  2226. xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}},
  2227. yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}},
  2228. pointRange: {get: function(){return sizeRange;}, set: function(_){sizeRange=_;}},
  2229. forceX: {get: function(){return forceX;}, set: function(_){forceX=_;}},
  2230. forceY: {get: function(){return forceY;}, set: function(_){forceY=_;}},
  2231. forcePoint: {get: function(){return forceSize;}, set: function(_){forceSize=_;}},
  2232. interactive: {get: function(){return interactive;}, set: function(_){interactive=_;}},
  2233. pointActive: {get: function(){return pointActive;}, set: function(_){pointActive=_;}},
  2234. padDataOuter: {get: function(){return padDataOuter;}, set: function(_){padDataOuter=_;}},
  2235. padData: {get: function(){return padData;}, set: function(_){padData=_;}},
  2236. clipEdge: {get: function(){return clipEdge;}, set: function(_){clipEdge=_;}},
  2237. clipVoronoi: {get: function(){return clipVoronoi;}, set: function(_){clipVoronoi=_;}},
  2238. clipRadius: {get: function(){return clipRadius;}, set: function(_){clipRadius=_;}},
  2239. showVoronoi: {get: function(){return showVoronoi;}, set: function(_){showVoronoi=_;}},
  2240. id: {get: function(){return id;}, set: function(_){id=_;}},
  2241. interactiveUpdateDelay: {get:function(){return interactiveUpdateDelay;}, set: function(_){interactiveUpdateDelay=_;}},
  2242. showLabels: {get: function(){return showLabels;}, set: function(_){ showLabels = _;}},
  2243. // simple functor options
  2244. x: {get: function(){return getX;}, set: function(_){getX = d3.functor(_);}},
  2245. y: {get: function(){return getY;}, set: function(_){getY = d3.functor(_);}},
  2246. pointSize: {get: function(){return getSize;}, set: function(_){getSize = d3.functor(_);}},
  2247. pointShape: {get: function(){return getShape;}, set: function(_){getShape = d3.functor(_);}},
  2248. // options that require extra logic in the setter
  2249. margin: {get: function(){return margin;}, set: function(_){
  2250. margin.top = _.top !== undefined ? _.top : margin.top;
  2251. margin.right = _.right !== undefined ? _.right : margin.right;
  2252. margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
  2253. margin.left = _.left !== undefined ? _.left : margin.left;
  2254. }},
  2255. duration: {get: function(){return duration;}, set: function(_){
  2256. duration = _;
  2257. renderWatch.reset(duration);
  2258. }},
  2259. color: {get: function(){return color;}, set: function(_){
  2260. color = nv.utils.getColor(_);
  2261. }},
  2262. useVoronoi: {get: function(){return useVoronoi;}, set: function(_){
  2263. useVoronoi = _;
  2264. if (useVoronoi === false) {
  2265. clipVoronoi = false;
  2266. }
  2267. }}
  2268. });
  2269. nv.utils.initOptions(chart);
  2270. return chart;
  2271. };
  2272. nv.models.multiBar = function() {
  2273. "use strict";
  2274. //============================================================
  2275. // Public Variables with Default Settings
  2276. //------------------------------------------------------------
  2277. var margin = {top: 0, right: 0, bottom: 0, left: 0}
  2278. , width = 960
  2279. , height = 500
  2280. , x = d3.scale.ordinal()
  2281. , y = d3.scale.linear()
  2282. , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one
  2283. , container = null
  2284. , getX = function(d) { return d.x }
  2285. , getY = function(d) { return d.y }
  2286. , forceY = [0] // 0 is forced by default.. this makes sense for the majority of bar graphs... user can always do chart.forceY([]) to remove
  2287. , clipEdge = true
  2288. , stacked = false
  2289. , stackOffset = 'zero' // options include 'silhouette', 'wiggle', 'expand', 'zero', or a custom function
  2290. , color = nv.utils.defaultColor()
  2291. , hideable = false
  2292. , barColor = null // adding the ability to set the color for each rather than the whole group
  2293. , disabled // used in conjunction with barColor to communicate from multiBarHorizontalChart what series are disabled
  2294. , duration = 500
  2295. , xDomain
  2296. , yDomain
  2297. , xRange
  2298. , yRange
  2299. , groupSpacing = 0.1
  2300. , dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd')
  2301. ;
  2302. //============================================================
  2303. // Private Variables
  2304. //------------------------------------------------------------
  2305. var x0, y0 //used to store previous scales
  2306. , renderWatch = nv.utils.renderWatch(dispatch, duration)
  2307. ;
  2308. var last_datalength = 0;
  2309. function chart(selection) {
  2310. renderWatch.reset();
  2311. selection.each(function(data) {
  2312. var availableWidth = width - margin.left - margin.right,
  2313. availableHeight = height - margin.top - margin.bottom;
  2314. container = d3.select(this);
  2315. nv.utils.initSVG(container);
  2316. var nonStackableCount = 0;
  2317. // This function defines the requirements for render complete
  2318. var endFn = function(d, i) {
  2319. if (d.series === data.length - 1 && i === data[0].values.length - 1)
  2320. return true;
  2321. return false;
  2322. };
  2323. if(hideable && data.length) hideable = [{
  2324. values: data[0].values.map(function(d) {
  2325. return {
  2326. x: d.x,
  2327. y: 0,
  2328. series: d.series,
  2329. size: 0.01
  2330. };}
  2331. )}];
  2332. if (stacked) {
  2333. var parsed = d3.layout.stack()
  2334. .offset(stackOffset)
  2335. .values(function(d){ return d.values })
  2336. .y(getY)
  2337. (!data.length && hideable ? hideable : data);
  2338. parsed.forEach(function(series, i){
  2339. // if series is non-stackable, use un-parsed data
  2340. if (series.nonStackable) {
  2341. data[i].nonStackableSeries = nonStackableCount++;
  2342. parsed[i] = data[i];
  2343. } else {
  2344. // don't stack this seires on top of the nonStackable seriees
  2345. if (i > 0 && parsed[i - 1].nonStackable){
  2346. parsed[i].values.map(function(d,j){
  2347. d.y0 -= parsed[i - 1].values[j].y;
  2348. d.y1 = d.y0 + d.y;
  2349. });
  2350. }
  2351. }
  2352. });
  2353. data = parsed;
  2354. }
  2355. //add series index and key to each data point for reference
  2356. data.forEach(function(series, i) {
  2357. series.values.forEach(function(point) {
  2358. point.series = i;
  2359. point.key = series.key;
  2360. });
  2361. });
  2362. // HACK for negative value stacking
  2363. if (stacked) {
  2364. data[0].values.map(function(d,i) {
  2365. var posBase = 0, negBase = 0;
  2366. data.map(function(d, idx) {
  2367. if (!data[idx].nonStackable) {
  2368. var f = d.values[i]
  2369. f.size = Math.abs(f.y);
  2370. if (f.y<0) {
  2371. f.y1 = negBase;
  2372. negBase = negBase - f.size;
  2373. } else
  2374. {
  2375. f.y1 = f.size + posBase;
  2376. posBase = posBase + f.size;
  2377. }
  2378. }
  2379. });
  2380. });
  2381. }
  2382. // Setup Scales
  2383. // remap and flatten the data for use in calculating the scales' domains
  2384. var seriesData = (xDomain && yDomain) ? [] : // if we know xDomain and yDomain, no need to calculate
  2385. data.map(function(d, idx) {
  2386. return d.values.map(function(d,i) {
  2387. return { x: getX(d,i), y: getY(d,i), y0: d.y0, y1: d.y1, idx:idx }
  2388. })
  2389. });
  2390. x.domain(xDomain || d3.merge(seriesData).map(function(d) { return d.x }))
  2391. .rangeBands(xRange || [0, availableWidth], groupSpacing);
  2392. y.domain(yDomain || d3.extent(d3.merge(seriesData).map(function(d) {
  2393. var domain = d.y;
  2394. // increase the domain range if this series is stackable
  2395. if (stacked && !data[d.idx].nonStackable) {
  2396. if (d.y > 0){
  2397. domain = d.y1
  2398. } else {
  2399. domain = d.y1 + d.y
  2400. }
  2401. }
  2402. return domain;
  2403. }).concat(forceY)))
  2404. .range(yRange || [availableHeight, 0]);
  2405. // If scale's domain don't have a range, slightly adjust to make one... so a chart can show a single data point
  2406. if (x.domain()[0] === x.domain()[1])
  2407. x.domain()[0] ?
  2408. x.domain([x.domain()[0] - x.domain()[0] * 0.01, x.domain()[1] + x.domain()[1] * 0.01])
  2409. : x.domain([-1,1]);
  2410. if (y.domain()[0] === y.domain()[1])
  2411. y.domain()[0] ?
  2412. y.domain([y.domain()[0] + y.domain()[0] * 0.01, y.domain()[1] - y.domain()[1] * 0.01])
  2413. : y.domain([-1,1]);
  2414. x0 = x0 || x;
  2415. y0 = y0 || y;
  2416. // Setup containers and skeleton of chart
  2417. var wrap = container.selectAll('g.nv-wrap.nv-multibar').data([data]);
  2418. var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-multibar');
  2419. var defsEnter = wrapEnter.append('defs');
  2420. var gEnter = wrapEnter.append('g');
  2421. var g = wrap.select('g');
  2422. gEnter.append('g').attr('class', 'nv-groups');
  2423. wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
  2424. defsEnter.append('clipPath')
  2425. .attr('id', 'nv-edge-clip-' + id)
  2426. .append('rect');
  2427. wrap.select('#nv-edge-clip-' + id + ' rect')
  2428. .attr('width', availableWidth)
  2429. .attr('height', availableHeight);
  2430. g.attr('clip-path', clipEdge ? 'url(#nv-edge-clip-' + id + ')' : '');
  2431. var groups = wrap.select('.nv-groups').selectAll('.nv-group')
  2432. .data(function(d) { return d }, function(d,i) { return i });
  2433. groups.enter().append('g')
  2434. .style('stroke-opacity', 1e-6)
  2435. .style('fill-opacity', 1e-6);
  2436. var exitTransition = renderWatch
  2437. .transition(groups.exit().selectAll('rect.nv-bar'), 'multibarExit', Math.min(100, duration))
  2438. .attr('y', function(d, i, j) {
  2439. var yVal = y0(0) || 0;
  2440. if (stacked) {
  2441. if (data[d.series] && !data[d.series].nonStackable) {
  2442. yVal = y0(d.y0);
  2443. }
  2444. }
  2445. return yVal;
  2446. })
  2447. .attr('height', 0)
  2448. .remove();
  2449. if (exitTransition.delay)
  2450. exitTransition.delay(function(d,i) {
  2451. var delay = i * (duration / (last_datalength + 1)) - i;
  2452. return delay;
  2453. });
  2454. groups
  2455. .attr('class', function(d,i) { return 'nv-group nv-series-' + i })
  2456. .classed('hover', function(d) { return d.hover })
  2457. .style('fill', function(d,i){ return color(d, i) })
  2458. .style('stroke', function(d,i){ return color(d, i) });
  2459. groups
  2460. .style('stroke-opacity', 1)
  2461. .style('fill-opacity', 0.75);
  2462. var bars = groups.selectAll('rect.nv-bar')
  2463. .data(function(d) { return (hideable && !data.length) ? hideable.values : d.values });
  2464. bars.exit().remove();
  2465. var barsEnter = bars.enter().append('rect')
  2466. .attr('class', function(d,i) { return getY(d,i) < 0 ? 'nv-bar negative' : 'nv-bar positive'})
  2467. .attr('x', function(d,i,j) {
  2468. return stacked && !data[j].nonStackable ? 0 : (j * x.rangeBand() / data.length )
  2469. })
  2470. .attr('y', function(d,i,j) { return y0(stacked && !data[j].nonStackable ? d.y0 : 0) || 0 })
  2471. .attr('height', 0)
  2472. .attr('width', function(d,i,j) { return x.rangeBand() / (stacked && !data[j].nonStackable ? 1 : data.length) })
  2473. .attr('transform', function(d,i) { return 'translate(' + x(getX(d,i)) + ',0)'; })
  2474. ;
  2475. bars
  2476. .style('fill', function(d,i,j){ return color(d, j, i); })
  2477. .style('stroke', function(d,i,j){ return color(d, j, i); })
  2478. .on('mouseover', function(d,i) { //TODO: figure out why j works above, but not here
  2479. d3.select(this).classed('hover', true);
  2480. dispatch.elementMouseover({
  2481. data: d,
  2482. index: i,
  2483. color: d3.select(this).style("fill")
  2484. });
  2485. })
  2486. .on('mouseout', function(d,i) {
  2487. d3.select(this).classed('hover', false);
  2488. dispatch.elementMouseout({
  2489. data: d,
  2490. index: i,
  2491. color: d3.select(this).style("fill")
  2492. });
  2493. })
  2494. .on('mousemove', function(d,i) {
  2495. dispatch.elementMousemove({
  2496. data: d,
  2497. index: i,
  2498. color: d3.select(this).style("fill")
  2499. });
  2500. })
  2501. .on('click', function(d,i) {
  2502. var element = this;
  2503. dispatch.elementClick({
  2504. data: d,
  2505. index: i,
  2506. color: d3.select(this).style("fill"),
  2507. event: d3.event,
  2508. element: element
  2509. });
  2510. d3.event.stopPropagation();
  2511. })
  2512. .on('dblclick', function(d,i) {
  2513. dispatch.elementDblClick({
  2514. data: d,
  2515. index: i,
  2516. color: d3.select(this).style("fill")
  2517. });
  2518. d3.event.stopPropagation();
  2519. });
  2520. bars
  2521. .attr('class', function(d,i) { return getY(d,i) < 0 ? 'nv-bar negative' : 'nv-bar positive'})
  2522. .attr('transform', function(d,i) { return 'translate(' + x(getX(d,i)) + ',0)'; })
  2523. if (barColor) {
  2524. if (!disabled) disabled = data.map(function() { return true });
  2525. bars
  2526. .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(); })
  2527. .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(); });
  2528. }
  2529. var barSelection =
  2530. bars.watchTransition(renderWatch, 'multibar', Math.min(250, duration))
  2531. .delay(function(d,i) {
  2532. return i * duration / data[0].values.length;
  2533. });
  2534. if (stacked){
  2535. barSelection
  2536. .attr('y', function(d,i,j) {
  2537. var yVal = 0;
  2538. // if stackable, stack it on top of the previous series
  2539. if (!data[j].nonStackable) {
  2540. yVal = y(d.y1);
  2541. } else {
  2542. if (getY(d,i) < 0){
  2543. yVal = y(0);
  2544. } else {
  2545. if (y(0) - y(getY(d,i)) < -1){
  2546. yVal = y(0) - 1;
  2547. } else {
  2548. yVal = y(getY(d, i)) || 0;
  2549. }
  2550. }
  2551. }
  2552. return yVal;
  2553. })
  2554. .attr('height', function(d,i,j) {
  2555. if (!data[j].nonStackable) {
  2556. return Math.max(Math.abs(y(d.y+d.y0) - y(d.y0)), 0);
  2557. } else {
  2558. return Math.max(Math.abs(y(getY(d,i)) - y(0)), 0) || 0;
  2559. }
  2560. })
  2561. .attr('x', function(d,i,j) {
  2562. var width = 0;
  2563. if (data[j].nonStackable) {
  2564. width = d.series * x.rangeBand() / data.length;
  2565. if (data.length !== nonStackableCount){
  2566. width = data[j].nonStackableSeries * x.rangeBand()/(nonStackableCount*2);
  2567. }
  2568. }
  2569. return width;
  2570. })
  2571. .attr('width', function(d,i,j){
  2572. if (!data[j].nonStackable) {
  2573. return x.rangeBand();
  2574. } else {
  2575. // if all series are nonStacable, take the full width
  2576. var width = (x.rangeBand() / nonStackableCount);
  2577. // otherwise, nonStackable graph will be only taking the half-width
  2578. // of the x rangeBand
  2579. if (data.length !== nonStackableCount) {
  2580. width = x.rangeBand()/(nonStackableCount*2);
  2581. }
  2582. return width;
  2583. }
  2584. });
  2585. }
  2586. else {
  2587. barSelection
  2588. .attr('x', function(d,i) {
  2589. return d.series * x.rangeBand() / data.length;
  2590. })
  2591. .attr('width', x.rangeBand() / data.length)
  2592. .attr('y', function(d,i) {
  2593. return getY(d,i) < 0 ?
  2594. y(0) :
  2595. y(0) - y(getY(d,i)) < 1 ?
  2596. y(0) - 1 :
  2597. y(getY(d,i)) || 0;
  2598. })
  2599. .attr('height', function(d,i) {
  2600. return Math.max(Math.abs(y(getY(d,i)) - y(0)),1) || 0;
  2601. });
  2602. }
  2603. //store old scales for use in transitions on update
  2604. x0 = x.copy();
  2605. y0 = y.copy();
  2606. // keep track of the last data value length for transition calculations
  2607. if (data[0] && data[0].values) {
  2608. last_datalength = data[0].values.length;
  2609. }
  2610. });
  2611. renderWatch.renderEnd('multibar immediate');
  2612. return chart;
  2613. }
  2614. //============================================================
  2615. // Expose Public Variables
  2616. //------------------------------------------------------------
  2617. chart.dispatch = dispatch;
  2618. chart.options = nv.utils.optionsFunc.bind(chart);
  2619. chart._options = Object.create({}, {
  2620. // simple options, just get/set the necessary values
  2621. width: {get: function(){return width;}, set: function(_){width=_;}},
  2622. height: {get: function(){return height;}, set: function(_){height=_;}},
  2623. x: {get: function(){return getX;}, set: function(_){getX=_;}},
  2624. y: {get: function(){return getY;}, set: function(_){getY=_;}},
  2625. xScale: {get: function(){return x;}, set: function(_){x=_;}},
  2626. yScale: {get: function(){return y;}, set: function(_){y=_;}},
  2627. xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}},
  2628. yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}},
  2629. xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}},
  2630. yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}},
  2631. forceY: {get: function(){return forceY;}, set: function(_){forceY=_;}},
  2632. stacked: {get: function(){return stacked;}, set: function(_){stacked=_;}},
  2633. stackOffset: {get: function(){return stackOffset;}, set: function(_){stackOffset=_;}},
  2634. clipEdge: {get: function(){return clipEdge;}, set: function(_){clipEdge=_;}},
  2635. disabled: {get: function(){return disabled;}, set: function(_){disabled=_;}},
  2636. id: {get: function(){return id;}, set: function(_){id=_;}},
  2637. hideable: {get: function(){return hideable;}, set: function(_){hideable=_;}},
  2638. groupSpacing:{get: function(){return groupSpacing;}, set: function(_){groupSpacing=_;}},
  2639. // options that require extra logic in the setter
  2640. margin: {get: function(){return margin;}, set: function(_){
  2641. margin.top = _.top !== undefined ? _.top : margin.top;
  2642. margin.right = _.right !== undefined ? _.right : margin.right;
  2643. margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
  2644. margin.left = _.left !== undefined ? _.left : margin.left;
  2645. }},
  2646. duration: {get: function(){return duration;}, set: function(_){
  2647. duration = _;
  2648. renderWatch.reset(duration);
  2649. }},
  2650. color: {get: function(){return color;}, set: function(_){
  2651. color = nv.utils.getColor(_);
  2652. }},
  2653. barColor: {get: function(){return barColor;}, set: function(_){
  2654. barColor = _ ? nv.utils.getColor(_) : null;
  2655. }}
  2656. });
  2657. nv.utils.initOptions(chart);
  2658. return chart;
  2659. };nv.models.legend = function() {
  2660. "use strict";
  2661. //============================================================
  2662. // Public Variables with Default Settings
  2663. //------------------------------------------------------------
  2664. var margin = {top: 5, right: 0, bottom: 5, left: 0}
  2665. , width = 400
  2666. , height = 20
  2667. , getKey = function(d) { return d.key }
  2668. , color = nv.utils.getColor()
  2669. , maxKeyLength = 20 //default value for key lengths
  2670. , align = true
  2671. , padding = 32 //define how much space between legend items. - recommend 32 for furious version
  2672. , rightAlign = true
  2673. , updateState = true //If true, legend will update data.disabled and trigger a 'stateChange' dispatch.
  2674. , radioButtonMode = false //If true, clicking legend items will cause it to behave like a radio button. (only one can be selected at a time)
  2675. , expanded = false
  2676. , dispatch = d3.dispatch('legendClick', 'legendDblclick', 'legendMouseover', 'legendMouseout', 'stateChange')
  2677. , vers = 'classic' //Options are "classic" and "furious"
  2678. ;
  2679. function chart(selection) {
  2680. selection.each(function(data) {
  2681. var availableWidth = width - margin.left - margin.right,
  2682. container = d3.select(this);
  2683. nv.utils.initSVG(container);
  2684. // Setup containers and skeleton of chart
  2685. var wrap = container.selectAll('g.nv-legend').data([data]);
  2686. var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-legend').append('g');
  2687. var g = wrap.select('g');
  2688. wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
  2689. var series = g.selectAll('.nv-series')
  2690. .data(function(d) {
  2691. if(vers != 'furious') return d;
  2692. return d.filter(function(n) {
  2693. return expanded ? true : !n.disengaged;
  2694. });
  2695. });
  2696. var seriesEnter = series.enter().append('g').attr('class', 'nv-series');
  2697. var seriesShape;
  2698. var versPadding;
  2699. switch(vers) {
  2700. case 'furious' :
  2701. versPadding = 23;
  2702. break;
  2703. case 'classic' :
  2704. versPadding = 20;
  2705. }
  2706. if(vers == 'classic') {
  2707. seriesEnter.append('circle')
  2708. .style('stroke-width', 2)
  2709. .attr('class','nv-legend-symbol')
  2710. .attr('r', 5);
  2711. seriesShape = series.select('circle');
  2712. } else if (vers == 'furious') {
  2713. seriesEnter.append('rect')
  2714. .style('stroke-width', 2)
  2715. .attr('class','nv-legend-symbol')
  2716. .attr('rx', 3)
  2717. .attr('ry', 3);
  2718. seriesShape = series.select('.nv-legend-symbol');
  2719. seriesEnter.append('g')
  2720. .attr('class', 'nv-check-box')
  2721. .property('innerHTML','<path d="M0.5,5 L22.5,5 L22.5,26.5 L0.5,26.5 L0.5,5 Z" class="nv-box"></path><path d="M5.5,12.8618467 L11.9185089,19.2803556 L31,0.198864511" class="nv-check"></path>')
  2722. .attr('transform', 'translate(-10,-8)scale(0.5)');
  2723. var seriesCheckbox = series.select('.nv-check-box');
  2724. seriesCheckbox.each(function(d,i) {
  2725. d3.select(this).selectAll('path')
  2726. .attr('stroke', setTextColor(d,i));
  2727. });
  2728. }
  2729. seriesEnter.append('text')
  2730. .attr('text-anchor', 'start')
  2731. .attr('class','nv-legend-text')
  2732. .attr('dy', '.32em')
  2733. .attr('dx', '8');
  2734. var seriesText = series.select('text.nv-legend-text');
  2735. series
  2736. .on('mouseover', function(d,i) {
  2737. dispatch.legendMouseover(d,i); //TODO: Make consistent with other event objects
  2738. })
  2739. .on('mouseout', function(d,i) {
  2740. dispatch.legendMouseout(d,i);
  2741. })
  2742. .on('click', function(d,i) {
  2743. dispatch.legendClick(d,i);
  2744. // make sure we re-get data in case it was modified
  2745. var data = series.data();
  2746. if (updateState) {
  2747. if(vers =='classic') {
  2748. if (radioButtonMode) {
  2749. //Radio button mode: set every series to disabled,
  2750. // and enable the clicked series.
  2751. data.forEach(function(series) { series.disabled = true});
  2752. d.disabled = false;
  2753. }
  2754. else {
  2755. d.disabled = !d.disabled;
  2756. if (data.every(function(series) { return series.disabled})) {
  2757. //the default behavior of NVD3 legends is, if every single series
  2758. // is disabled, turn all series' back on.
  2759. data.forEach(function(series) { series.disabled = false});
  2760. }
  2761. }
  2762. } else if(vers == 'furious') {
  2763. if(expanded) {
  2764. d.disengaged = !d.disengaged;
  2765. d.userDisabled = d.userDisabled == undefined ? !!d.disabled : d.userDisabled;
  2766. d.disabled = d.disengaged || d.userDisabled;
  2767. } else if (!expanded) {
  2768. d.disabled = !d.disabled;
  2769. d.userDisabled = d.disabled;
  2770. var engaged = data.filter(function(d) { return !d.disengaged; });
  2771. if (engaged.every(function(series) { return series.userDisabled })) {
  2772. //the default behavior of NVD3 legends is, if every single series
  2773. // is disabled, turn all series' back on.
  2774. data.forEach(function(series) {
  2775. series.disabled = series.userDisabled = false;
  2776. });
  2777. }
  2778. }
  2779. }
  2780. dispatch.stateChange({
  2781. disabled: data.map(function(d) { return !!d.disabled }),
  2782. disengaged: data.map(function(d) { return !!d.disengaged })
  2783. });
  2784. }
  2785. })
  2786. .on('dblclick', function(d,i) {
  2787. if(vers == 'furious' && expanded) return;
  2788. dispatch.legendDblclick(d,i);
  2789. if (updateState) {
  2790. // make sure we re-get data in case it was modified
  2791. var data = series.data();
  2792. //the default behavior of NVD3 legends, when double clicking one,
  2793. // is to set all other series' to false, and make the double clicked series enabled.
  2794. data.forEach(function(series) {
  2795. series.disabled = true;
  2796. if(vers == 'furious') series.userDisabled = series.disabled;
  2797. });
  2798. d.disabled = false;
  2799. if(vers == 'furious') d.userDisabled = d.disabled;
  2800. dispatch.stateChange({
  2801. disabled: data.map(function(d) { return !!d.disabled })
  2802. });
  2803. }
  2804. });
  2805. series.classed('nv-disabled', function(d) { return d.userDisabled });
  2806. series.exit().remove();
  2807. seriesText
  2808. .attr('fill', setTextColor)
  2809. .text(getKey);
  2810. //TODO: implement fixed-width and max-width options (max-width is especially useful with the align option)
  2811. // NEW ALIGNING CODE, TODO: clean up
  2812. var legendWidth = 0;
  2813. if (align) {
  2814. var seriesWidths = [];
  2815. series.each(function(d,i) {
  2816. var legendText;
  2817. if (getKey(d).length > maxKeyLength) {
  2818. var trimmedKey = getKey(d).substring(0, maxKeyLength);
  2819. legendText = d3.select(this).select('text').text(trimmedKey + "...");
  2820. d3.select(this).append("svg:title").text(getKey(d));
  2821. } else {
  2822. legendText = d3.select(this).select('text');
  2823. }
  2824. var nodeTextLength;
  2825. try {
  2826. nodeTextLength = legendText.node().getComputedTextLength();
  2827. // If the legendText is display:none'd (nodeTextLength == 0), simulate an error so we approximate, instead
  2828. if(nodeTextLength <= 0) throw Error();
  2829. }
  2830. catch(e) {
  2831. nodeTextLength = nv.utils.calcApproxTextWidth(legendText);
  2832. }
  2833. seriesWidths.push(nodeTextLength + padding);
  2834. });
  2835. var seriesPerRow = 0;
  2836. var columnWidths = [];
  2837. legendWidth = 0;
  2838. while ( legendWidth < availableWidth && seriesPerRow < seriesWidths.length) {
  2839. columnWidths[seriesPerRow] = seriesWidths[seriesPerRow];
  2840. legendWidth += seriesWidths[seriesPerRow++];
  2841. }
  2842. if (seriesPerRow === 0) seriesPerRow = 1; //minimum of one series per row
  2843. while ( legendWidth > availableWidth && seriesPerRow > 1 ) {
  2844. columnWidths = [];
  2845. seriesPerRow--;
  2846. for (var k = 0; k < seriesWidths.length; k++) {
  2847. if (seriesWidths[k] > (columnWidths[k % seriesPerRow] || 0) )
  2848. columnWidths[k % seriesPerRow] = seriesWidths[k];
  2849. }
  2850. legendWidth = columnWidths.reduce(function(prev, cur, index, array) {
  2851. return prev + cur;
  2852. });
  2853. }
  2854. var xPositions = [];
  2855. for (var i = 0, curX = 0; i < seriesPerRow; i++) {
  2856. xPositions[i] = curX;
  2857. curX += columnWidths[i];
  2858. }
  2859. series
  2860. .attr('transform', function(d, i) {
  2861. return 'translate(' + xPositions[i % seriesPerRow] + ',' + (5 + Math.floor(i / seriesPerRow) * versPadding) + ')';
  2862. });
  2863. //position legend as far right as possible within the total width
  2864. if (rightAlign) {
  2865. g.attr('transform', 'translate(' + (width - margin.right - legendWidth) + ',' + margin.top + ')');
  2866. }
  2867. else {
  2868. g.attr('transform', 'translate(0' + ',' + margin.top + ')');
  2869. }
  2870. height = margin.top + margin.bottom + (Math.ceil(seriesWidths.length / seriesPerRow) * versPadding);
  2871. } else {
  2872. var ypos = 5,
  2873. newxpos = 5,
  2874. maxwidth = 0,
  2875. xpos;
  2876. series
  2877. .attr('transform', function(d, i) {
  2878. var length = d3.select(this).select('text').node().getComputedTextLength() + padding;
  2879. xpos = newxpos;
  2880. if (width < margin.left + margin.right + xpos + length) {
  2881. newxpos = xpos = 5;
  2882. ypos += versPadding;
  2883. }
  2884. newxpos += length;
  2885. if (newxpos > maxwidth) maxwidth = newxpos;
  2886. if(legendWidth < xpos + maxwidth) {
  2887. legendWidth = xpos + maxwidth;
  2888. }
  2889. return 'translate(' + xpos + ',' + ypos + ')';
  2890. });
  2891. //position legend as far right as possible within the total width
  2892. g.attr('transform', 'translate(' + (width - margin.right - maxwidth) + ',' + margin.top + ')');
  2893. height = margin.top + margin.bottom + ypos + 15;
  2894. }
  2895. if(vers == 'furious') {
  2896. // Size rectangles after text is placed
  2897. seriesShape
  2898. .attr('width', function(d,i) {
  2899. return seriesText[0][i].getComputedTextLength() + 27;
  2900. })
  2901. .attr('height', 18)
  2902. .attr('y', -9)
  2903. .attr('x', -15);
  2904. // The background for the expanded legend (UI)
  2905. gEnter.insert('rect',':first-child')
  2906. .attr('class', 'nv-legend-bg')
  2907. .attr('fill', '#eee')
  2908. // .attr('stroke', '#444')
  2909. .attr('opacity',0);
  2910. var seriesBG = g.select('.nv-legend-bg');
  2911. seriesBG
  2912. .transition().duration(300)
  2913. .attr('x', -versPadding )
  2914. .attr('width', legendWidth + versPadding - 12)
  2915. .attr('height', height + 10)
  2916. .attr('y', -margin.top - 10)
  2917. .attr('opacity', expanded ? 1 : 0);
  2918. }
  2919. seriesShape
  2920. .style('fill', setBGColor)
  2921. .style('fill-opacity', setBGOpacity)
  2922. .style('stroke', setBGColor);
  2923. });
  2924. function setTextColor(d,i) {
  2925. if(vers != 'furious') return '#000';
  2926. if(expanded) {
  2927. return d.disengaged ? '#000' : '#fff';
  2928. } else if (!expanded) {
  2929. if(!d.color) d.color = color(d,i);
  2930. return !!d.disabled ? d.color : '#fff';
  2931. }
  2932. }
  2933. function setBGColor(d,i) {
  2934. if(expanded && vers == 'furious') {
  2935. return d.disengaged ? '#eee' : d.color || color(d,i);
  2936. } else {
  2937. return d.color || color(d,i);
  2938. }
  2939. }
  2940. function setBGOpacity(d,i) {
  2941. if(expanded && vers == 'furious') {
  2942. return 1;
  2943. } else {
  2944. return !!d.disabled ? 0 : 1;
  2945. }
  2946. }
  2947. return chart;
  2948. }
  2949. //============================================================
  2950. // Expose Public Variables
  2951. //------------------------------------------------------------
  2952. chart.dispatch = dispatch;
  2953. chart.options = nv.utils.optionsFunc.bind(chart);
  2954. chart._options = Object.create({}, {
  2955. // simple options, just get/set the necessary values
  2956. width: {get: function(){return width;}, set: function(_){width=_;}},
  2957. height: {get: function(){return height;}, set: function(_){height=_;}},
  2958. key: {get: function(){return getKey;}, set: function(_){getKey=_;}},
  2959. align: {get: function(){return align;}, set: function(_){align=_;}},
  2960. maxKeyLength: {get: function(){return maxKeyLength;}, set: function(_){maxKeyLength=_;}},
  2961. rightAlign: {get: function(){return rightAlign;}, set: function(_){rightAlign=_;}},
  2962. padding: {get: function(){return padding;}, set: function(_){padding=_;}},
  2963. updateState: {get: function(){return updateState;}, set: function(_){updateState=_;}},
  2964. radioButtonMode: {get: function(){return radioButtonMode;}, set: function(_){radioButtonMode=_;}},
  2965. expanded: {get: function(){return expanded;}, set: function(_){expanded=_;}},
  2966. vers: {get: function(){return vers;}, set: function(_){vers=_;}},
  2967. // options that require extra logic in the setter
  2968. margin: {get: function(){return margin;}, set: function(_){
  2969. margin.top = _.top !== undefined ? _.top : margin.top;
  2970. margin.right = _.right !== undefined ? _.right : margin.right;
  2971. margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
  2972. margin.left = _.left !== undefined ? _.left : margin.left;
  2973. }},
  2974. color: {get: function(){return color;}, set: function(_){
  2975. color = nv.utils.getColor(_);
  2976. }}
  2977. });
  2978. nv.utils.initOptions(chart);
  2979. return chart;
  2980. };
  2981. nv.models.lineChart = function() {
  2982. "use strict";
  2983. //============================================================
  2984. // Public Variables with Default Settings
  2985. //------------------------------------------------------------
  2986. var lines = nv.models.line()
  2987. , xAxis = nv.models.axis()
  2988. , yAxis = nv.models.axis()
  2989. , legend = nv.models.legend()
  2990. , interactiveLayer = nv.interactiveGuideline()
  2991. , tooltip = nv.models.tooltip()
  2992. , lines2 = nv.models.line()
  2993. , x2Axis = nv.models.axis()
  2994. , y2Axis = nv.models.axis()
  2995. , brush = d3.svg.brush()
  2996. ;
  2997. var margin = {top: 30, right: 20, bottom: 50, left: 60}
  2998. , margin2 = {top: 0, right: 20, bottom: 20, left: 60}
  2999. , color = nv.utils.defaultColor()
  3000. , width = null
  3001. , height = null
  3002. , showLegend = true
  3003. , legendPosition = 'top'
  3004. , showXAxis = true
  3005. , showYAxis = true
  3006. , rightAlignYAxis = false
  3007. , useInteractiveGuideline = false
  3008. , x
  3009. , y
  3010. , x2
  3011. , y2
  3012. , focusEnable = false
  3013. , focusShowAxisY = false
  3014. , focusShowAxisX = true
  3015. , focusHeight = 50
  3016. , brushExtent = null
  3017. , state = nv.utils.state()
  3018. , defaultState = null
  3019. , noData = null
  3020. , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'brush', 'stateChange', 'changeState', 'renderEnd')
  3021. , duration = 250
  3022. ;
  3023. // set options on sub-objects for this chart
  3024. xAxis.orient('bottom').tickPadding(7);
  3025. yAxis.orient(rightAlignYAxis ? 'right' : 'left');
  3026. lines.clipEdge(true).duration(0);
  3027. lines2.interactive(false);
  3028. // We don't want any points emitted for the focus chart's scatter graph.
  3029. lines2.pointActive(function(d) { return false; });
  3030. x2Axis.orient('bottom').tickPadding(5);
  3031. y2Axis.orient(rightAlignYAxis ? 'right' : 'left');
  3032. tooltip.valueFormatter(function(d, i) {
  3033. return yAxis.tickFormat()(d, i);
  3034. }).headerFormatter(function(d, i) {
  3035. return xAxis.tickFormat()(d, i);
  3036. });
  3037. interactiveLayer.tooltip.valueFormatter(function(d, i) {
  3038. return yAxis.tickFormat()(d, i);
  3039. }).headerFormatter(function(d, i) {
  3040. return xAxis.tickFormat()(d, i);
  3041. });
  3042. //============================================================
  3043. // Private Variables
  3044. //------------------------------------------------------------
  3045. var renderWatch = nv.utils.renderWatch(dispatch, duration);
  3046. var stateGetter = function(data) {
  3047. return function(){
  3048. return {
  3049. active: data.map(function(d) { return !d.disabled; })
  3050. };
  3051. };
  3052. };
  3053. var stateSetter = function(data) {
  3054. return function(state) {
  3055. if (state.active !== undefined)
  3056. data.forEach(function(series,i) {
  3057. series.disabled = !state.active[i];
  3058. });
  3059. };
  3060. };
  3061. function chart(selection) {
  3062. renderWatch.reset();
  3063. renderWatch.models(lines);
  3064. renderWatch.models(lines2);
  3065. if (showXAxis) renderWatch.models(xAxis);
  3066. if (showYAxis) renderWatch.models(yAxis);
  3067. if (focusShowAxisX) renderWatch.models(x2Axis);
  3068. if (focusShowAxisY) renderWatch.models(y2Axis);
  3069. selection.each(function(data) {
  3070. var container = d3.select(this);
  3071. nv.utils.initSVG(container);
  3072. var availableWidth = nv.utils.availableWidth(width, container, margin),
  3073. availableHeight1 = nv.utils.availableHeight(height, container, margin) - (focusEnable ? focusHeight : 0),
  3074. availableHeight2 = focusHeight - margin2.top - margin2.bottom;
  3075. chart.update = function() {
  3076. if( duration === 0 ) {
  3077. container.call( chart );
  3078. } else {
  3079. container.transition().duration(duration).call(chart);
  3080. }
  3081. };
  3082. chart.container = this;
  3083. state
  3084. .setter(stateSetter(data), chart.update)
  3085. .getter(stateGetter(data))
  3086. .update();
  3087. // DEPRECATED set state.disabled
  3088. state.disabled = data.map(function(d) { return !!d.disabled; });
  3089. if (!defaultState) {
  3090. var key;
  3091. defaultState = {};
  3092. for (key in state) {
  3093. if (state[key] instanceof Array)
  3094. defaultState[key] = state[key].slice(0);
  3095. else
  3096. defaultState[key] = state[key];
  3097. }
  3098. }
  3099. // Display noData message if there's nothing to show.
  3100. if (!data || !data.length || !data.filter(function(d) { return d.values.length; }).length) {
  3101. nv.utils.noData(chart, container);
  3102. return chart;
  3103. } else {
  3104. container.selectAll('.nv-noData').remove();
  3105. }
  3106. // Setup Scales
  3107. x = lines.xScale();
  3108. y = lines.yScale();
  3109. x2 = lines2.xScale();
  3110. y2 = lines2.yScale();
  3111. // Setup containers and skeleton of chart
  3112. var wrap = container.selectAll('g.nv-wrap.nv-lineChart').data([data]);
  3113. var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-lineChart').append('g');
  3114. var g = wrap.select('g');
  3115. gEnter.append('g').attr('class', 'nv-legendWrap');
  3116. var focusEnter = gEnter.append('g').attr('class', 'nv-focus');
  3117. focusEnter.append('g').attr('class', 'nv-background').append('rect');
  3118. focusEnter.append('g').attr('class', 'nv-x nv-axis');
  3119. focusEnter.append('g').attr('class', 'nv-y nv-axis');
  3120. focusEnter.append('g').attr('class', 'nv-linesWrap');
  3121. focusEnter.append('g').attr('class', 'nv-interactive');
  3122. var contextEnter = gEnter.append('g').attr('class', 'nv-context');
  3123. contextEnter.append('g').attr('class', 'nv-background').append('rect');
  3124. contextEnter.append('g').attr('class', 'nv-x nv-axis');
  3125. contextEnter.append('g').attr('class', 'nv-y nv-axis');
  3126. contextEnter.append('g').attr('class', 'nv-linesWrap');
  3127. contextEnter.append('g').attr('class', 'nv-brushBackground');
  3128. contextEnter.append('g').attr('class', 'nv-x nv-brush');
  3129. // Legend
  3130. if (!showLegend) {
  3131. g.select('.nv-legendWrap').selectAll('*').remove();
  3132. } else {
  3133. legend.width(availableWidth);
  3134. g.select('.nv-legendWrap')
  3135. .datum(data)
  3136. .call(legend);
  3137. if (legendPosition === 'bottom') {
  3138. wrap.select('.nv-legendWrap')
  3139. .attr('transform', 'translate(0,' + (availableHeight1 + legend.height()) +')');
  3140. } else if (legendPosition === 'top') {
  3141. if ( margin.top != legend.height()) {
  3142. margin.top = legend.height();
  3143. availableHeight1 = nv.utils.availableHeight(height, container, margin) - (focusEnable ? focusHeight : 0);
  3144. }
  3145. wrap.select('.nv-legendWrap')
  3146. .attr('transform', 'translate(0,' + (-margin.top) +')');
  3147. }
  3148. }
  3149. wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
  3150. if (rightAlignYAxis) {
  3151. g.select(".nv-y.nv-axis")
  3152. .attr("transform", "translate(" + availableWidth + ",0)");
  3153. }
  3154. //Set up interactive layer
  3155. if (useInteractiveGuideline) {
  3156. interactiveLayer
  3157. .width(availableWidth)
  3158. .height(availableHeight1)
  3159. .margin({left:margin.left, top:margin.top})
  3160. .svgContainer(container)
  3161. .xScale(x);
  3162. wrap.select(".nv-interactive").call(interactiveLayer);
  3163. }
  3164. g.select('.nv-focus .nv-background rect')
  3165. .attr('width', availableWidth)
  3166. .attr('height', availableHeight1);
  3167. lines
  3168. .width(availableWidth)
  3169. .height(availableHeight1)
  3170. .color(data.map(function(d,i) {
  3171. return d.color || color(d, i);
  3172. }).filter(function(d,i) { return !data[i].disabled; }));
  3173. var linesWrap = g.select('.nv-linesWrap')
  3174. .datum(data.filter(function(d) { return !d.disabled; }));
  3175. // Setup Main (Focus) Axes
  3176. if (showXAxis) {
  3177. xAxis
  3178. .scale(x)
  3179. ._ticks(nv.utils.calcTicksX(availableWidth/100, data) )
  3180. .tickSize(-availableHeight1, 0);
  3181. }
  3182. if (showYAxis) {
  3183. yAxis
  3184. .scale(y)
  3185. ._ticks( nv.utils.calcTicksY(availableHeight1/36, data) )
  3186. .tickSize( -availableWidth, 0);
  3187. }
  3188. //============================================================
  3189. // Update Axes
  3190. //============================================================
  3191. function updateXAxis() {
  3192. if(showXAxis) {
  3193. g.select('.nv-focus .nv-x.nv-axis')
  3194. .transition()
  3195. .duration(duration)
  3196. .call(xAxis)
  3197. ;
  3198. }
  3199. }
  3200. function updateYAxis() {
  3201. if(showYAxis) {
  3202. g.select('.nv-focus .nv-y.nv-axis')
  3203. .transition()
  3204. .duration(duration)
  3205. .call(yAxis)
  3206. ;
  3207. }
  3208. }
  3209. g.select('.nv-focus .nv-x.nv-axis')
  3210. .attr('transform', 'translate(0,' + availableHeight1 + ')');
  3211. if( !focusEnable )
  3212. {
  3213. linesWrap.call(lines);
  3214. updateXAxis();
  3215. updateYAxis();
  3216. }
  3217. else
  3218. {
  3219. lines2
  3220. .defined(lines.defined())
  3221. .width(availableWidth)
  3222. .height(availableHeight2)
  3223. .color(data.map(function(d,i) {
  3224. return d.color || color(d, i);
  3225. }).filter(function(d,i) { return !data[i].disabled; }));
  3226. g.select('.nv-context')
  3227. .attr('transform', 'translate(0,' + ( availableHeight1 + margin.bottom + margin2.top) + ')')
  3228. .style('display', focusEnable ? 'initial' : 'none')
  3229. ;
  3230. var contextLinesWrap = g.select('.nv-context .nv-linesWrap')
  3231. .datum(data.filter(function(d) { return !d.disabled; }))
  3232. ;
  3233. d3.transition(contextLinesWrap).call(lines2);
  3234. // Setup Brush
  3235. brush
  3236. .x(x2)
  3237. .on('brush', function() {
  3238. onBrush();
  3239. });
  3240. if (brushExtent) brush.extent(brushExtent);
  3241. var brushBG = g.select('.nv-brushBackground').selectAll('g')
  3242. .data([brushExtent || brush.extent()]);
  3243. var brushBGenter = brushBG.enter()
  3244. .append('g');
  3245. brushBGenter.append('rect')
  3246. .attr('class', 'left')
  3247. .attr('x', 0)
  3248. .attr('y', 0)
  3249. .attr('height', availableHeight2);
  3250. brushBGenter.append('rect')
  3251. .attr('class', 'right')
  3252. .attr('x', 0)
  3253. .attr('y', 0)
  3254. .attr('height', availableHeight2);
  3255. var gBrush = g.select('.nv-x.nv-brush')
  3256. .call(brush);
  3257. gBrush.selectAll('rect')
  3258. .attr('height', availableHeight2);
  3259. gBrush.selectAll('.resize').append('path').attr('d', resizePath);
  3260. onBrush();
  3261. g.select('.nv-context .nv-background rect')
  3262. .attr('width', availableWidth)
  3263. .attr('height', availableHeight2);
  3264. // Setup Secondary (Context) Axes
  3265. if (focusShowAxisX) {
  3266. x2Axis
  3267. .scale(x2)
  3268. ._ticks( nv.utils.calcTicksX(availableWidth/100, data) )
  3269. .tickSize(-availableHeight2, 0);
  3270. g.select('.nv-context .nv-x.nv-axis')
  3271. .attr('transform', 'translate(0,' + y2.range()[0] + ')');
  3272. d3.transition(g.select('.nv-context .nv-x.nv-axis'))
  3273. .call(x2Axis);
  3274. }
  3275. if (focusShowAxisY) {
  3276. y2Axis
  3277. .scale(y2)
  3278. ._ticks( nv.utils.calcTicksY(availableHeight2/36, data) )
  3279. .tickSize( -availableWidth, 0);
  3280. d3.transition(g.select('.nv-context .nv-y.nv-axis'))
  3281. .call(y2Axis);
  3282. }
  3283. g.select('.nv-context .nv-x.nv-axis')
  3284. .attr('transform', 'translate(0,' + y2.range()[0] + ')');
  3285. }
  3286. //============================================================
  3287. // Event Handling/Dispatching (in chart's scope)
  3288. //------------------------------------------------------------
  3289. legend.dispatch.on('stateChange', function(newState) {
  3290. for (var key in newState)
  3291. state[key] = newState[key];
  3292. dispatch.stateChange(state);
  3293. chart.update();
  3294. });
  3295. interactiveLayer.dispatch.on('elementMousemove', function(e) {
  3296. lines.clearHighlights();
  3297. var singlePoint, pointIndex, pointXLocation, allData = [];
  3298. data
  3299. .filter(function(series, i) {
  3300. series.seriesIndex = i;
  3301. return !series.disabled && !series.disableTooltip;
  3302. })
  3303. .forEach(function(series,i) {
  3304. var extent = focusEnable ? (brush.empty() ? x2.domain() : brush.extent()) : x.domain();
  3305. var currentValues = series.values.filter(function(d,i) {
  3306. return lines.x()(d,i) >= extent[0] && lines.x()(d,i) <= extent[1];
  3307. });
  3308. pointIndex = nv.interactiveBisect(currentValues, e.pointXValue, lines.x());
  3309. var point = currentValues[pointIndex];
  3310. var pointYValue = chart.y()(point, pointIndex);
  3311. if (pointYValue !== null) {
  3312. lines.highlightPoint(series.seriesIndex, pointIndex, true);
  3313. }
  3314. if (point === undefined) return;
  3315. if (singlePoint === undefined) singlePoint = point;
  3316. if (pointXLocation === undefined) pointXLocation = chart.xScale()(chart.x()(point,pointIndex));
  3317. allData.push({
  3318. key: series.key,
  3319. value: pointYValue,
  3320. color: color(series,series.seriesIndex),
  3321. data: point
  3322. });
  3323. });
  3324. //Highlight the tooltip entry based on which point the mouse is closest to.
  3325. if (allData.length > 2) {
  3326. var yValue = chart.yScale().invert(e.mouseY);
  3327. var domainExtent = Math.abs(chart.yScale().domain()[0] - chart.yScale().domain()[1]);
  3328. var threshold = 0.03 * domainExtent;
  3329. var indexToHighlight = nv.nearestValueIndex(allData.map(function(d){return d.value;}),yValue,threshold);
  3330. if (indexToHighlight !== null)
  3331. allData[indexToHighlight].highlight = true;
  3332. }
  3333. var defaultValueFormatter = function(d,i) {
  3334. return d == null ? "N/A" : yAxis.tickFormat()(d);
  3335. };
  3336. interactiveLayer.tooltip
  3337. .chartContainer(chart.container.parentNode)
  3338. .valueFormatter(interactiveLayer.tooltip.valueFormatter() || defaultValueFormatter)
  3339. .data({
  3340. value: chart.x()( singlePoint,pointIndex ),
  3341. index: pointIndex,
  3342. series: allData
  3343. })();
  3344. interactiveLayer.renderGuideLine(pointXLocation);
  3345. });
  3346. interactiveLayer.dispatch.on('elementClick', function(e) {
  3347. var pointXLocation, allData = [];
  3348. data.filter(function(series, i) {
  3349. series.seriesIndex = i;
  3350. return !series.disabled;
  3351. }).forEach(function(series) {
  3352. var pointIndex = nv.interactiveBisect(series.values, e.pointXValue, chart.x());
  3353. var point = series.values[pointIndex];
  3354. if (typeof point === 'undefined') return;
  3355. if (typeof pointXLocation === 'undefined') pointXLocation = chart.xScale()(chart.x()(point,pointIndex));
  3356. var yPos = chart.yScale()(chart.y()(point,pointIndex));
  3357. allData.push({
  3358. point: point,
  3359. pointIndex: pointIndex,
  3360. pos: [pointXLocation, yPos],
  3361. seriesIndex: series.seriesIndex,
  3362. series: series
  3363. });
  3364. });
  3365. lines.dispatch.elementClick(allData);
  3366. });
  3367. interactiveLayer.dispatch.on("elementMouseout",function(e) {
  3368. lines.clearHighlights();
  3369. });
  3370. dispatch.on('changeState', function(e) {
  3371. if (typeof e.disabled !== 'undefined' && data.length === e.disabled.length) {
  3372. data.forEach(function(series,i) {
  3373. series.disabled = e.disabled[i];
  3374. });
  3375. state.disabled = e.disabled;
  3376. }
  3377. chart.update();
  3378. });
  3379. //============================================================
  3380. // Functions
  3381. //------------------------------------------------------------
  3382. // Taken from crossfilter (http://square.github.com/crossfilter/)
  3383. function resizePath(d) {
  3384. var e = +(d == 'e'),
  3385. x = e ? 1 : -1,
  3386. y = availableHeight2 / 3;
  3387. return 'M' + (0.5 * x) + ',' + y
  3388. + 'A6,6 0 0 ' + e + ' ' + (6.5 * x) + ',' + (y + 6)
  3389. + 'V' + (2 * y - 6)
  3390. + 'A6,6 0 0 ' + e + ' ' + (0.5 * x) + ',' + (2 * y)
  3391. + 'Z'
  3392. + 'M' + (2.5 * x) + ',' + (y + 8)
  3393. + 'V' + (2 * y - 8)
  3394. + 'M' + (4.5 * x) + ',' + (y + 8)
  3395. + 'V' + (2 * y - 8);
  3396. }
  3397. function updateBrushBG() {
  3398. if (!brush.empty()) brush.extent(brushExtent);
  3399. brushBG
  3400. .data([brush.empty() ? x2.domain() : brushExtent])
  3401. .each(function(d,i) {
  3402. var leftWidth = x2(d[0]) - x.range()[0],
  3403. rightWidth = availableWidth - x2(d[1]);
  3404. d3.select(this).select('.left')
  3405. .attr('width', leftWidth < 0 ? 0 : leftWidth);
  3406. d3.select(this).select('.right')
  3407. .attr('x', x2(d[1]))
  3408. .attr('width', rightWidth < 0 ? 0 : rightWidth);
  3409. });
  3410. }
  3411. function onBrush() {
  3412. brushExtent = brush.empty() ? null : brush.extent();
  3413. var extent = brush.empty() ? x2.domain() : brush.extent();
  3414. //The brush extent cannot be less than one. If it is, don't update the line chart.
  3415. if (Math.abs(extent[0] - extent[1]) <= 1) {
  3416. return;
  3417. }
  3418. dispatch.brush({extent: extent, brush: brush});
  3419. updateBrushBG();
  3420. // Update Main (Focus)
  3421. var focusLinesWrap = g.select('.nv-focus .nv-linesWrap')
  3422. .datum(
  3423. data
  3424. .filter(function(d) { return !d.disabled; })
  3425. .map(function(d,i) {
  3426. return {
  3427. key: d.key,
  3428. area: d.area,
  3429. classed: d.classed,
  3430. values: d.values.filter(function(d,i) {
  3431. return lines.x()(d,i) >= extent[0] && lines.x()(d,i) <= extent[1];
  3432. }),
  3433. disableTooltip: d.disableTooltip
  3434. };
  3435. })
  3436. );
  3437. focusLinesWrap.transition().duration(duration).call(lines);
  3438. // Update Main (Focus) Axes
  3439. updateXAxis();
  3440. updateYAxis();
  3441. }
  3442. });
  3443. renderWatch.renderEnd('lineChart immediate');
  3444. return chart;
  3445. }
  3446. //============================================================
  3447. // Event Handling/Dispatching (out of chart's scope)
  3448. //------------------------------------------------------------
  3449. lines.dispatch.on('elementMouseover.tooltip', function(evt) {
  3450. if(!evt.series.disableTooltip){
  3451. tooltip.data(evt).hidden(false);
  3452. }
  3453. });
  3454. lines.dispatch.on('elementMouseout.tooltip', function(evt) {
  3455. tooltip.hidden(true);
  3456. });
  3457. //============================================================
  3458. // Expose Public Variables
  3459. //------------------------------------------------------------
  3460. // expose chart's sub-components
  3461. chart.dispatch = dispatch;
  3462. chart.lines = lines;
  3463. chart.lines2 = lines2;
  3464. chart.legend = legend;
  3465. chart.xAxis = xAxis;
  3466. chart.x2Axis = x2Axis;
  3467. chart.yAxis = yAxis;
  3468. chart.y2Axis = y2Axis;
  3469. chart.interactiveLayer = interactiveLayer;
  3470. chart.tooltip = tooltip;
  3471. chart.state = state;
  3472. chart.dispatch = dispatch;
  3473. chart.options = nv.utils.optionsFunc.bind(chart);
  3474. chart._options = Object.create({}, {
  3475. // simple options, just get/set the necessary values
  3476. width: {get: function(){return width;}, set: function(_){width=_;}},
  3477. height: {get: function(){return height;}, set: function(_){height=_;}},
  3478. showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}},
  3479. legendPosition: {get: function(){return legendPosition;}, set: function(_){legendPosition=_;}},
  3480. showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}},
  3481. showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}},
  3482. focusEnable: {get: function(){return focusEnable;}, set: function(_){focusEnable=_;}},
  3483. focusHeight: {get: function(){return height2;}, set: function(_){focusHeight=_;}},
  3484. focusShowAxisX: {get: function(){return focusShowAxisX;}, set: function(_){focusShowAxisX=_;}},
  3485. focusShowAxisY: {get: function(){return focusShowAxisY;}, set: function(_){focusShowAxisY=_;}},
  3486. brushExtent: {get: function(){return brushExtent;}, set: function(_){brushExtent=_;}},
  3487. defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}},
  3488. noData: {get: function(){return noData;}, set: function(_){noData=_;}},
  3489. // options that require extra logic in the setter
  3490. margin: {get: function(){return margin;}, set: function(_){
  3491. margin.top = _.top !== undefined ? _.top : margin.top;
  3492. margin.right = _.right !== undefined ? _.right : margin.right;
  3493. margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
  3494. margin.left = _.left !== undefined ? _.left : margin.left;
  3495. }},
  3496. duration: {get: function(){return duration;}, set: function(_){
  3497. duration = _;
  3498. renderWatch.reset(duration);
  3499. lines.duration(duration);
  3500. xAxis.duration(duration);
  3501. x2Axis.duration(duration);
  3502. yAxis.duration(duration);
  3503. y2Axis.duration(duration);
  3504. }},
  3505. focusMargin: {get: function(){return margin2;}, set: function(_){
  3506. margin2.top = _.top !== undefined ? _.top : margin2.top;
  3507. margin2.right = _.right !== undefined ? _.right : margin2.right;
  3508. margin2.bottom = _.bottom !== undefined ? _.bottom : margin2.bottom;
  3509. margin2.left = _.left !== undefined ? _.left : margin2.left;
  3510. }},
  3511. color: {get: function(){return color;}, set: function(_){
  3512. color = nv.utils.getColor(_);
  3513. legend.color(color);
  3514. lines.color(color);
  3515. }},
  3516. interpolate: {get: function(){return lines.interpolate();}, set: function(_){
  3517. lines.interpolate(_);
  3518. lines2.interpolate(_);
  3519. }},
  3520. xTickFormat: {get: function(){return xAxis.tickFormat();}, set: function(_){
  3521. xAxis.tickFormat(_);
  3522. x2Axis.tickFormat(_);
  3523. }},
  3524. yTickFormat: {get: function(){return yAxis.tickFormat();}, set: function(_){
  3525. yAxis.tickFormat(_);
  3526. y2Axis.tickFormat(_);
  3527. }},
  3528. x: {get: function(){return lines.x();}, set: function(_){
  3529. lines.x(_);
  3530. lines2.x(_);
  3531. }},
  3532. y: {get: function(){return lines.y();}, set: function(_){
  3533. lines.y(_);
  3534. lines2.y(_);
  3535. }},
  3536. rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){
  3537. rightAlignYAxis = _;
  3538. yAxis.orient( rightAlignYAxis ? 'right' : 'left');
  3539. }},
  3540. useInteractiveGuideline: {get: function(){return useInteractiveGuideline;}, set: function(_){
  3541. useInteractiveGuideline = _;
  3542. if (useInteractiveGuideline) {
  3543. lines.interactive(false);
  3544. lines.useVoronoi(false);
  3545. }
  3546. }}
  3547. });
  3548. nv.utils.inheritOptions(chart, lines);
  3549. nv.utils.initOptions(chart);
  3550. return chart;
  3551. };
  3552. nv.models.lineWithFocusChart = function() {
  3553. return nv.models.lineChart()
  3554. .margin({ bottom: 30 })
  3555. .focusEnable( true );
  3556. };
  3557. nv.models.line = function() {
  3558. "use strict";
  3559. //============================================================
  3560. // Public Variables with Default Settings
  3561. //------------------------------------------------------------
  3562. var scatter = nv.models.scatter()
  3563. ;
  3564. var margin = {top: 0, right: 0, bottom: 0, left: 0}
  3565. , width = 960
  3566. , height = 500
  3567. , container = null
  3568. , strokeWidth = 1.5
  3569. , color = nv.utils.defaultColor() // a function that returns a color
  3570. , getX = function(d) { return d.x } // accessor to get the x value from a data point
  3571. , getY = function(d) { return d.y } // accessor to get the y value from a data point
  3572. , 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
  3573. , isArea = function(d) { return d.area } // decides if a line is an area or just a line
  3574. , clipEdge = false // if true, masks lines within x and y scale
  3575. , x //can be accessed via chart.xScale()
  3576. , y //can be accessed via chart.yScale()
  3577. , interpolate = "linear" // controls the line interpolation
  3578. , duration = 250
  3579. , dispatch = d3.dispatch('elementClick', 'elementMouseover', 'elementMouseout', 'renderEnd')
  3580. ;
  3581. scatter
  3582. .pointSize(16) // default size
  3583. .pointDomain([16,256]) //set to speed up calculation, needs to be unset if there is a custom size accessor
  3584. ;
  3585. //============================================================
  3586. //============================================================
  3587. // Private Variables
  3588. //------------------------------------------------------------
  3589. var x0, y0 //used to store previous scales
  3590. , renderWatch = nv.utils.renderWatch(dispatch, duration)
  3591. ;
  3592. //============================================================
  3593. function chart(selection) {
  3594. renderWatch.reset();
  3595. renderWatch.models(scatter);
  3596. selection.each(function(data) {
  3597. container = d3.select(this);
  3598. var availableWidth = nv.utils.availableWidth(width, container, margin),
  3599. availableHeight = nv.utils.availableHeight(height, container, margin);
  3600. nv.utils.initSVG(container);
  3601. // Setup Scales
  3602. x = scatter.xScale();
  3603. y = scatter.yScale();
  3604. x0 = x0 || x;
  3605. y0 = y0 || y;
  3606. // Setup containers and skeleton of chart
  3607. var wrap = container.selectAll('g.nv-wrap.nv-line').data([data]);
  3608. var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-line');
  3609. var defsEnter = wrapEnter.append('defs');
  3610. var gEnter = wrapEnter.append('g');
  3611. var g = wrap.select('g');
  3612. gEnter.append('g').attr('class', 'nv-groups');
  3613. gEnter.append('g').attr('class', 'nv-scatterWrap');
  3614. wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
  3615. scatter
  3616. .width(availableWidth)
  3617. .height(availableHeight);
  3618. var scatterWrap = wrap.select('.nv-scatterWrap');
  3619. scatterWrap.call(scatter);
  3620. defsEnter.append('clipPath')
  3621. .attr('id', 'nv-edge-clip-' + scatter.id())
  3622. .append('rect');
  3623. wrap.select('#nv-edge-clip-' + scatter.id() + ' rect')
  3624. .attr('width', availableWidth)
  3625. .attr('height', (availableHeight > 0) ? availableHeight : 0);
  3626. g .attr('clip-path', clipEdge ? 'url(#nv-edge-clip-' + scatter.id() + ')' : '');
  3627. scatterWrap
  3628. .attr('clip-path', clipEdge ? 'url(#nv-edge-clip-' + scatter.id() + ')' : '');
  3629. var groups = wrap.select('.nv-groups').selectAll('.nv-group')
  3630. .data(function(d) { return d }, function(d) { return d.key });
  3631. groups.enter().append('g')
  3632. .style('stroke-opacity', 1e-6)
  3633. .style('stroke-width', function(d) { return d.strokeWidth || strokeWidth })
  3634. .style('fill-opacity', 1e-6);
  3635. groups.exit().remove();
  3636. groups
  3637. .attr('class', function(d,i) {
  3638. return (d.classed || '') + ' nv-group nv-series-' + i;
  3639. })
  3640. .classed('hover', function(d) { return d.hover })
  3641. .style('fill', function(d,i){ return color(d, i) })
  3642. .style('stroke', function(d,i){ return color(d, i)});
  3643. groups.watchTransition(renderWatch, 'line: groups')
  3644. .style('stroke-opacity', 1)
  3645. .style('fill-opacity', function(d) { return d.fillOpacity || .5});
  3646. var areaPaths = groups.selectAll('path.nv-area')
  3647. .data(function(d) { return isArea(d) ? [d] : [] }); // this is done differently than lines because I need to check if series is an area
  3648. areaPaths.enter().append('path')
  3649. .attr('class', 'nv-area')
  3650. .attr('d', function(d) {
  3651. return d3.svg.area()
  3652. .interpolate(interpolate)
  3653. .defined(defined)
  3654. .x(function(d,i) { return nv.utils.NaNtoZero(x0(getX(d,i))) })
  3655. .y0(function(d,i) { return nv.utils.NaNtoZero(y0(getY(d,i))) })
  3656. .y1(function(d,i) { return y0( y.domain()[0] <= 0 ? y.domain()[1] >= 0 ? 0 : y.domain()[1] : y.domain()[0] ) })
  3657. //.y1(function(d,i) { return y0(0) }) //assuming 0 is within y domain.. may need to tweak this
  3658. .apply(this, [d.values])
  3659. });
  3660. groups.exit().selectAll('path.nv-area')
  3661. .remove();
  3662. areaPaths.watchTransition(renderWatch, 'line: areaPaths')
  3663. .attr('d', function(d) {
  3664. return d3.svg.area()
  3665. .interpolate(interpolate)
  3666. .defined(defined)
  3667. .x(function(d,i) { return nv.utils.NaNtoZero(x(getX(d,i))) })
  3668. .y0(function(d,i) { return nv.utils.NaNtoZero(y(getY(d,i))) })
  3669. .y1(function(d,i) { return y( y.domain()[0] <= 0 ? y.domain()[1] >= 0 ? 0 : y.domain()[1] : y.domain()[0] ) })
  3670. //.y1(function(d,i) { return y0(0) }) //assuming 0 is within y domain.. may need to tweak this
  3671. .apply(this, [d.values])
  3672. });
  3673. var linePaths = groups.selectAll('path.nv-line')
  3674. .data(function(d) { return [d.values] });
  3675. linePaths.enter().append('path')
  3676. .attr('class', 'nv-line')
  3677. .attr('d',
  3678. d3.svg.line()
  3679. .interpolate(interpolate)
  3680. .defined(defined)
  3681. .x(function(d,i) { return nv.utils.NaNtoZero(x0(getX(d,i))) })
  3682. .y(function(d,i) { return nv.utils.NaNtoZero(y0(getY(d,i))) })
  3683. );
  3684. linePaths.watchTransition(renderWatch, 'line: linePaths')
  3685. .attr('d',
  3686. d3.svg.line()
  3687. .interpolate(interpolate)
  3688. .defined(defined)
  3689. .x(function(d,i) { return nv.utils.NaNtoZero(x(getX(d,i))) })
  3690. .y(function(d,i) { return nv.utils.NaNtoZero(y(getY(d,i))) })
  3691. );
  3692. //store old scales for use in transitions on update
  3693. x0 = x.copy();
  3694. y0 = y.copy();
  3695. });
  3696. renderWatch.renderEnd('line immediate');
  3697. return chart;
  3698. }
  3699. //============================================================
  3700. // Expose Public Variables
  3701. //------------------------------------------------------------
  3702. chart.dispatch = dispatch;
  3703. chart.scatter = scatter;
  3704. // Pass through events
  3705. scatter.dispatch.on('elementClick', function(){ dispatch.elementClick.apply(this, arguments); });
  3706. scatter.dispatch.on('elementMouseover', function(){ dispatch.elementMouseover.apply(this, arguments); });
  3707. scatter.dispatch.on('elementMouseout', function(){ dispatch.elementMouseout.apply(this, arguments); });
  3708. chart.options = nv.utils.optionsFunc.bind(chart);
  3709. chart._options = Object.create({}, {
  3710. // simple options, just get/set the necessary values
  3711. width: {get: function(){return width;}, set: function(_){width=_;}},
  3712. height: {get: function(){return height;}, set: function(_){height=_;}},
  3713. defined: {get: function(){return defined;}, set: function(_){defined=_;}},
  3714. interpolate: {get: function(){return interpolate;}, set: function(_){interpolate=_;}},
  3715. clipEdge: {get: function(){return clipEdge;}, set: function(_){clipEdge=_;}},
  3716. // options that require extra logic in the setter
  3717. margin: {get: function(){return margin;}, set: function(_){
  3718. margin.top = _.top !== undefined ? _.top : margin.top;
  3719. margin.right = _.right !== undefined ? _.right : margin.right;
  3720. margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
  3721. margin.left = _.left !== undefined ? _.left : margin.left;
  3722. }},
  3723. duration: {get: function(){return duration;}, set: function(_){
  3724. duration = _;
  3725. renderWatch.reset(duration);
  3726. scatter.duration(duration);
  3727. }},
  3728. isArea: {get: function(){return isArea;}, set: function(_){
  3729. isArea = d3.functor(_);
  3730. }},
  3731. x: {get: function(){return getX;}, set: function(_){
  3732. getX = _;
  3733. scatter.x(_);
  3734. }},
  3735. y: {get: function(){return getY;}, set: function(_){
  3736. getY = _;
  3737. scatter.y(_);
  3738. }},
  3739. color: {get: function(){return color;}, set: function(_){
  3740. color = nv.utils.getColor(_);
  3741. scatter.color(color);
  3742. }}
  3743. });
  3744. nv.utils.inheritOptions(chart, scatter);
  3745. nv.utils.initOptions(chart);
  3746. return chart;
  3747. };
  3748. nv.models.discreteBarChart = function() {
  3749. "use strict";
  3750. //============================================================
  3751. // Public Variables with Default Settings
  3752. //------------------------------------------------------------
  3753. var discretebar = nv.models.discreteBar()
  3754. , xAxis = nv.models.axis()
  3755. , yAxis = nv.models.axis()
  3756. , legend = nv.models.legend()
  3757. , tooltip = nv.models.tooltip()
  3758. ;
  3759. var margin = {top: 15, right: 10, bottom: 50, left: 60}
  3760. , width = null
  3761. , height = null
  3762. , color = nv.utils.getColor()
  3763. , showLegend = false
  3764. , showXAxis = true
  3765. , showYAxis = true
  3766. , rightAlignYAxis = false
  3767. , staggerLabels = false
  3768. , wrapLabels = false
  3769. , rotateLabels = 0
  3770. , x
  3771. , y
  3772. , noData = null
  3773. , dispatch = d3.dispatch('beforeUpdate','renderEnd')
  3774. , duration = 250
  3775. ;
  3776. xAxis
  3777. .orient('bottom')
  3778. .showMaxMin(false)
  3779. .tickFormat(function(d) { return d })
  3780. ;
  3781. yAxis
  3782. .orient((rightAlignYAxis) ? 'right' : 'left')
  3783. .tickFormat(d3.format(',.1f'))
  3784. ;
  3785. tooltip
  3786. .duration(0)
  3787. .headerEnabled(false)
  3788. .valueFormatter(function(d, i) {
  3789. return yAxis.tickFormat()(d, i);
  3790. })
  3791. .keyFormatter(function(d, i) {
  3792. return xAxis.tickFormat()(d, i);
  3793. });
  3794. //============================================================
  3795. // Private Variables
  3796. //------------------------------------------------------------
  3797. var renderWatch = nv.utils.renderWatch(dispatch, duration);
  3798. function chart(selection) {
  3799. renderWatch.reset();
  3800. renderWatch.models(discretebar);
  3801. if (showXAxis) renderWatch.models(xAxis);
  3802. if (showYAxis) renderWatch.models(yAxis);
  3803. selection.each(function(data) {
  3804. var container = d3.select(this),
  3805. that = this;
  3806. nv.utils.initSVG(container);
  3807. var availableWidth = nv.utils.availableWidth(width, container, margin),
  3808. availableHeight = nv.utils.availableHeight(height, container, margin);
  3809. chart.update = function() {
  3810. dispatch.beforeUpdate();
  3811. container.transition().duration(duration).call(chart);
  3812. };
  3813. chart.container = this;
  3814. // Display No Data message if there's nothing to show.
  3815. if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) {
  3816. nv.utils.noData(chart, container);
  3817. return chart;
  3818. } else {
  3819. container.selectAll('.nv-noData').remove();
  3820. }
  3821. // Setup Scales
  3822. x = discretebar.xScale();
  3823. y = discretebar.yScale().clamp(true);
  3824. // Setup containers and skeleton of chart
  3825. var wrap = container.selectAll('g.nv-wrap.nv-discreteBarWithAxes').data([data]);
  3826. var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-discreteBarWithAxes').append('g');
  3827. var defsEnter = gEnter.append('defs');
  3828. var g = wrap.select('g');
  3829. gEnter.append('g').attr('class', 'nv-x nv-axis');
  3830. gEnter.append('g').attr('class', 'nv-y nv-axis')
  3831. .append('g').attr('class', 'nv-zeroLine')
  3832. .append('line');
  3833. gEnter.append('g').attr('class', 'nv-barsWrap');
  3834. gEnter.append('g').attr('class', 'nv-legendWrap');
  3835. g.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
  3836. // Legend
  3837. if (!showLegend) {
  3838. g.select('.nv-legendWrap').selectAll('*').remove();
  3839. } else {
  3840. legend.width(availableWidth);
  3841. g.select('.nv-legendWrap')
  3842. .datum(data)
  3843. .call(legend);
  3844. if ( margin.top != legend.height()) {
  3845. margin.top = legend.height();
  3846. availableHeight = nv.utils.availableHeight(height, container, margin);
  3847. }
  3848. wrap.select('.nv-legendWrap')
  3849. .attr('transform', 'translate(0,' + (-margin.top) +')')
  3850. }
  3851. if (rightAlignYAxis) {
  3852. g.select(".nv-y.nv-axis")
  3853. .attr("transform", "translate(" + availableWidth + ",0)");
  3854. }
  3855. // Main Chart Component(s)
  3856. discretebar
  3857. .width(availableWidth)
  3858. .height(availableHeight);
  3859. var barsWrap = g.select('.nv-barsWrap')
  3860. .datum(data.filter(function(d) { return !d.disabled }));
  3861. barsWrap.transition().call(discretebar);
  3862. defsEnter.append('clipPath')
  3863. .attr('id', 'nv-x-label-clip-' + discretebar.id())
  3864. .append('rect');
  3865. g.select('#nv-x-label-clip-' + discretebar.id() + ' rect')
  3866. .attr('width', x.rangeBand() * (staggerLabels ? 2 : 1))
  3867. .attr('height', 16)
  3868. .attr('x', -x.rangeBand() / (staggerLabels ? 1 : 2 ));
  3869. // Setup Axes
  3870. if (showXAxis) {
  3871. xAxis
  3872. .scale(x)
  3873. ._ticks( nv.utils.calcTicksX(availableWidth/100, data) )
  3874. .tickSize(-availableHeight, 0);
  3875. g.select('.nv-x.nv-axis')
  3876. .attr('transform', 'translate(0,' + (y.range()[0] + ((discretebar.showValues() && y.domain()[0] < 0) ? 16 : 0)) + ')');
  3877. g.select('.nv-x.nv-axis').call(xAxis);
  3878. var xTicks = g.select('.nv-x.nv-axis').selectAll('g');
  3879. if (staggerLabels) {
  3880. xTicks
  3881. .selectAll('text')
  3882. .attr('transform', function(d,i,j) { return 'translate(0,' + (j % 2 == 0 ? '5' : '17') + ')' })
  3883. }
  3884. if (rotateLabels) {
  3885. xTicks
  3886. .selectAll('.tick text')
  3887. .attr('transform', 'rotate(' + rotateLabels + ' 0,0)')
  3888. .style('text-anchor', rotateLabels > 0 ? 'start' : 'end');
  3889. }
  3890. if (wrapLabels) {
  3891. g.selectAll('.tick text')
  3892. .call(nv.utils.wrapTicks, chart.xAxis.rangeBand())
  3893. }
  3894. }
  3895. if (showYAxis) {
  3896. yAxis
  3897. .scale(y)
  3898. ._ticks( nv.utils.calcTicksY(availableHeight/36, data) )
  3899. .tickSize( -availableWidth, 0);
  3900. g.select('.nv-y.nv-axis').call(yAxis);
  3901. }
  3902. // Zero line
  3903. g.select(".nv-zeroLine line")
  3904. .attr("x1",0)
  3905. .attr("x2",(rightAlignYAxis) ? -availableWidth : availableWidth)
  3906. .attr("y1", y(0))
  3907. .attr("y2", y(0))
  3908. ;
  3909. });
  3910. renderWatch.renderEnd('discreteBar chart immediate');
  3911. return chart;
  3912. }
  3913. //============================================================
  3914. // Event Handling/Dispatching (out of chart's scope)
  3915. //------------------------------------------------------------
  3916. discretebar.dispatch.on('elementMouseover.tooltip', function(evt) {
  3917. evt['series'] = {
  3918. key: chart.x()(evt.data),
  3919. value: chart.y()(evt.data),
  3920. color: evt.color
  3921. };
  3922. tooltip.data(evt).hidden(false);
  3923. });
  3924. discretebar.dispatch.on('elementMouseout.tooltip', function(evt) {
  3925. tooltip.hidden(true);
  3926. });
  3927. discretebar.dispatch.on('elementMousemove.tooltip', function(evt) {
  3928. tooltip();
  3929. });
  3930. //============================================================
  3931. // Expose Public Variables
  3932. //------------------------------------------------------------
  3933. chart.dispatch = dispatch;
  3934. chart.discretebar = discretebar;
  3935. chart.legend = legend;
  3936. chart.xAxis = xAxis;
  3937. chart.yAxis = yAxis;
  3938. chart.tooltip = tooltip;
  3939. chart.options = nv.utils.optionsFunc.bind(chart);
  3940. chart._options = Object.create({}, {
  3941. // simple options, just get/set the necessary values
  3942. width: {get: function(){return width;}, set: function(_){width=_;}},
  3943. height: {get: function(){return height;}, set: function(_){height=_;}},
  3944. showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}},
  3945. staggerLabels: {get: function(){return staggerLabels;}, set: function(_){staggerLabels=_;}},
  3946. rotateLabels: {get: function(){return rotateLabels;}, set: function(_){rotateLabels=_;}},
  3947. wrapLabels: {get: function(){return wrapLabels;}, set: function(_){wrapLabels=!!_;}},
  3948. showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}},
  3949. showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}},
  3950. noData: {get: function(){return noData;}, set: function(_){noData=_;}},
  3951. // options that require extra logic in the setter
  3952. margin: {get: function(){return margin;}, set: function(_){
  3953. margin.top = _.top !== undefined ? _.top : margin.top;
  3954. margin.right = _.right !== undefined ? _.right : margin.right;
  3955. margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
  3956. margin.left = _.left !== undefined ? _.left : margin.left;
  3957. }},
  3958. duration: {get: function(){return duration;}, set: function(_){
  3959. duration = _;
  3960. renderWatch.reset(duration);
  3961. discretebar.duration(duration);
  3962. xAxis.duration(duration);
  3963. yAxis.duration(duration);
  3964. }},
  3965. color: {get: function(){return color;}, set: function(_){
  3966. color = nv.utils.getColor(_);
  3967. discretebar.color(color);
  3968. legend.color(color);
  3969. }},
  3970. rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){
  3971. rightAlignYAxis = _;
  3972. yAxis.orient( (_) ? 'right' : 'left');
  3973. }}
  3974. });
  3975. nv.utils.inheritOptions(chart, discretebar);
  3976. nv.utils.initOptions(chart);
  3977. return chart;
  3978. }
  3979. nv.models.pieChart = function() {
  3980. "use strict";
  3981. //============================================================
  3982. // Public Variables with Default Settings
  3983. //------------------------------------------------------------
  3984. var pie = nv.models.pie();
  3985. var legend = nv.models.legend();
  3986. var tooltip = nv.models.tooltip();
  3987. var margin = {top: 30, right: 20, bottom: 20, left: 20}
  3988. , width = null
  3989. , height = null
  3990. , showLegend = true
  3991. , legendPosition = "top"
  3992. , color = nv.utils.defaultColor()
  3993. , state = nv.utils.state()
  3994. , defaultState = null
  3995. , noData = null
  3996. , duration = 250
  3997. , dispatch = d3.dispatch('stateChange', 'changeState','renderEnd')
  3998. ;
  3999. tooltip
  4000. .duration(0)
  4001. .headerEnabled(false)
  4002. .valueFormatter(function(d, i) {
  4003. return pie.valueFormat()(d, i);
  4004. });
  4005. //============================================================
  4006. // Private Variables
  4007. //------------------------------------------------------------
  4008. var renderWatch = nv.utils.renderWatch(dispatch);
  4009. var stateGetter = function(data) {
  4010. return function(){
  4011. return {
  4012. active: data.map(function(d) { return !d.disabled })
  4013. };
  4014. }
  4015. };
  4016. var stateSetter = function(data) {
  4017. return function(state) {
  4018. if (state.active !== undefined) {
  4019. data.forEach(function (series, i) {
  4020. series.disabled = !state.active[i];
  4021. });
  4022. }
  4023. }
  4024. };
  4025. //============================================================
  4026. // Chart function
  4027. //------------------------------------------------------------
  4028. function chart(selection) {
  4029. renderWatch.reset();
  4030. renderWatch.models(pie);
  4031. selection.each(function(data) {
  4032. var container = d3.select(this);
  4033. nv.utils.initSVG(container);
  4034. var that = this;
  4035. var availableWidth = nv.utils.availableWidth(width, container, margin),
  4036. availableHeight = nv.utils.availableHeight(height, container, margin);
  4037. chart.update = function() { container.transition().call(chart); };
  4038. chart.container = this;
  4039. state.setter(stateSetter(data), chart.update)
  4040. .getter(stateGetter(data))
  4041. .update();
  4042. //set state.disabled
  4043. state.disabled = data.map(function(d) { return !!d.disabled });
  4044. if (!defaultState) {
  4045. var key;
  4046. defaultState = {};
  4047. for (key in state) {
  4048. if (state[key] instanceof Array)
  4049. defaultState[key] = state[key].slice(0);
  4050. else
  4051. defaultState[key] = state[key];
  4052. }
  4053. }
  4054. // Display No Data message if there's nothing to show.
  4055. if (!data || !data.length) {
  4056. nv.utils.noData(chart, container);
  4057. return chart;
  4058. } else {
  4059. container.selectAll('.nv-noData').remove();
  4060. }
  4061. // Setup containers and skeleton of chart
  4062. var wrap = container.selectAll('g.nv-wrap.nv-pieChart').data([data]);
  4063. var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-pieChart').append('g');
  4064. var g = wrap.select('g');
  4065. gEnter.append('g').attr('class', 'nv-pieWrap');
  4066. gEnter.append('g').attr('class', 'nv-legendWrap');
  4067. // Legend
  4068. if (!showLegend) {
  4069. g.select('.nv-legendWrap').selectAll('*').remove();
  4070. } else {
  4071. if (legendPosition === "top") {
  4072. legend.width( availableWidth ).key(pie.x());
  4073. wrap.select('.nv-legendWrap')
  4074. .datum(data)
  4075. .call(legend);
  4076. if ( margin.top != legend.height()) {
  4077. margin.top = legend.height();
  4078. availableHeight = nv.utils.availableHeight(height, container, margin);
  4079. }
  4080. wrap.select('.nv-legendWrap')
  4081. .attr('transform', 'translate(0,' + (-margin.top) +')');
  4082. } else if (legendPosition === "right") {
  4083. var legendWidth = nv.models.legend().width();
  4084. if (availableWidth / 2 < legendWidth) {
  4085. legendWidth = (availableWidth / 2)
  4086. }
  4087. legend.height(availableHeight).key(pie.x());
  4088. legend.width(legendWidth);
  4089. availableWidth -= legend.width();
  4090. wrap.select('.nv-legendWrap')
  4091. .datum(data)
  4092. .call(legend)
  4093. .attr('transform', 'translate(' + (availableWidth) +',0)');
  4094. }
  4095. }
  4096. wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
  4097. // Main Chart Component(s)
  4098. pie.width(availableWidth).height(availableHeight);
  4099. var pieWrap = g.select('.nv-pieWrap').datum([data]);
  4100. d3.transition(pieWrap).call(pie);
  4101. //============================================================
  4102. // Event Handling/Dispatching (in chart's scope)
  4103. //------------------------------------------------------------
  4104. legend.dispatch.on('stateChange', function(newState) {
  4105. for (var key in newState) {
  4106. state[key] = newState[key];
  4107. }
  4108. dispatch.stateChange(state);
  4109. chart.update();
  4110. });
  4111. // Update chart from a state object passed to event handler
  4112. dispatch.on('changeState', function(e) {
  4113. if (typeof e.disabled !== 'undefined') {
  4114. data.forEach(function(series,i) {
  4115. series.disabled = e.disabled[i];
  4116. });
  4117. state.disabled = e.disabled;
  4118. }
  4119. chart.update();
  4120. });
  4121. });
  4122. renderWatch.renderEnd('pieChart immediate');
  4123. return chart;
  4124. }
  4125. //============================================================
  4126. // Event Handling/Dispatching (out of chart's scope)
  4127. //------------------------------------------------------------
  4128. pie.dispatch.on('elementMouseover.tooltip', function(evt) {
  4129. evt['series'] = {
  4130. key: chart.x()(evt.data),
  4131. value: chart.y()(evt.data),
  4132. color: evt.color
  4133. };
  4134. tooltip.data(evt).hidden(false);
  4135. });
  4136. pie.dispatch.on('elementMouseout.tooltip', function(evt) {
  4137. tooltip.hidden(true);
  4138. });
  4139. pie.dispatch.on('elementMousemove.tooltip', function(evt) {
  4140. tooltip();
  4141. });
  4142. //============================================================
  4143. // Expose Public Variables
  4144. //------------------------------------------------------------
  4145. // expose chart's sub-components
  4146. chart.legend = legend;
  4147. chart.dispatch = dispatch;
  4148. chart.pie = pie;
  4149. chart.tooltip = tooltip;
  4150. chart.options = nv.utils.optionsFunc.bind(chart);
  4151. // use Object get/set functionality to map between vars and chart functions
  4152. chart._options = Object.create({}, {
  4153. // simple options, just get/set the necessary values
  4154. width: {get: function(){return width;}, set: function(_){width=_;}},
  4155. height: {get: function(){return height;}, set: function(_){height=_;}},
  4156. noData: {get: function(){return noData;}, set: function(_){noData=_;}},
  4157. showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}},
  4158. legendPosition: {get: function(){return legendPosition;}, set: function(_){legendPosition=_;}},
  4159. defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}},
  4160. // options that require extra logic in the setter
  4161. color: {get: function(){return color;}, set: function(_){
  4162. color = _;
  4163. legend.color(color);
  4164. pie.color(color);
  4165. }},
  4166. duration: {get: function(){return duration;}, set: function(_){
  4167. duration = _;
  4168. renderWatch.reset(duration);
  4169. }},
  4170. margin: {get: function(){return margin;}, set: function(_){
  4171. margin.top = _.top !== undefined ? _.top : margin.top;
  4172. margin.right = _.right !== undefined ? _.right : margin.right;
  4173. margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
  4174. margin.left = _.left !== undefined ? _.left : margin.left;
  4175. }}
  4176. });
  4177. nv.utils.inheritOptions(chart, pie);
  4178. nv.utils.initOptions(chart);
  4179. return chart;
  4180. };
  4181. //TODO: consider deprecating by adding necessary features to multiBar model
  4182. nv.models.discreteBar = function() {
  4183. "use strict";
  4184. //============================================================
  4185. // Public Variables with Default Settings
  4186. //------------------------------------------------------------
  4187. var margin = {top: 0, right: 0, bottom: 0, left: 0}
  4188. , width = 960
  4189. , height = 500
  4190. , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one
  4191. , container
  4192. , x = d3.scale.ordinal()
  4193. , y = d3.scale.linear()
  4194. , getX = function(d) { return d.x }
  4195. , getY = function(d) { return d.y }
  4196. , forceY = [0] // 0 is forced by default.. this makes sense for the majority of bar graphs... user can always do chart.forceY([]) to remove
  4197. , color = nv.utils.defaultColor()
  4198. , showValues = false
  4199. , valueFormat = d3.format(',.2f')
  4200. , xDomain
  4201. , yDomain
  4202. , xRange
  4203. , yRange
  4204. , dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd')
  4205. , rectClass = 'discreteBar'
  4206. , duration = 250
  4207. ;
  4208. //============================================================
  4209. // Private Variables
  4210. //------------------------------------------------------------
  4211. var x0, y0;
  4212. var renderWatch = nv.utils.renderWatch(dispatch, duration);
  4213. function chart(selection) {
  4214. renderWatch.reset();
  4215. selection.each(function(data) {
  4216. var availableWidth = width - margin.left - margin.right,
  4217. availableHeight = height - margin.top - margin.bottom;
  4218. container = d3.select(this);
  4219. nv.utils.initSVG(container);
  4220. //add series index to each data point for reference
  4221. data.forEach(function(series, i) {
  4222. series.values.forEach(function(point) {
  4223. point.series = i;
  4224. });
  4225. });
  4226. // Setup Scales
  4227. // remap and flatten the data for use in calculating the scales' domains
  4228. var seriesData = (xDomain && yDomain) ? [] : // if we know xDomain and yDomain, no need to calculate
  4229. data.map(function(d) {
  4230. return d.values.map(function(d,i) {
  4231. return { x: getX(d,i), y: getY(d,i), y0: d.y0 }
  4232. })
  4233. });
  4234. x .domain(xDomain || d3.merge(seriesData).map(function(d) { return d.x }))
  4235. .rangeBands(xRange || [0, availableWidth], .1);
  4236. y .domain(yDomain || d3.extent(d3.merge(seriesData).map(function(d) { return d.y }).concat(forceY)));
  4237. // If showValues, pad the Y axis range to account for label height
  4238. if (showValues) y.range(yRange || [availableHeight - (y.domain()[0] < 0 ? 12 : 0), y.domain()[1] > 0 ? 12 : 0]);
  4239. else y.range(yRange || [availableHeight, 0]);
  4240. //store old scales if they exist
  4241. x0 = x0 || x;
  4242. y0 = y0 || y.copy().range([y(0),y(0)]);
  4243. // Setup containers and skeleton of chart
  4244. var wrap = container.selectAll('g.nv-wrap.nv-discretebar').data([data]);
  4245. var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-discretebar');
  4246. var gEnter = wrapEnter.append('g');
  4247. var g = wrap.select('g');
  4248. gEnter.append('g').attr('class', 'nv-groups');
  4249. wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
  4250. //TODO: by definition, the discrete bar should not have multiple groups, will modify/remove later
  4251. var groups = wrap.select('.nv-groups').selectAll('.nv-group')
  4252. .data(function(d) { return d }, function(d) { return d.key });
  4253. groups.enter().append('g')
  4254. .style('stroke-opacity', 1e-6)
  4255. .style('fill-opacity', 1e-6);
  4256. groups.exit()
  4257. .watchTransition(renderWatch, 'discreteBar: exit groups')
  4258. .style('stroke-opacity', 1e-6)
  4259. .style('fill-opacity', 1e-6)
  4260. .remove();
  4261. groups
  4262. .attr('class', function(d,i) { return 'nv-group nv-series-' + i })
  4263. .classed('hover', function(d) { return d.hover });
  4264. groups
  4265. .watchTransition(renderWatch, 'discreteBar: groups')
  4266. .style('stroke-opacity', 1)
  4267. .style('fill-opacity', .75);
  4268. var bars = groups.selectAll('g.nv-bar')
  4269. .data(function(d) { return d.values });
  4270. bars.exit().remove();
  4271. var barsEnter = bars.enter().append('g')
  4272. .attr('transform', function(d,i,j) {
  4273. return 'translate(' + (x(getX(d,i)) + x.rangeBand() * .05 ) + ', ' + y(0) + ')'
  4274. })
  4275. .on('mouseover', function(d,i) { //TODO: figure out why j works above, but not here
  4276. d3.select(this).classed('hover', true);
  4277. dispatch.elementMouseover({
  4278. data: d,
  4279. index: i,
  4280. color: d3.select(this).style("fill")
  4281. });
  4282. })
  4283. .on('mouseout', function(d,i) {
  4284. d3.select(this).classed('hover', false);
  4285. dispatch.elementMouseout({
  4286. data: d,
  4287. index: i,
  4288. color: d3.select(this).style("fill")
  4289. });
  4290. })
  4291. .on('mousemove', function(d,i) {
  4292. dispatch.elementMousemove({
  4293. data: d,
  4294. index: i,
  4295. color: d3.select(this).style("fill")
  4296. });
  4297. })
  4298. .on('click', function(d,i) {
  4299. var element = this;
  4300. dispatch.elementClick({
  4301. data: d,
  4302. index: i,
  4303. color: d3.select(this).style("fill"),
  4304. event: d3.event,
  4305. element: element
  4306. });
  4307. d3.event.stopPropagation();
  4308. })
  4309. .on('dblclick', function(d,i) {
  4310. dispatch.elementDblClick({
  4311. data: d,
  4312. index: i,
  4313. color: d3.select(this).style("fill")
  4314. });
  4315. d3.event.stopPropagation();
  4316. });
  4317. barsEnter.append('rect')
  4318. .attr('height', 0)
  4319. .attr('width', x.rangeBand() * .9 / data.length )
  4320. if (showValues) {
  4321. barsEnter.append('text')
  4322. .attr('text-anchor', 'middle')
  4323. ;
  4324. bars.select('text')
  4325. .text(function(d,i) { return valueFormat(getY(d,i)) })
  4326. .watchTransition(renderWatch, 'discreteBar: bars text')
  4327. .attr('x', x.rangeBand() * .9 / 2)
  4328. .attr('y', function(d,i) { return getY(d,i) < 0 ? y(getY(d,i)) - y(0) + 12 : -4 })
  4329. ;
  4330. } else {
  4331. bars.selectAll('text').remove();
  4332. }
  4333. bars
  4334. .attr('class', function(d,i) { return getY(d,i) < 0 ? 'nv-bar negative' : 'nv-bar positive' })
  4335. .style('fill', function(d,i) { return d.color || color(d,i) })
  4336. .style('stroke', function(d,i) { return d.color || color(d,i) })
  4337. .select('rect')
  4338. .attr('class', rectClass)
  4339. .watchTransition(renderWatch, 'discreteBar: bars rect')
  4340. .attr('width', x.rangeBand() * .9 / data.length);
  4341. bars.watchTransition(renderWatch, 'discreteBar: bars')
  4342. //.delay(function(d,i) { return i * 1200 / data[0].values.length })
  4343. .attr('transform', function(d,i) {
  4344. var left = x(getX(d,i)) + x.rangeBand() * .05,
  4345. top = getY(d,i) < 0 ?
  4346. y(0) :
  4347. y(0) - y(getY(d,i)) < 1 ?
  4348. y(0) - 1 : //make 1 px positive bars show up above y=0
  4349. y(getY(d,i));
  4350. return 'translate(' + left + ', ' + top + ')'
  4351. })
  4352. .select('rect')
  4353. .attr('height', function(d,i) {
  4354. return Math.max(Math.abs(y(getY(d,i)) - y(0)), 1)
  4355. });
  4356. //store old scales for use in transitions on update
  4357. x0 = x.copy();
  4358. y0 = y.copy();
  4359. });
  4360. renderWatch.renderEnd('discreteBar immediate');
  4361. return chart;
  4362. }
  4363. //============================================================
  4364. // Expose Public Variables
  4365. //------------------------------------------------------------
  4366. chart.dispatch = dispatch;
  4367. chart.options = nv.utils.optionsFunc.bind(chart);
  4368. chart._options = Object.create({}, {
  4369. // simple options, just get/set the necessary values
  4370. width: {get: function(){return width;}, set: function(_){width=_;}},
  4371. height: {get: function(){return height;}, set: function(_){height=_;}},
  4372. forceY: {get: function(){return forceY;}, set: function(_){forceY=_;}},
  4373. showValues: {get: function(){return showValues;}, set: function(_){showValues=_;}},
  4374. x: {get: function(){return getX;}, set: function(_){getX=_;}},
  4375. y: {get: function(){return getY;}, set: function(_){getY=_;}},
  4376. xScale: {get: function(){return x;}, set: function(_){x=_;}},
  4377. yScale: {get: function(){return y;}, set: function(_){y=_;}},
  4378. xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}},
  4379. yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}},
  4380. xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}},
  4381. yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}},
  4382. valueFormat: {get: function(){return valueFormat;}, set: function(_){valueFormat=_;}},
  4383. id: {get: function(){return id;}, set: function(_){id=_;}},
  4384. rectClass: {get: function(){return rectClass;}, set: function(_){rectClass=_;}},
  4385. // options that require extra logic in the setter
  4386. margin: {get: function(){return margin;}, set: function(_){
  4387. margin.top = _.top !== undefined ? _.top : margin.top;
  4388. margin.right = _.right !== undefined ? _.right : margin.right;
  4389. margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
  4390. margin.left = _.left !== undefined ? _.left : margin.left;
  4391. }},
  4392. color: {get: function(){return color;}, set: function(_){
  4393. color = nv.utils.getColor(_);
  4394. }},
  4395. duration: {get: function(){return duration;}, set: function(_){
  4396. duration = _;
  4397. renderWatch.reset(duration);
  4398. }}
  4399. });
  4400. nv.utils.initOptions(chart);
  4401. return chart;
  4402. };
  4403. nv.models.multiBarChart = function() {
  4404. "use strict";
  4405. //============================================================
  4406. // Public Variables with Default Settings
  4407. //------------------------------------------------------------
  4408. var multibar = nv.models.multiBar()
  4409. , xAxis = nv.models.axis()
  4410. , yAxis = nv.models.axis()
  4411. , interactiveLayer = nv.interactiveGuideline()
  4412. , legend = nv.models.legend()
  4413. , controls = nv.models.legend()
  4414. , tooltip = nv.models.tooltip()
  4415. ;
  4416. var margin = {top: 30, right: 20, bottom: 50, left: 60}
  4417. , width = null
  4418. , height = null
  4419. , color = nv.utils.defaultColor()
  4420. , showControls = true
  4421. , controlLabels = {}
  4422. , showLegend = true
  4423. , showXAxis = true
  4424. , showYAxis = true
  4425. , rightAlignYAxis = false
  4426. , reduceXTicks = true // if false a tick will show for every data point
  4427. , staggerLabels = false
  4428. , wrapLabels = false
  4429. , rotateLabels = 0
  4430. , x //can be accessed via chart.xScale()
  4431. , y //can be accessed via chart.yScale()
  4432. , state = nv.utils.state()
  4433. , defaultState = null
  4434. , noData = null
  4435. , dispatch = d3.dispatch('stateChange', 'changeState', 'renderEnd')
  4436. , controlWidth = function() { return showControls ? 180 : 0 }
  4437. , duration = 250
  4438. , useInteractiveGuideline = false
  4439. ;
  4440. state.stacked = false // DEPRECATED Maintained for backward compatibility
  4441. multibar.stacked(false);
  4442. xAxis
  4443. .orient('bottom')
  4444. .tickPadding(7)
  4445. .showMaxMin(false)
  4446. .tickFormat(function(d) { return d })
  4447. ;
  4448. yAxis
  4449. .orient((rightAlignYAxis) ? 'right' : 'left')
  4450. .tickFormat(d3.format(',.1f'))
  4451. ;
  4452. tooltip
  4453. .duration(0)
  4454. .valueFormatter(function(d, i) {
  4455. return yAxis.tickFormat()(d, i);
  4456. })
  4457. .headerFormatter(function(d, i) {
  4458. return xAxis.tickFormat()(d, i);
  4459. });
  4460. controls.updateState(false);
  4461. //============================================================
  4462. // Private Variables
  4463. //------------------------------------------------------------
  4464. var renderWatch = nv.utils.renderWatch(dispatch);
  4465. var stacked = false;
  4466. var stateGetter = function(data) {
  4467. return function(){
  4468. return {
  4469. active: data.map(function(d) { return !d.disabled }),
  4470. stacked: stacked
  4471. };
  4472. }
  4473. };
  4474. var stateSetter = function(data) {
  4475. return function(state) {
  4476. if (state.stacked !== undefined)
  4477. stacked = state.stacked;
  4478. if (state.active !== undefined)
  4479. data.forEach(function(series,i) {
  4480. series.disabled = !state.active[i];
  4481. });
  4482. }
  4483. };
  4484. function chart(selection) {
  4485. renderWatch.reset();
  4486. renderWatch.models(multibar);
  4487. if (showXAxis) renderWatch.models(xAxis);
  4488. if (showYAxis) renderWatch.models(yAxis);
  4489. selection.each(function(data) {
  4490. var container = d3.select(this),
  4491. that = this;
  4492. nv.utils.initSVG(container);
  4493. var availableWidth = nv.utils.availableWidth(width, container, margin),
  4494. availableHeight = nv.utils.availableHeight(height, container, margin);
  4495. chart.update = function() {
  4496. if (duration === 0)
  4497. container.call(chart);
  4498. else
  4499. container.transition()
  4500. .duration(duration)
  4501. .call(chart);
  4502. };
  4503. chart.container = this;
  4504. state
  4505. .setter(stateSetter(data), chart.update)
  4506. .getter(stateGetter(data))
  4507. .update();
  4508. // DEPRECATED set state.disableddisabled
  4509. state.disabled = data.map(function(d) { return !!d.disabled });
  4510. if (!defaultState) {
  4511. var key;
  4512. defaultState = {};
  4513. for (key in state) {
  4514. if (state[key] instanceof Array)
  4515. defaultState[key] = state[key].slice(0);
  4516. else
  4517. defaultState[key] = state[key];
  4518. }
  4519. }
  4520. // Display noData message if there's nothing to show.
  4521. if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) {
  4522. nv.utils.noData(chart, container)
  4523. return chart;
  4524. } else {
  4525. container.selectAll('.nv-noData').remove();
  4526. }
  4527. // Setup Scales
  4528. x = multibar.xScale();
  4529. y = multibar.yScale();
  4530. // Setup containers and skeleton of chart
  4531. var wrap = container.selectAll('g.nv-wrap.nv-multiBarWithLegend').data([data]);
  4532. var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-multiBarWithLegend').append('g');
  4533. var g = wrap.select('g');
  4534. gEnter.append('g').attr('class', 'nv-x nv-axis');
  4535. gEnter.append('g').attr('class', 'nv-y nv-axis');
  4536. gEnter.append('g').attr('class', 'nv-barsWrap');
  4537. gEnter.append('g').attr('class', 'nv-legendWrap');
  4538. gEnter.append('g').attr('class', 'nv-controlsWrap');
  4539. gEnter.append('g').attr('class', 'nv-interactive');
  4540. // Legend
  4541. if (!showLegend) {
  4542. g.select('.nv-legendWrap').selectAll('*').remove();
  4543. } else {
  4544. legend.width(availableWidth - controlWidth());
  4545. g.select('.nv-legendWrap')
  4546. .datum(data)
  4547. .call(legend);
  4548. if ( margin.top != legend.height()) {
  4549. margin.top = legend.height();
  4550. availableHeight = nv.utils.availableHeight(height, container, margin);
  4551. }
  4552. g.select('.nv-legendWrap')
  4553. .attr('transform', 'translate(' + controlWidth() + ',' + (-margin.top) +')');
  4554. }
  4555. // Controls
  4556. if (!showControls) {
  4557. g.select('.nv-controlsWrap').selectAll('*').remove();
  4558. } else {
  4559. var controlsData = [
  4560. { key: controlLabels.grouped || 'Grouped', disabled: multibar.stacked() },
  4561. { key: controlLabels.stacked || 'Stacked', disabled: !multibar.stacked() }
  4562. ];
  4563. controls.width(controlWidth()).color(['#444', '#444', '#444']);
  4564. g.select('.nv-controlsWrap')
  4565. .datum(controlsData)
  4566. .attr('transform', 'translate(0,' + (-margin.top) +')')
  4567. .call(controls);
  4568. }
  4569. wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
  4570. if (rightAlignYAxis) {
  4571. g.select(".nv-y.nv-axis")
  4572. .attr("transform", "translate(" + availableWidth + ",0)");
  4573. }
  4574. // Main Chart Component(s)
  4575. multibar
  4576. .disabled(data.map(function(series) { return series.disabled }))
  4577. .width(availableWidth)
  4578. .height(availableHeight)
  4579. .color(data.map(function(d,i) {
  4580. return d.color || color(d, i);
  4581. }).filter(function(d,i) { return !data[i].disabled }));
  4582. var barsWrap = g.select('.nv-barsWrap')
  4583. .datum(data.filter(function(d) { return !d.disabled }));
  4584. barsWrap.call(multibar);
  4585. // Setup Axes
  4586. if (showXAxis) {
  4587. xAxis
  4588. .scale(x)
  4589. ._ticks( nv.utils.calcTicksX(availableWidth/100, data) )
  4590. .tickSize(-availableHeight, 0);
  4591. g.select('.nv-x.nv-axis')
  4592. .attr('transform', 'translate(0,' + y.range()[0] + ')');
  4593. g.select('.nv-x.nv-axis')
  4594. .call(xAxis);
  4595. var xTicks = g.select('.nv-x.nv-axis > g').selectAll('g');
  4596. xTicks
  4597. .selectAll('line, text')
  4598. .style('opacity', 1)
  4599. if (staggerLabels) {
  4600. var getTranslate = function(x,y) {
  4601. return "translate(" + x + "," + y + ")";
  4602. };
  4603. var staggerUp = 5, staggerDown = 17; //pixels to stagger by
  4604. // Issue #140
  4605. xTicks
  4606. .selectAll("text")
  4607. .attr('transform', function(d,i,j) {
  4608. return getTranslate(0, (j % 2 == 0 ? staggerUp : staggerDown));
  4609. });
  4610. var totalInBetweenTicks = d3.selectAll(".nv-x.nv-axis .nv-wrap g g text")[0].length;
  4611. g.selectAll(".nv-x.nv-axis .nv-axisMaxMin text")
  4612. .attr("transform", function(d,i) {
  4613. return getTranslate(0, (i === 0 || totalInBetweenTicks % 2 !== 0) ? staggerDown : staggerUp);
  4614. });
  4615. }
  4616. if (wrapLabels) {
  4617. g.selectAll('.tick text')
  4618. .call(nv.utils.wrapTicks, chart.xAxis.rangeBand())
  4619. }
  4620. if (reduceXTicks)
  4621. xTicks
  4622. .filter(function(d,i) {
  4623. return i % Math.ceil(data[0].values.length / (availableWidth / 100)) !== 0;
  4624. })
  4625. .selectAll('text, line')
  4626. .style('opacity', 0);
  4627. if(rotateLabels)
  4628. xTicks
  4629. .selectAll('.tick text')
  4630. .attr('transform', 'rotate(' + rotateLabels + ' 0,0)')
  4631. .style('text-anchor', rotateLabels > 0 ? 'start' : 'end');
  4632. g.select('.nv-x.nv-axis').selectAll('g.nv-axisMaxMin text')
  4633. .style('opacity', 1);
  4634. }
  4635. if (showYAxis) {
  4636. yAxis
  4637. .scale(y)
  4638. ._ticks( nv.utils.calcTicksY(availableHeight/36, data) )
  4639. .tickSize( -availableWidth, 0);
  4640. g.select('.nv-y.nv-axis')
  4641. .call(yAxis);
  4642. }
  4643. //Set up interactive layer
  4644. if (useInteractiveGuideline) {
  4645. interactiveLayer
  4646. .width(availableWidth)
  4647. .height(availableHeight)
  4648. .margin({left:margin.left, top:margin.top})
  4649. .svgContainer(container)
  4650. .xScale(x);
  4651. wrap.select(".nv-interactive").call(interactiveLayer);
  4652. }
  4653. //============================================================
  4654. // Event Handling/Dispatching (in chart's scope)
  4655. //------------------------------------------------------------
  4656. legend.dispatch.on('stateChange', function(newState) {
  4657. for (var key in newState)
  4658. state[key] = newState[key];
  4659. dispatch.stateChange(state);
  4660. chart.update();
  4661. });
  4662. controls.dispatch.on('legendClick', function(d,i) {
  4663. if (!d.disabled) return;
  4664. controlsData = controlsData.map(function(s) {
  4665. s.disabled = true;
  4666. return s;
  4667. });
  4668. d.disabled = false;
  4669. switch (d.key) {
  4670. case 'Grouped':
  4671. case controlLabels.grouped:
  4672. multibar.stacked(false);
  4673. break;
  4674. case 'Stacked':
  4675. case controlLabels.stacked:
  4676. multibar.stacked(true);
  4677. break;
  4678. }
  4679. state.stacked = multibar.stacked();
  4680. dispatch.stateChange(state);
  4681. chart.update();
  4682. });
  4683. // Update chart from a state object passed to event handler
  4684. dispatch.on('changeState', function(e) {
  4685. if (typeof e.disabled !== 'undefined') {
  4686. data.forEach(function(series,i) {
  4687. series.disabled = e.disabled[i];
  4688. });
  4689. state.disabled = e.disabled;
  4690. }
  4691. if (typeof e.stacked !== 'undefined') {
  4692. multibar.stacked(e.stacked);
  4693. state.stacked = e.stacked;
  4694. stacked = e.stacked;
  4695. }
  4696. chart.update();
  4697. });
  4698. if (useInteractiveGuideline) {
  4699. interactiveLayer.dispatch.on('elementMousemove', function(e) {
  4700. if (e.pointXValue == undefined) return;
  4701. var singlePoint, pointIndex, pointXLocation, xValue, allData = [];
  4702. data
  4703. .filter(function(series, i) {
  4704. series.seriesIndex = i;
  4705. return !series.disabled;
  4706. })
  4707. .forEach(function(series,i) {
  4708. pointIndex = x.domain().indexOf(e.pointXValue)
  4709. var point = series.values[pointIndex];
  4710. if (point === undefined) return;
  4711. xValue = point.x;
  4712. if (singlePoint === undefined) singlePoint = point;
  4713. if (pointXLocation === undefined) pointXLocation = e.mouseX
  4714. allData.push({
  4715. key: series.key,
  4716. value: chart.y()(point, pointIndex),
  4717. color: color(series,series.seriesIndex),
  4718. data: series.values[pointIndex]
  4719. });
  4720. });
  4721. interactiveLayer.tooltip
  4722. .chartContainer(that.parentNode)
  4723. .data({
  4724. value: xValue,
  4725. index: pointIndex,
  4726. series: allData
  4727. })();
  4728. interactiveLayer.renderGuideLine(pointXLocation);
  4729. });
  4730. interactiveLayer.dispatch.on("elementMouseout",function(e) {
  4731. interactiveLayer.tooltip.hidden(true);
  4732. });
  4733. }
  4734. else {
  4735. multibar.dispatch.on('elementMouseover.tooltip', function(evt) {
  4736. evt.value = chart.x()(evt.data);
  4737. evt['series'] = {
  4738. key: evt.data.key,
  4739. value: chart.y()(evt.data),
  4740. color: evt.color
  4741. };
  4742. tooltip.data(evt).hidden(false);
  4743. });
  4744. multibar.dispatch.on('elementMouseout.tooltip', function(evt) {
  4745. tooltip.hidden(true);
  4746. });
  4747. multibar.dispatch.on('elementMousemove.tooltip', function(evt) {
  4748. tooltip();
  4749. });
  4750. }
  4751. });
  4752. renderWatch.renderEnd('multibarchart immediate');
  4753. return chart;
  4754. }
  4755. //============================================================
  4756. // Expose Public Variables
  4757. //------------------------------------------------------------
  4758. // expose chart's sub-components
  4759. chart.dispatch = dispatch;
  4760. chart.multibar = multibar;
  4761. chart.legend = legend;
  4762. chart.controls = controls;
  4763. chart.xAxis = xAxis;
  4764. chart.yAxis = yAxis;
  4765. chart.state = state;
  4766. chart.tooltip = tooltip;
  4767. chart.interactiveLayer = interactiveLayer;
  4768. chart.options = nv.utils.optionsFunc.bind(chart);
  4769. chart._options = Object.create({}, {
  4770. // simple options, just get/set the necessary values
  4771. width: {get: function(){return width;}, set: function(_){width=_;}},
  4772. height: {get: function(){return height;}, set: function(_){height=_;}},
  4773. showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}},
  4774. showControls: {get: function(){return showControls;}, set: function(_){showControls=_;}},
  4775. controlLabels: {get: function(){return controlLabels;}, set: function(_){controlLabels=_;}},
  4776. showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}},
  4777. showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}},
  4778. defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}},
  4779. noData: {get: function(){return noData;}, set: function(_){noData=_;}},
  4780. reduceXTicks: {get: function(){return reduceXTicks;}, set: function(_){reduceXTicks=_;}},
  4781. rotateLabels: {get: function(){return rotateLabels;}, set: function(_){rotateLabels=_;}},
  4782. staggerLabels: {get: function(){return staggerLabels;}, set: function(_){staggerLabels=_;}},
  4783. wrapLabels: {get: function(){return wrapLabels;}, set: function(_){wrapLabels=!!_;}},
  4784. // options that require extra logic in the setter
  4785. margin: {get: function(){return margin;}, set: function(_){
  4786. margin.top = _.top !== undefined ? _.top : margin.top;
  4787. margin.right = _.right !== undefined ? _.right : margin.right;
  4788. margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
  4789. margin.left = _.left !== undefined ? _.left : margin.left;
  4790. }},
  4791. duration: {get: function(){return duration;}, set: function(_){
  4792. duration = _;
  4793. multibar.duration(duration);
  4794. xAxis.duration(duration);
  4795. yAxis.duration(duration);
  4796. renderWatch.reset(duration);
  4797. }},
  4798. color: {get: function(){return color;}, set: function(_){
  4799. color = nv.utils.getColor(_);
  4800. legend.color(color);
  4801. }},
  4802. rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){
  4803. rightAlignYAxis = _;
  4804. yAxis.orient( rightAlignYAxis ? 'right' : 'left');
  4805. }},
  4806. useInteractiveGuideline: {get: function(){return useInteractiveGuideline;}, set: function(_){
  4807. useInteractiveGuideline = _;
  4808. }},
  4809. barColor: {get: function(){return multibar.barColor;}, set: function(_){
  4810. multibar.barColor(_);
  4811. legend.color(function(d,i) {return d3.rgb('#ccc').darker(i * 1.5).toString();})
  4812. }}
  4813. });
  4814. nv.utils.inheritOptions(chart, multibar);
  4815. nv.utils.initOptions(chart);
  4816. return chart;
  4817. };
  4818. nv.models.pie = function() {
  4819. "use strict";
  4820. //============================================================
  4821. // Public Variables with Default Settings
  4822. //------------------------------------------------------------
  4823. var margin = {top: 0, right: 0, bottom: 0, left: 0}
  4824. , width = 500
  4825. , height = 500
  4826. , getX = function(d) { return d.x }
  4827. , getY = function(d) { return d.y }
  4828. , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one
  4829. , container = null
  4830. , color = nv.utils.defaultColor()
  4831. , valueFormat = d3.format(',.2f')
  4832. , showLabels = true
  4833. , labelsOutside = false
  4834. , labelType = "key"
  4835. , labelThreshold = .02 //if slice percentage is under this, don't show label
  4836. , donut = false
  4837. , title = false
  4838. , growOnHover = true
  4839. , titleOffset = 0
  4840. , labelSunbeamLayout = false
  4841. , startAngle = false
  4842. , padAngle = false
  4843. , endAngle = false
  4844. , cornerRadius = 0
  4845. , donutRatio = 0.5
  4846. , arcsRadius = []
  4847. , dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd')
  4848. ;
  4849. var arcs = [];
  4850. var arcsOver = [];
  4851. //============================================================
  4852. // chart function
  4853. //------------------------------------------------------------
  4854. var renderWatch = nv.utils.renderWatch(dispatch);
  4855. function chart(selection) {
  4856. renderWatch.reset();
  4857. selection.each(function(data) {
  4858. var availableWidth = width - margin.left - margin.right
  4859. , availableHeight = height - margin.top - margin.bottom
  4860. , radius = Math.min(availableWidth, availableHeight) / 2
  4861. , arcsRadiusOuter = []
  4862. , arcsRadiusInner = []
  4863. ;
  4864. container = d3.select(this)
  4865. if (arcsRadius.length === 0) {
  4866. var outer = radius - radius / 5;
  4867. var inner = donutRatio * radius;
  4868. for (var i = 0; i < data[0].length; i++) {
  4869. arcsRadiusOuter.push(outer);
  4870. arcsRadiusInner.push(inner);
  4871. }
  4872. } else {
  4873. if(growOnHover){
  4874. arcsRadiusOuter = arcsRadius.map(function (d) { return (d.outer - d.outer / 5) * radius; });
  4875. arcsRadiusInner = arcsRadius.map(function (d) { return (d.inner - d.inner / 5) * radius; });
  4876. donutRatio = d3.min(arcsRadius.map(function (d) { return (d.inner - d.inner / 5); }));
  4877. } else {
  4878. arcsRadiusOuter = arcsRadius.map(function (d) { return d.outer * radius; });
  4879. arcsRadiusInner = arcsRadius.map(function (d) { return d.inner * radius; });
  4880. donutRatio = d3.min(arcsRadius.map(function (d) { return d.inner; }));
  4881. }
  4882. }
  4883. nv.utils.initSVG(container);
  4884. // Setup containers and skeleton of chart
  4885. var wrap = container.selectAll('.nv-wrap.nv-pie').data(data);
  4886. var wrapEnter = wrap.enter().append('g').attr('class','nvd3 nv-wrap nv-pie nv-chart-' + id);
  4887. var gEnter = wrapEnter.append('g');
  4888. var g = wrap.select('g');
  4889. var g_pie = gEnter.append('g').attr('class', 'nv-pie');
  4890. gEnter.append('g').attr('class', 'nv-pieLabels');
  4891. wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
  4892. g.select('.nv-pie').attr('transform', 'translate(' + availableWidth / 2 + ',' + availableHeight / 2 + ')');
  4893. g.select('.nv-pieLabels').attr('transform', 'translate(' + availableWidth / 2 + ',' + availableHeight / 2 + ')');
  4894. //
  4895. container.on('click', function(d,i) {
  4896. dispatch.chartClick({
  4897. data: d,
  4898. index: i,
  4899. pos: d3.event,
  4900. id: id
  4901. });
  4902. });
  4903. arcs = [];
  4904. arcsOver = [];
  4905. for (var i = 0; i < data[0].length; i++) {
  4906. var arc = d3.svg.arc().outerRadius(arcsRadiusOuter[i]);
  4907. var arcOver = d3.svg.arc().outerRadius(arcsRadiusOuter[i] + 5);
  4908. if (startAngle !== false) {
  4909. arc.startAngle(startAngle);
  4910. arcOver.startAngle(startAngle);
  4911. }
  4912. if (endAngle !== false) {
  4913. arc.endAngle(endAngle);
  4914. arcOver.endAngle(endAngle);
  4915. }
  4916. if (donut) {
  4917. arc.innerRadius(arcsRadiusInner[i]);
  4918. arcOver.innerRadius(arcsRadiusInner[i]);
  4919. }
  4920. if (arc.cornerRadius && cornerRadius) {
  4921. arc.cornerRadius(cornerRadius);
  4922. arcOver.cornerRadius(cornerRadius);
  4923. }
  4924. arcs.push(arc);
  4925. arcsOver.push(arcOver);
  4926. }
  4927. // Setup the Pie chart and choose the data element
  4928. var pie = d3.layout.pie()
  4929. .sort(null)
  4930. .value(function(d) { return d.disabled ? 0 : getY(d) });
  4931. // padAngle added in d3 3.5
  4932. if (pie.padAngle && padAngle) {
  4933. pie.padAngle(padAngle);
  4934. }
  4935. // if title is specified and donut, put it in the middle
  4936. if (donut && title) {
  4937. g_pie.append("text").attr('class', 'nv-pie-title');
  4938. wrap.select('.nv-pie-title')
  4939. .style("text-anchor", "middle")
  4940. .text(function (d) {
  4941. return title;
  4942. })
  4943. .style("font-size", (Math.min(availableWidth, availableHeight)) * donutRatio * 2 / (title.length + 2) + "px")
  4944. .attr("dy", "0.35em") // trick to vertically center text
  4945. .attr('transform', function(d, i) {
  4946. return 'translate(0, '+ titleOffset + ')';
  4947. });
  4948. }
  4949. var slices = wrap.select('.nv-pie').selectAll('.nv-slice').data(pie);
  4950. var pieLabels = wrap.select('.nv-pieLabels').selectAll('.nv-label').data(pie);
  4951. slices.exit().remove();
  4952. pieLabels.exit().remove();
  4953. var ae = slices.enter().append('g');
  4954. ae.attr('class', 'nv-slice');
  4955. ae.on('mouseover', function(d, i) {
  4956. d3.select(this).classed('hover', true);
  4957. if (growOnHover) {
  4958. d3.select(this).select("path").transition()
  4959. .duration(70)
  4960. .attr("d", arcsOver[i]);
  4961. }
  4962. dispatch.elementMouseover({
  4963. data: d.data,
  4964. index: i,
  4965. color: d3.select(this).style("fill")
  4966. });
  4967. });
  4968. ae.on('mouseout', function(d, i) {
  4969. d3.select(this).classed('hover', false);
  4970. if (growOnHover) {
  4971. d3.select(this).select("path").transition()
  4972. .duration(50)
  4973. .attr("d", arcs[i]);
  4974. }
  4975. dispatch.elementMouseout({data: d.data, index: i});
  4976. });
  4977. ae.on('mousemove', function(d, i) {
  4978. dispatch.elementMousemove({data: d.data, index: i});
  4979. });
  4980. ae.on('click', function(d, i) {
  4981. var element = this;
  4982. dispatch.elementClick({
  4983. data: d.data,
  4984. index: i,
  4985. color: d3.select(this).style("fill"),
  4986. event: d3.event,
  4987. element: element
  4988. });
  4989. });
  4990. ae.on('dblclick', function(d, i) {
  4991. dispatch.elementDblClick({
  4992. data: d.data,
  4993. index: i,
  4994. color: d3.select(this).style("fill")
  4995. });
  4996. });
  4997. slices.attr('fill', function(d,i) { return color(d.data, i); });
  4998. slices.attr('stroke', function(d,i) { return color(d.data, i); });
  4999. var paths = ae.append('path').each(function(d) {
  5000. this._current = d;
  5001. });
  5002. slices.select('path')
  5003. .transition()
  5004. .attr('d', function (d, i) { return arcs[i](d); })
  5005. .attrTween('d', arcTween);
  5006. if (showLabels) {
  5007. // This does the normal label
  5008. var labelsArc = [];
  5009. for (var i = 0; i < data[0].length; i++) {
  5010. labelsArc.push(arcs[i]);
  5011. if (labelsOutside) {
  5012. if (donut) {
  5013. labelsArc[i] = d3.svg.arc().outerRadius(arcs[i].outerRadius());
  5014. if (startAngle !== false) labelsArc[i].startAngle(startAngle);
  5015. if (endAngle !== false) labelsArc[i].endAngle(endAngle);
  5016. }
  5017. } else if (!donut) {
  5018. labelsArc[i].innerRadius(0);
  5019. }
  5020. }
  5021. pieLabels.enter().append("g").classed("nv-label",true).each(function(d,i) {
  5022. var group = d3.select(this);
  5023. group.attr('transform', function (d, i) {
  5024. if (labelSunbeamLayout) {
  5025. d.outerRadius = arcsRadiusOuter[i] + 10; // Set Outer Coordinate
  5026. d.innerRadius = arcsRadiusOuter[i] + 15; // Set Inner Coordinate
  5027. var rotateAngle = (d.startAngle + d.endAngle) / 2 * (180 / Math.PI);
  5028. if ((d.startAngle + d.endAngle) / 2 < Math.PI) {
  5029. rotateAngle -= 90;
  5030. } else {
  5031. rotateAngle += 90;
  5032. }
  5033. return 'translate(' + labelsArc[i].centroid(d) + ') rotate(' + rotateAngle + ')';
  5034. } else {
  5035. d.outerRadius = radius + 10; // Set Outer Coordinate
  5036. d.innerRadius = radius + 15; // Set Inner Coordinate
  5037. return 'translate(' + labelsArc[i].centroid(d) + ')'
  5038. }
  5039. });
  5040. group.append('rect')
  5041. .style('stroke', '#fff')
  5042. .style('fill', '#fff')
  5043. .attr("rx", 3)
  5044. .attr("ry", 3);
  5045. group.append('text')
  5046. .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
  5047. .style('fill', '#000')
  5048. });
  5049. var labelLocationHash = {};
  5050. var avgHeight = 14;
  5051. var avgWidth = 140;
  5052. var createHashKey = function(coordinates) {
  5053. return Math.floor(coordinates[0]/avgWidth) * avgWidth + ',' + Math.floor(coordinates[1]/avgHeight) * avgHeight;
  5054. };
  5055. var getSlicePercentage = function(d) {
  5056. return (d.endAngle - d.startAngle) / (2 * Math.PI);
  5057. };
  5058. pieLabels.watchTransition(renderWatch, 'pie labels').attr('transform', function (d, i) {
  5059. if (labelSunbeamLayout) {
  5060. d.outerRadius = arcsRadiusOuter[i] + 10; // Set Outer Coordinate
  5061. d.innerRadius = arcsRadiusOuter[i] + 15; // Set Inner Coordinate
  5062. var rotateAngle = (d.startAngle + d.endAngle) / 2 * (180 / Math.PI);
  5063. if ((d.startAngle + d.endAngle) / 2 < Math.PI) {
  5064. rotateAngle -= 90;
  5065. } else {
  5066. rotateAngle += 90;
  5067. }
  5068. return 'translate(' + labelsArc[i].centroid(d) + ') rotate(' + rotateAngle + ')';
  5069. } else {
  5070. d.outerRadius = radius + 10; // Set Outer Coordinate
  5071. d.innerRadius = radius + 15; // Set Inner Coordinate
  5072. /*
  5073. Overlapping pie labels are not good. What this attempts to do is, prevent overlapping.
  5074. Each label location is hashed, and if a hash collision occurs, we assume an overlap.
  5075. Adjust the label's y-position to remove the overlap.
  5076. */
  5077. var center = labelsArc[i].centroid(d);
  5078. var percent = getSlicePercentage(d);
  5079. if (d.value && percent >= labelThreshold) {
  5080. var hashKey = createHashKey(center);
  5081. if (labelLocationHash[hashKey]) {
  5082. center[1] -= avgHeight;
  5083. }
  5084. labelLocationHash[createHashKey(center)] = true;
  5085. }
  5086. return 'translate(' + center + ')'
  5087. }
  5088. });
  5089. pieLabels.select(".nv-label text")
  5090. .style('text-anchor', function(d,i) {
  5091. //center the text on it's origin or begin/end if orthogonal aligned
  5092. return labelSunbeamLayout ? ((d.startAngle + d.endAngle) / 2 < Math.PI ? 'start' : 'end') : 'middle';
  5093. })
  5094. .text(function(d, i) {
  5095. var percent = getSlicePercentage(d);
  5096. var label = '';
  5097. if (!d.value || percent < labelThreshold) return '';
  5098. if(typeof labelType === 'function') {
  5099. label = labelType(d, i, {
  5100. 'key': getX(d.data),
  5101. 'value': getY(d.data),
  5102. 'percent': valueFormat(percent)
  5103. });
  5104. } else {
  5105. switch (labelType) {
  5106. case 'key':
  5107. label = getX(d.data);
  5108. break;
  5109. case 'value':
  5110. label = valueFormat(getY(d.data));
  5111. break;
  5112. case 'percent':
  5113. label = d3.format('%')(percent);
  5114. break;
  5115. }
  5116. }
  5117. return label;
  5118. })
  5119. ;
  5120. }
  5121. // Computes the angle of an arc, converting from radians to degrees.
  5122. function angle(d) {
  5123. var a = (d.startAngle + d.endAngle) * 90 / Math.PI - 90;
  5124. return a > 90 ? a - 180 : a;
  5125. }
  5126. function arcTween(a, idx) {
  5127. a.endAngle = isNaN(a.endAngle) ? 0 : a.endAngle;
  5128. a.startAngle = isNaN(a.startAngle) ? 0 : a.startAngle;
  5129. if (!donut) a.innerRadius = 0;
  5130. var i = d3.interpolate(this._current, a);
  5131. this._current = i(0);
  5132. return function (t) {
  5133. return arcs[idx](i(t));
  5134. };
  5135. }
  5136. });
  5137. renderWatch.renderEnd('pie immediate');
  5138. return chart;
  5139. }
  5140. //============================================================
  5141. // Expose Public Variables
  5142. //------------------------------------------------------------
  5143. chart.dispatch = dispatch;
  5144. chart.options = nv.utils.optionsFunc.bind(chart);
  5145. chart._options = Object.create({}, {
  5146. // simple options, just get/set the necessary values
  5147. arcsRadius: { get: function () { return arcsRadius; }, set: function (_) { arcsRadius = _; } },
  5148. width: {get: function(){return width;}, set: function(_){width=_;}},
  5149. height: {get: function(){return height;}, set: function(_){height=_;}},
  5150. showLabels: {get: function(){return showLabels;}, set: function(_){showLabels=_;}},
  5151. title: {get: function(){return title;}, set: function(_){title=_;}},
  5152. titleOffset: {get: function(){return titleOffset;}, set: function(_){titleOffset=_;}},
  5153. labelThreshold: {get: function(){return labelThreshold;}, set: function(_){labelThreshold=_;}},
  5154. valueFormat: {get: function(){return valueFormat;}, set: function(_){valueFormat=_;}},
  5155. x: {get: function(){return getX;}, set: function(_){getX=_;}},
  5156. id: {get: function(){return id;}, set: function(_){id=_;}},
  5157. endAngle: {get: function(){return endAngle;}, set: function(_){endAngle=_;}},
  5158. startAngle: {get: function(){return startAngle;}, set: function(_){startAngle=_;}},
  5159. padAngle: {get: function(){return padAngle;}, set: function(_){padAngle=_;}},
  5160. cornerRadius: {get: function(){return cornerRadius;}, set: function(_){cornerRadius=_;}},
  5161. donutRatio: {get: function(){return donutRatio;}, set: function(_){donutRatio=_;}},
  5162. labelsOutside: {get: function(){return labelsOutside;}, set: function(_){labelsOutside=_;}},
  5163. labelSunbeamLayout: {get: function(){return labelSunbeamLayout;}, set: function(_){labelSunbeamLayout=_;}},
  5164. donut: {get: function(){return donut;}, set: function(_){donut=_;}},
  5165. growOnHover: {get: function(){return growOnHover;}, set: function(_){growOnHover=_;}},
  5166. // depreciated after 1.7.1
  5167. pieLabelsOutside: {get: function(){return labelsOutside;}, set: function(_){
  5168. labelsOutside=_;
  5169. nv.deprecated('pieLabelsOutside', 'use labelsOutside instead');
  5170. }},
  5171. // depreciated after 1.7.1
  5172. donutLabelsOutside: {get: function(){return labelsOutside;}, set: function(_){
  5173. labelsOutside=_;
  5174. nv.deprecated('donutLabelsOutside', 'use labelsOutside instead');
  5175. }},
  5176. // deprecated after 1.7.1
  5177. labelFormat: {get: function(){ return valueFormat;}, set: function(_) {
  5178. valueFormat=_;
  5179. nv.deprecated('labelFormat','use valueFormat instead');
  5180. }},
  5181. // options that require extra logic in the setter
  5182. margin: {get: function(){return margin;}, set: function(_){
  5183. margin.top = typeof _.top != 'undefined' ? _.top : margin.top;
  5184. margin.right = typeof _.right != 'undefined' ? _.right : margin.right;
  5185. margin.bottom = typeof _.bottom != 'undefined' ? _.bottom : margin.bottom;
  5186. margin.left = typeof _.left != 'undefined' ? _.left : margin.left;
  5187. }},
  5188. y: {get: function(){return getY;}, set: function(_){
  5189. getY=d3.functor(_);
  5190. }},
  5191. color: {get: function(){return color;}, set: function(_){
  5192. color=nv.utils.getColor(_);
  5193. }},
  5194. labelType: {get: function(){return labelType;}, set: function(_){
  5195. labelType= _ || 'key';
  5196. }}
  5197. });
  5198. nv.utils.initOptions(chart);
  5199. return chart;
  5200. };
  5201. nv.version = "1.8.2-dev";
  5202. })();