blob: 40a0d761324cac9bd0ce2e53801f82200985f9a7 [file] [log] [blame]
/**************************************************************************
* AngularJS-nvD3, v0.1.1; MIT License; 24/02/2015 19:26
* http://krispo.github.io/angular-nvd3
**************************************************************************/
(function(){
'use strict';
angular.module('nvd3', [])
.directive('nvd3', ['utils', function(utils){
return {
restrict: 'AE',
scope: {
data: '=', //chart data, [required]
options: '=', //chart options, according to nvd3 core api, [required]
api: '=?', //directive global api, [optional]
events: '=?', //global events that directive would subscribe to, [optional]
config: '=?' //global directive configuration, [optional]
},
link: function(scope, element, attrs){
var defaultConfig = {
extended: false,
visible: true,
disabled: false,
autorefresh: true,
refreshDataOnly: false,
deepWatchData: true,
debounce: 10 // default 10ms, time silence to prevent refresh while multiple options changes at a time
};
//basic directive configuration
scope._config = angular.extend(defaultConfig, scope.config);
//directive global api
scope.api = {
// Fully refresh directive
refresh: function(){
scope.api.updateWithOptions(scope.options);
},
// Update chart layout (for example if container is resized)
update: function() {
scope.chart.update();
},
// Update chart with new options
updateWithOptions: function(options){
// Clearing
scope.api.clearElement();
// Exit if options are not yet bound
if (angular.isDefined(options) === false) return;
// Exit if chart is hidden
if (!scope._config.visible) return;
// Initialize chart with specific type
scope.chart = nv.models[options.chart.type]();
// Generate random chart ID
scope.chart.id = Math.random().toString(36).substr(2, 15);
angular.forEach(scope.chart, function(value, key){
if (key === 'options' || key === 'id' || key === 'resizeHandler');
else if (key === 'dispatch') {
if (options.chart[key] === undefined || options.chart[key] === null) {
if (scope._config.extended) options.chart[key] = {};
}
configureEvents(scope.chart[key], options.chart[key]);
}
else if ([
'lines',
'lines1',
'lines2',
'bars', // TODO: Fix bug in nvd3, nv.models.historicalBar - chart.interactive (false -> _)
'bars1',
'bars2',
'stack1',
'stack2',
'multibar',
'discretebar',
'pie',
'scatter',
'bullet',
'sparkline',
'legend',
'distX',
'distY',
'xAxis',
'x2Axis',
'yAxis',
'yAxis1',
'yAxis2',
'y1Axis',
'y2Axis',
'y3Axis',
'y4Axis',
'interactiveLayer',
'controls'
].indexOf(key) >= 0){
if (options.chart[key] === undefined || options.chart[key] === null) {
if (scope._config.extended) options.chart[key] = {};
}
configure(scope.chart[key], options.chart[key], options.chart.type);
}
else if (//TODO: need to fix bug in nvd3
(key ==='clipEdge' && options.chart.type === 'multiBarHorizontalChart')
|| (key === 'clipVoronoi' && options.chart.type === 'historicalBarChart')
|| (key === 'color' && options.chart.type === 'indentedTreeChart')
|| (key === 'defined' && (options.chart.type === 'historicalBarChart' || options.chart.type === 'cumulativeLineChart' || options.chart.type === 'lineWithFisheyeChart'))
|| (key === 'forceX' && (options.chart.type === 'multiBarChart' || options.chart.type === 'discreteBarChart' || options.chart.type === 'multiBarHorizontalChart'))
|| (key === 'interpolate' && options.chart.type === 'historicalBarChart')
|| (key === 'isArea' && options.chart.type === 'historicalBarChart')
|| (key === 'size' && options.chart.type === 'historicalBarChart')
|| (key === 'stacked' && options.chart.type === 'stackedAreaChart')
|| (key === 'values' && options.chart.type === 'pieChart')
|| (key === 'xScale' && options.chart.type === 'scatterChart')
|| (key === 'yScale' && options.chart.type === 'scatterChart')
|| (key === 'x' && (options.chart.type === 'lineWithFocusChart' || options.chart.type === 'multiChart'))
|| (key === 'y' && (options.chart.type === 'lineWithFocusChart' || options.chart.type === 'multiChart'))
);
else if (options.chart[key] === undefined || options.chart[key] === null){
if (scope._config.extended) options.chart[key] = value();
}
else scope.chart[key](options.chart[key]);
});
// Update with data
scope.api.updateWithData(scope.data);
// Configure wrappers
if (options['title'] || scope._config.extended) configureWrapper('title');
if (options['subtitle'] || scope._config.extended) configureWrapper('subtitle');
if (options['caption'] || scope._config.extended) configureWrapper('caption');
// Configure styles
if (options['styles'] || scope._config.extended) configureStyles();
nv.addGraph(function() {
// Update the chart when window resizes
scope.chart.resizeHandler = utils.windowResize(function() { scope.chart.update(); });
return scope.chart;
}, options.chart['callback']);
},
// Update chart with new data
updateWithData: function (data){
if (data) {
scope.options.chart['transitionDuration'] = +scope.options.chart['transitionDuration'] || 250;
// remove whole svg element with old data
d3.select(element[0]).select('svg').remove();
// Select the current element to add <svg> element and to render the chart in
d3.select(element[0]).append('svg')
.attr('height', scope.options.chart.height)
.attr('width', scope.options.chart.width)
.datum(data)
.transition().duration(scope.options.chart['transitionDuration'])
.call(scope.chart);
// Set up svg height and width. It is important for all browsers...
d3.select(element[0]).select('svg')[0][0].style.height = scope.options.chart.height + 'px';
d3.select(element[0]).select('svg')[0][0].style.width = scope.options.chart.width + 'px';
if (scope.options.chart.type === 'multiChart') scope.chart.update(); // multiChart is not automatically updated
}
},
// Fully clear directive element
clearElement: function (){
element.find('.title').remove();
element.find('.subtitle').remove();
element.find('.caption').remove();
element.empty();
if (scope.chart) {
// clear window resize event handler
if (scope.chart.resizeHandler) scope.chart.resizeHandler.clear();
// remove chart from nv.graph list
for (var i = 0; i < nv.graphs.length; i++)
if (nv.graphs[i].id === scope.chart.id) {
nv.graphs.splice(i, 1);
}
}
scope.chart = null;
nv.tooltip.cleanup();
},
// Get full directive scope
getScope: function(){ return scope; }
};
// Configure the chart model with the passed options
function configure(chart, options, chartType){
if (chart && options){
angular.forEach(chart, function(value, key){
if (key === 'dispatch') {
if (options[key] === undefined || options[key] === null) {
if (scope._config.extended) options[key] = {};
}
configureEvents(value, options[key]);
}
else if (//TODO: need to fix bug in nvd3
(key === 'xScale' && chartType === 'scatterChart')
|| (key === 'yScale' && chartType === 'scatterChart')
|| (key === 'values' && chartType === 'pieChart'));
else if ([
'scatter',
'defined',
'options',
'axis',
'rangeBand',
'rangeBands'
].indexOf(key) < 0){
if (options[key] === undefined || options[key] === null){
if (scope._config.extended) options[key] = value();
}
else chart[key](options[key]);
}
});
}
}
// Subscribe to the chart events (contained in 'dispatch')
// and pass eventHandler functions in the 'options' parameter
function configureEvents(dispatch, options){
if (dispatch && options){
angular.forEach(dispatch, function(value, key){
if (options[key] === undefined || options[key] === null){
if (scope._config.extended) options[key] = value.on;
}
else dispatch.on(key + '._', options[key]);
});
}
}
// Configure 'title', 'subtitle', 'caption'.
// nvd3 has no sufficient models for it yet.
function configureWrapper(name){
var _ = utils.deepExtend(defaultWrapper(name), scope.options[name] || {});
if (scope._config.extended) scope.options[name] = _;
var wrapElement = angular.element('<div></div>').html(_['html'] || '')
.addClass(name).addClass(_.class)
.removeAttr('style')
.css(_.css);
if (!_['html']) wrapElement.text(_.text);
if (_.enable) {
if (name === 'title') element.prepend(wrapElement);
else if (name === 'subtitle') element.find('.title').after(wrapElement);
else if (name === 'caption') element.append(wrapElement);
}
}
// Add some styles to the whole directive element
function configureStyles(){
var _ = utils.deepExtend(defaultStyles(), scope.options['styles'] || {});
if (scope._config.extended) scope.options['styles'] = _;
angular.forEach(_.classes, function(value, key){
value ? element.addClass(key) : element.removeClass(key);
});
element.removeAttr('style').css(_.css);
}
// Default values for 'title', 'subtitle', 'caption'
function defaultWrapper(_){
switch (_){
case 'title': return {
enable: false,
text: 'Write Your Title',
class: 'h4',
css: {
width: scope.options.chart.width + 'px',
textAlign: 'center'
}
};
case 'subtitle': return {
enable: false,
text: 'Write Your Subtitle',
css: {
width: scope.options.chart.width + 'px',
textAlign: 'center'
}
};
case 'caption': return {
enable: false,
text: 'Figure 1. Write Your Caption text.',
css: {
width: scope.options.chart.width + 'px',
textAlign: 'center'
}
};
}
}
// Default values for styles
function defaultStyles(){
return {
classes: {
'with-3d-shadow': true,
'with-transitions': true,
'gallery': false
},
css: {}
};
}
/* Event Handling */
// Watching on options changing
scope.$watch('options', utils.debounce(function(newOptions){
if (!scope._config.disabled && scope._config.autorefresh) scope.api.refresh();
}, scope._config.debounce, true), true);
// Watching on data changing
scope.$watch('data', function(newData, oldData){
if (newData !== oldData && scope.chart){
if (!scope._config.disabled && scope._config.autorefresh) {
scope._config.refreshDataOnly ? scope.chart.update() : scope.api.refresh(); // if wanted to refresh data only, use chart.update method, otherwise use full refresh.
}
}
}, scope._config.deepWatchData);
// Watching on config changing
scope.$watch('config', function(newConfig, oldConfig){
if (newConfig !== oldConfig){
scope._config = angular.extend(defaultConfig, newConfig);
scope.api.refresh();
}
}, true);
//subscribe on global events
angular.forEach(scope.events, function(eventHandler, event){
scope.$on(event, function(e){
return eventHandler(e, scope);
});
});
// remove completely when directive is destroyed
element.on('$destroy', function () {
scope.api.clearElement();
});
}
};
}])
.factory('utils', function(){
return {
debounce: function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
},
windowResize: function(handler) {
if (window.addEventListener) {
window.addEventListener('resize', handler);
}
// return object with clear function to remove the single added callback.
return {
callback: handler,
clear: function() {
window.removeEventListener('resize', handler);
}
};
},
deepExtend: function(dst){
var me = this;
angular.forEach(arguments, function(obj) {
if (obj !== dst) {
angular.forEach(obj, function(value, key) {
if (dst[key] && dst[key].constructor && dst[key].constructor === Object) {
me.deepExtend(dst[key], value);
} else {
dst[key] = value;
}
});
}
});
return dst;
}
};
});
})();