Utilizando gráficos de C3.js con directivas de AngularJS

A la hora de realizar visualización de datos con JavaScript, D3.js es una opción eficiente y sumamente expresiva. Los ejemplos de la página oficial son prueba de las amplísimas posibilidades que otorga al usuario. Pero, para algunas necesidades, el nivel de abstracción de D3 puede resultar demasiado bajo. Este gráfico de líneas, por ejemplo, si bien permite configurar hasta el menor detalle, también exige especificar cada parte del mismo (sin ir más lejos, hay que determinar la posición de ambos ejes del gráfico).

C3.js viene a suplir esta necesidad. Esta librería permite crear gráficos comúnmente usados (de líneas, de barras, de torta, etc.) que están basados en D3 pero que requieren mucha menos configuración por parte del usuario. El gráfico de líneas que en D3 ocupaba alrededor de sesenta líneas de código puede hacerse en C3 con unas diez (acá hay un ejemplo).

Un escenario posible es querer agregar un gráfico de C3 a un proyecto construído con AngularJS. Si bien hay muchas formas de hacerlo, vamos a concentrarnos en cómo lograrlo con las directivas de Angular.

Una primera aproximación es:

app.controller("myController", function($scope) {
    $scope.myData =[
        ["f1", 4, 8, 15, 16, 23, 42],
        ["f2", 3, 1, 4, 1, 5, 9, 2, 6, 5]
    ];
});
app.directive("c3Graph", function() {
    var linkFunction = function(scope) {
        c3.generate({
            bindto: '#chart',
            data: {
                columns: scope.data
            }
        });
    };

    return {
        link: linkFunction,
        scope: {
            data: '='
        },
        template: '<div id="chart"></div>'
    };
});

Lo cual podemos usar en el código HTML de la siguiente manera:

<c3-graph data="myData"></c3-graph>

Esto produce el siguiente gráfico (click para ampliar):

chart1

Sin embargo, esto tiene un problema. Si se usa la directiva dos o más veces, el DOM contendrá elementos con el mismo identificador (#chart), por lo que sólo uno de los gráficos funcionará. Esto está lejos de ser lo ideal.

Una posible solución a este problema es indicar en los atributos de la directiva el identificador del div en el que se quiere crear el gráfico:

app.controller("myController", function($scope) {
    $scope.myData = [
        ["f1", 4, 8, 15, 16, 23, 42],
        ["f2", 3, 1, 4, 1, 5, 9, 2, 6, 5]
    ];
    $scope.anotherData = [
        ["g", 2, 7, 1, 8, 2, 8, 1, 8, 2]
    ];
});
app.directive("c3Graph", function() {
    var linkFunction = function(scope) {
        c3.generate({
            bindto: '#' + scope.bindToId,
            data: {
                columns: scope.data
            }
        });
    };

    return {
        link: linkFunction,
        scope: {
            bindToId: '@',
            data: '='
        }
    };
});

Para después hacer:

<c3-graph data="myData" bind-to-id="chart1">
    <div id="chart1"></div>
</c3-graph>
<c3-graph data="anotherData" bind-to-id="chart2">
    <div id="chart2"></div>
</c3-graph>

Produciendo así los siguientes gráficos:

chart2

De esta forma se puede utilizar la directiva cuantas veces se desee, aunque a costa de que su uso sea un poco más verborrágico.

Con esto alcanza para empezar, pero hay otras cosas importantes a tener en cuenta. Quizá la más importante, siendo que el framework utilizado es Angular, es que el gráfico se actualice cuando los datos cambian. Pero éste se genera una vez y C3 no lo modifica por sí mismo en lo sucesivo. Para lograrlo, se puede registrar un listener para los datos en la función de link de la directiva:

var linkFunction = function(scope) {
    var graph = c3.generate({
        bindto: '#' + scope.bindToId,
        data: {
            columns: scope.data
        }
    });

    scope.$watch('data', function(newData) {
        graph.load({
            columns: newData
        });
    }, true);
};

De esta forma, cuando los datos asociados al gráfico cambien, el mismo se actualizará.

Dependiendo del caso, esto puede conllevar problemas de eficiencia, por lo que esta solución puede no ser adecuada. Una alternativa es utilizar eventos ($scope.$on y $scope.$broadcast) para determinar cuándo debe actualizarse un gráfico.

Acá se puede ver un ejemplo completo y éste es el código del mismo.

Get in Touch