(function(root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['angular', 'dc', 'lodash', 'd3'], function(angular, dc, _, d3) { return (root.returnExportsGlobal = factory(angular, dc, _, d3)); }); } else if (typeof exports === 'object') { // Node. Does not work with strict CommonJS, but // only CommonJS-like enviroments that support module.exports, // like Node. module.exports = factory(); } else { root['angularDc'] = factory(root.angular, root.dc, root._, root.d3); } }(this, function(angular, dc, _, d3) { 'use strict'; var angularDc = angular.module('angularDc', []); /* The main directive in angularDc, responsible creating Dc.js charts. The goal of this directive is to provide a AngularJs interface to the existing features of Dc.js. */ angularDc.directive('dcChart', [ '$timeout', function($timeout) { /* Whitelisted options to be read from a chart's html attributes. */ var directiveOptions = [ 'name', 'onRenderlet', 'onFiltered', 'onPostRedraw', 'onPostRender', 'onPreRedraw', 'onPreRender', 'onZoomed', 'postSetupChart' ]; /* Called during the directive's linking phase, this function creates a Dc.js chart. The chart is configured based on settings read from the $scope and the html element. */ function setupChart(scope, iElement, iAttrs, options) { // Get the element this directive blongs to, the root of chart var chartElement = iElement[0], // Get the chart type to create // Rather than creating a directive for each type of chart // we take it as our main parameter, and use that to call the correct Dc.js // chart constructor chartType = iAttrs.dcChart, // Get the Dc.js 'Chart Group', if any, for this chart. // Charts within a group are tied together chartGroupName = iAttrs.dcChartGroup || undefined; // Get the chart creation function for the chartType var chartFactory = dc[chartType]; // Create an unconfigured instance of the chart var chart = chartFactory(chartElement, chartGroupName); // Get the potential set of options for this chart // Used for mapping chartElement's html attributes to chart options var validOptions = getValidOptionsForChart(chart); // Get additional options from chartElement's html attributes. // All options are prepended with 'dc-'' to avoid clashing with html own meaning (e.g width) // All options are parsed in angular's $parse language, so beware, it is not javascript! options = getOptionsFromAttrs(scope, iAttrs, validOptions); // we may have a dc-options attribute which contain a javascript object for stuff // not writtable in $parse language if ('options' in options) { options = _.merge(options, options.options); options.options = undefined; } // If we have a dc-name attribute, we populate the scope with the chart // object dc-name if ('name' in options) { scope[options.name] = chart; options.name = undefined; } // Configure the chart based on options chart.options(options); // Get event handlers, if any, from options var eventHandlers = _({ 'renderlet': options.onRenderlet, 'preRender': options.onPreRender, 'postRender': options.onPostRender, 'preRedraw': options.onPreRedraw, 'postRedraw': options.onPostRedraw, 'filtered': options.onFiltered, 'zoomed': options.onZoomed }).omit(_.isUndefined); // Register the eventHandlers with the chart (Dc.js) eventHandlers.each(function(handler, evt) { chart.on(evt, handler); }); // Run the postSetupChart callback, if provided if (_.isFunction(options.postSetupChart)) { options.postSetupChart(chart, options); } return chart; } function getValidOptionsForChart(chart) { // all chart options are exposed via a function return _(chart).functions().extend(directiveOptions).map(function(s) { return 'dc' + s.charAt(0).toUpperCase() + s.substring(1); }).value(); } function getOptionsFromAttrs(scope, iAttrs, validOptions) { return _(iAttrs.$attr).keys().intersection(validOptions).map(function(key) { var value = scope.$eval(iAttrs[key]); // remove the dc- prefix if any if (key.substring(0, 2) === 'dc') { key = key.charAt(2).toLowerCase() + key.substring(3); } return [ key, value ]; }).zipObject().value(); } return { restrict: 'A', link: function(scope, iElement, iAttrs) { var printExceptions = false; // add dc, d3 and commonly used Date method to the scope to allow snippets to be configured in // the templates scope.dc = dc; scope.d3 = d3; scope.DateTime = function(a, b, c, d, e, f) { return new Date(a, b, c, d, e, f); }; scope.Date = function(a, b, c) { return new Date(a, b, c); }; // watch for the scope to settle until all the attributes are defined var unwatch = scope.$watch(function() { var options = _(iAttrs.$attr).keys().filter(function(s) { return s.substring(0, 2) === 'dc' && s !== 'dcChart' && s !== 'dcChartGroup'; }).map(function(key) { try { // We ignore exception waiting for the data to be potentially loaded // by the controller var r = scope.$eval(iAttrs[key]); if (_.isUndefined(r)) { throw Error(iAttrs[key] + ' is undefined'); } return r; } catch (e) { if (printExceptions) { console.log('unable to eval' + key + ':' + iAttrs[key]); throw e; } return undefined; } }); if (options.any(_.isUndefined)) { // return undefined if there is at least one undefined option // so that the $watch dont call us again at this $digest time return undefined; } return options.value(); }, function(options) { if (!_.isUndefined(options)) { // Stop the $watch, as we now created the charts unwatch(); var chart = setupChart(scope, iElement, iAttrs); // populate the .reset childrens with necessary reset callbacks var a = angular.element(iElement[0].querySelector('a.reset')); a.on('click', function() { chart.filterAll(); dc.redrawAll(); }); a.attr('href', 'javascript:;'); a.css('display', 'none'); // watching the attributes is costly, so we stop after first rendering chart.render(); } }); // if after 4 second we still get exceptions, we should raise them // to help debugging. $timeout will trigger another round of check. $timeout(function() { printExceptions = true; }, 2000); } }; } ]); /* Another directive, responsible of integration angular's select directive with dc. This directive helps to filter by arbitrary dimensions without need for another graph. Note that if there is a graph on the same dimension as the select, changing the select value will not update the graph's own selection. This is also the case if you make 2 graphs with same dimension. This is a limitation of the underlying lib dc.js */ angularDc.directive('dcSelect', [ function() { return { restrict: 'E', scope: { dcDimension: '=', allLabel: '@' }, template: '