1 /**************************************************************************
2 * AngularJS-nvD3, v1.0.7; MIT
3 * http://krispo.github.io/angular-nvd3
4 **************************************************************************/
9 angular.module('nvd3', [])
11 .directive('nvd3', ['nvd3Utils', function(nvd3Utils){
15 data: '=', //chart data, [required]
16 options: '=', //chart options, according to nvd3 core api, [required]
17 api: '=?', //directive global api, [optional]
18 events: '=?', //global events that directive would subscribe to, [optional]
19 config: '=?', //global directive configuration, [optional]
20 onReady: '&?' //callback function that is called with internal scope when directive is created [optional]
22 link: function(scope, element, attrs){
27 refreshDataOnly: true,
28 deepWatchOptions: true,
30 deepWatchDataDepth: 2, // 0 - by reference (cheap), 1 - by collection item (the middle), 2 - by value (expensive)
31 debounce: 10, // default 10ms, time silence to prevent refresh while multiple options changes at a time
32 debounceImmediate: true // immediate flag for debounce function
35 //flag indicates if directive and chart is ready
36 scope.isReady = false;
38 //basic directive configuration
39 scope._config = angular.extend(defaultConfig, scope.config);
41 //directive global api
43 // Fully refresh directive
45 scope.api.updateWithOptions(scope.options);
49 // Fully refresh directive with specified timeout
50 refreshWithTimeout: function(t){
51 setTimeout(function(){
56 // Update chart layout (for example if container is resized)
58 if (scope.chart && scope.svg) {
59 scope.svg.datum(scope.data).call(scope.chart);
60 // scope.chart.update();
66 // Update chart layout with specified timeout
67 updateWithTimeout: function(t){
68 setTimeout(function(){
73 // Update chart with new options
74 updateWithOptions: function(options){
76 scope.api.clearElement();
78 // Exit if options are not yet bound
79 if (angular.isDefined(options) === false) return;
81 // Exit if chart is hidden
82 if (!scope._config.visible) return;
84 // Initialize chart with specific type
85 scope.chart = nv.models[options.chart.type]();
87 // Generate random chart ID
88 scope.chart.id = Math.random().toString(36).substr(2, 15);
90 angular.forEach(scope.chart, function(value, key){
102 ].indexOf(key) >= 0);
104 else if (key === 'dispatch') {
105 if (options.chart[key] === undefined || options.chart[key] === null) {
106 if (scope._config.extended) options.chart[key] = {};
108 configureEvents(scope.chart[key], options.chart[key]);
145 ].indexOf(key) >= 0 ||
146 // stacked is a component for stackedAreaChart, but a boolean for multiBarChart and multiBarHorizontalChart
147 (key === 'stacked' && options.chart.type === 'stackedAreaChart')) {
148 if (options.chart[key] === undefined || options.chart[key] === null) {
149 if (scope._config.extended) options.chart[key] = {};
151 configure(scope.chart[key], options.chart[key], options.chart.type);
154 //TODO: need to fix bug in nvd3
155 else if ((key === 'focusHeight') && options.chart.type === 'lineChart');
156 else if ((key === 'focusHeight') && options.chart.type === 'lineWithFocusChart');
157 else if ((key === 'xTickFormat' || key === 'yTickFormat') && options.chart.type === 'lineWithFocusChart');
158 else if ((key === 'tooltips') && options.chart.type === 'boxPlotChart');
159 else if ((key === 'tooltipXContent' || key === 'tooltipYContent') && options.chart.type === 'scatterChart');
160 else if ((key === 'x' || key === 'y') && options.chart.type === 'forceDirectedGraph');
162 else if (options.chart[key] === undefined || options.chart[key] === null){
163 if (scope._config.extended) {
164 if (key==='barColor')
165 options.chart[key] = value()();
167 options.chart[key] = value();
171 else scope.chart[key](options.chart[key]);
175 if (options.chart.type === 'sunburstChart') {
176 scope.api.updateWithData(angular.copy(scope.data));
178 scope.api.updateWithData(scope.data);
181 // Configure wrappers
182 if (options['title'] || scope._config.extended) configureWrapper('title');
183 if (options['subtitle'] || scope._config.extended) configureWrapper('subtitle');
184 if (options['caption'] || scope._config.extended) configureWrapper('caption');
188 if (options['styles'] || scope._config.extended) configureStyles();
190 nv.addGraph(function() {
191 if (!scope.chart) return;
193 // Remove resize handler. Due to async execution should be placed here, not in the clearElement
194 if (scope.chart.resizeHandler) scope.chart.resizeHandler.clear();
196 // Update the chart when window resizes
197 scope.chart.resizeHandler = nv.utils.windowResize(function() {
198 scope.chart && scope.chart.update && scope.chart.update();
202 if (options.chart.zoom !== undefined && [
205 'candlestickBarChart',
206 'cumulativeLineChart',
207 'historicalBarChart',
210 ].indexOf(options.chart.type) > -1) {
211 nvd3Utils.zoom(scope, options);
215 }, options.chart['callback']);
218 // Update chart with new data
219 updateWithData: function (data){
221 // remove whole svg element with old data
222 d3.select(element[0]).select('svg').remove();
226 // Select the current element to add <svg> element and to render the chart in
227 scope.svg = d3.select(element[0]).append('svg');
228 if (h = scope.options.chart.height) {
229 if (!isNaN(+h)) h += 'px'; //check if height is number
230 scope.svg.attr('height', h).style({height: h});
232 if (w = scope.options.chart.width) {
233 if (!isNaN(+w)) w += 'px'; //check if width is number
234 scope.svg.attr('width', w).style({width: w});
236 scope.svg.attr('width', '100%').style({width: '100%'});
239 scope.svg.datum(data).call(scope.chart);
243 // Fully clear directive element
244 clearElement: function (){
245 element.find('.title').remove();
246 element.find('.subtitle').remove();
247 element.find('.caption').remove();
250 // remove tooltip if exists
251 if (scope.chart && scope.chart.tooltip && scope.chart.tooltip.id) {
252 d3.select('#' + scope.chart.tooltip.id()).remove();
255 // To be compatible with old nvd3 (v1.7.1)
256 if (nv.graphs && scope.chart) {
257 for (var i = nv.graphs.length - 1; i >= 0; i--) {
258 if (nv.graphs[i] && (nv.graphs[i].id === scope.chart.id)) {
259 nv.graphs.splice(i, 1);
263 if (nv.tooltip && nv.tooltip.cleanup) {
264 nv.tooltip.cleanup();
266 if (scope.chart && scope.chart.resizeHandler) scope.chart.resizeHandler.clear();
270 // Get full directive scope
271 getScope: function(){ return scope; },
273 // Get directive element
274 getElement: function(){ return element; }
277 // Configure the chart model with the passed options
278 function configure(chart, options, chartType){
279 if (chart && options){
280 angular.forEach(chart, function(value, key){
282 else if (key === 'dispatch') {
283 if (options[key] === undefined || options[key] === null) {
284 if (scope._config.extended) options[key] = {};
286 configureEvents(value, options[key]);
288 else if (key === 'tooltip') {
289 if (options[key] === undefined || options[key] === null) {
290 if (scope._config.extended) options[key] = {};
292 configure(chart[key], options[key], chartType);
294 else if (key === 'contentGenerator') {
295 if (options[key]) chart[key](options[key]);
302 'nvPointerEventsClass',
310 ].indexOf(key) === -1) {
311 if (options[key] === undefined || options[key] === null){
312 if (scope._config.extended) options[key] = value();
314 else chart[key](options[key]);
320 // Subscribe to the chart events (contained in 'dispatch')
321 // and pass eventHandler functions in the 'options' parameter
322 function configureEvents(dispatch, options){
323 if (dispatch && options){
324 angular.forEach(dispatch, function(value, key){
325 if (options[key] === undefined || options[key] === null){
326 if (scope._config.extended) options[key] = value.on;
328 else dispatch.on(key + '._', options[key]);
333 // Configure 'title', 'subtitle', 'caption'.
334 // nvd3 has no sufficient models for it yet.
335 function configureWrapper(name){
336 var _ = nvd3Utils.deepExtend(defaultWrapper(name), scope.options[name] || {});
338 if (scope._config.extended) scope.options[name] = _;
340 var wrapElement = angular.element('<div></div>').html(_['html'] || '')
341 .addClass(name).addClass(_.className)
345 if (!_['html']) wrapElement.text(_.text);
348 if (name === 'title') element.prepend(wrapElement);
349 else if (name === 'subtitle') angular.element(element[0].querySelector('.title')).after(wrapElement);
350 else if (name === 'caption') element.append(wrapElement);
354 // Add some styles to the whole directive element
355 function configureStyles(){
356 var _ = nvd3Utils.deepExtend(defaultStyles(), scope.options['styles'] || {});
358 if (scope._config.extended) scope.options['styles'] = _;
360 angular.forEach(_.classes, function(value, key){
361 value ? element.addClass(key) : element.removeClass(key);
364 element.removeAttr('style').css(_.css);
367 // Default values for 'title', 'subtitle', 'caption'
368 function defaultWrapper(_){
370 case 'title': return {
372 text: 'Write Your Title',
375 width: scope.options.chart.width + 'px',
379 case 'subtitle': return {
381 text: 'Write Your Subtitle',
383 width: scope.options.chart.width + 'px',
387 case 'caption': return {
389 text: 'Figure 1. Write Your Caption text.',
391 width: scope.options.chart.width + 'px',
398 // Default values for styles
399 function defaultStyles(){
402 'with-3d-shadow': true,
403 'with-transitions': true,
411 // Watching on options changing
412 if (scope._config.deepWatchOptions) {
413 scope.$watch('options', nvd3Utils.debounce(function(newOptions){
414 if (!scope._config.disabled) scope.api.refresh();
415 }, scope._config.debounce, scope._config.debounceImmediate), true);
418 // Watching on data changing
419 function dataWatchFn(newData, oldData) {
420 if (newData !== oldData){
421 if (!scope._config.disabled) {
422 scope._config.refreshDataOnly ? scope.api.update() : scope.api.refresh(); // if wanted to refresh data only, use update method, otherwise use full refresh.
426 if (scope._config.deepWatchData) {
427 if (scope._config.deepWatchDataDepth === 1) {
428 scope.$watchCollection('data', dataWatchFn);
430 scope.$watch('data', dataWatchFn, scope._config.deepWatchDataDepth === 2);
434 // Watching on config changing
435 scope.$watch('config', function(newConfig, oldConfig){
436 if (newConfig !== oldConfig){
437 scope._config = angular.extend(defaultConfig, newConfig);
442 // Refresh chart first time if deepWatchOptions and deepWatchData are false
443 if (!scope._config.deepWatchOptions && !scope._config.deepWatchData) {
447 //subscribe on global events
448 angular.forEach(scope.events, function(eventHandler, event){
449 scope.$on(event, function(e, args){
450 return eventHandler(e, scope, args);
454 // remove completely when directive is destroyed
455 element.on('$destroy', function () {
456 scope.api.clearElement();
459 // trigger onReady callback if directive is ready
460 scope.$watch('isReady', function(isReady){
462 if (scope.onReady && typeof scope.onReady() === 'function') scope.onReady()(scope, element);
469 .factory('nvd3Utils', function(){
471 debounce: function(func, wait, immediate) {
474 var context = this, args = arguments;
475 var later = function() {
477 if (!immediate) func.apply(context, args);
479 var callNow = immediate && !timeout;
480 clearTimeout(timeout);
481 timeout = setTimeout(later, wait);
482 if (callNow) func.apply(context, args);
485 deepExtend: function(dst){
487 angular.forEach(arguments, function(obj) {
489 angular.forEach(obj, function(value, key) {
490 if (dst[key] && dst[key].constructor && dst[key].constructor === Object) {
491 me.deepExtend(dst[key], value);
500 zoom: function(scope, options) {
501 var zoom = options.chart.zoom;
503 // check if zoom enabled
504 var enabled = (typeof zoom.enabled === 'undefined' || zoom.enabled === null) ? true : zoom.enabled;
505 if (!enabled) return;
507 var xScale = scope.chart.xAxis.scale()
508 , yScale = scope.chart.yAxis.scale()
509 , xDomain = scope.chart.xDomain || xScale.domain
510 , yDomain = scope.chart.yDomain || yScale.domain
511 , x_boundary = xScale.domain().slice()
512 , y_boundary = yScale.domain().slice()
514 // initialize zoom options
515 , scale = zoom.scale || 1
516 , translate = zoom.translate || [0, 0]
517 , scaleExtent = zoom.scaleExtent || [1, 10]
518 , useFixedDomain = zoom.useFixedDomain || false
519 , useNiceScale = zoom.useNiceScale || false
520 , horizontalOff = zoom.horizontalOff || false
521 , verticalOff = zoom.verticalOff || false
522 , unzoomEventType = zoom.unzoomEventType || 'dblclick.zoom'
524 // auxiliary functions
539 fixDomain = function (domain, boundary) {
540 domain[0] = Math.min(Math.max(domain[0], boundary[0]), boundary[1] - boundary[1] / scaleExtent[1]);
541 domain[1] = Math.max(boundary[0] + boundary[1] / scaleExtent[1], Math.min(domain[1], boundary[1]));
545 // zoom event handler
546 zoomed = function () {
547 if (zoom.zoomed !== undefined) {
548 var domains = zoom.zoomed(xScale.domain(), yScale.domain());
549 if (!horizontalOff) xDomain([domains.x1, domains.x2]);
550 if (!verticalOff) yDomain([domains.y1, domains.y2]);
552 if (!horizontalOff) xDomain(useFixedDomain ? fixDomain(xScale.domain(), x_boundary) : xScale.domain());
553 if (!verticalOff) yDomain(useFixedDomain ? fixDomain(yScale.domain(), y_boundary) : yScale.domain());
555 scope.chart.update();
558 // unzoomed event handler
559 unzoomed = function () {
560 if (zoom.unzoomed !== undefined) {
561 var domains = zoom.unzoomed(xScale.domain(), yScale.domain());
562 if (!horizontalOff) xDomain([domains.x1, domains.x2]);
563 if (!verticalOff) yDomain([domains.y1, domains.y2]);
565 if (!horizontalOff) xDomain(x_boundary);
566 if (!verticalOff) yDomain(y_boundary);
568 d3zoom.scale(scale).translate(translate);
569 scope.chart.update();
572 // zoomend event handler
573 zoomend = function () {
574 if (zoom.zoomend !== undefined) {
579 // create d3 zoom handler
580 d3zoom = d3.behavior.zoom()
583 .scaleExtent(scaleExtent)
585 .on('zoomend', zoomend);
587 scope.svg.call(d3zoom);
589 d3zoom.scale(scale).translate(translate).event(scope.svg);
591 if (unzoomEventType !== 'none') scope.svg.on(unzoomEventType, unzoomed);