Utilizar Chart.js con Knockout.js

Dentro del caótico vibrante mundo de las librerías para Javascript, existen muchas alternativas para mostrar gráficos en pantalla, pero la que más me gusta, por su equilibro entre facilidad de uso y vistosidad de resultados es chart.js.

Micro introducción a Chart.js

En la completa documentación de chart.js podéis encontrar información de sobra para manejarla con soltura, pero para que os hagáis una idea, para mostrar un gráfico de tarta típico sólo hace falta el siguiente código (podéis jugar con él en jsfiddle):

<canvas id="sales-chart" width="400" height="400"></canvas>
var context = document.getElementById('sales-chart').getContext('2d');
var chart = new Chart(context).Pie([
  { label: 'Red', value: 75, color: '#F7464A' },
  { label: 'Not-Red', value: 23, color: '#4D5360' }
]);

Lo único que necesitamos es tener un elemento canvas (como el que usamos para implementar el juego de la vida en Javascript) y crear un objeto Chart sobre él. Este objeto cuenta con distintos métodos para crear cada tipo de gráfico, y cada método recibe dos objetos, uno obligatorio con los datos a mostrar, y otro opcional con la configuración del gráfico (ejes, leyendas, etc.).

Disclaimer

Lo lógico sería haber escrito este post explicando cómo usar Chart.js en una directiva de angularjs que para eso es el framework de moda (con permiso de ReactJS), pero como mi relación con angular está de capa caída y la aplicación donde quería integrarlo usa Knockout.js, ha tocado Knockout.js.

En ningún momento pretendo incentivar el uso de Knockout.js ;-)

Creando un custom binding para Knockout.js

Ya expliqué en su momento cómo crear custom bindings con Knockout.js, así que no me extenderé mucho, pero la idea básica es que podemos añadir nuevas propiedades al objeto ko.bindingHandlers y a través de las funciones init y update indicar cómo debe comportarse nuestro binding.

La estructura es la siguiente:

ko.bindingHandlers.myCustomBinding = {
  init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
    // Función invocada cuando se crea el binding. 
    // Úsala para modificar el DOM, enganchar manejadores de eventos, etc.
  },
  update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
    // Función invocada cada vez que cambia el valor de la propiedad enlazada
    // Úsala para actualizar la información mostrada en el DOM
  }
};

Viendo esto, definir un custom binding no parece muy complicado. Realmente lo más difícil en este caso es decidir cómo queremos construir el API para definir nuestro binding.

Chart.js permite configurar infinidad de parámetros tanto a través de los datos que se le pasan, como a través de las opciones del gráfico, pero para que sea relativamente cómodo de manejar, he optado por crear la siguiente sintaxis:

<canvas width="400" height="400" 
  data-bind="chartType: 'Pie', chartData: quarterSales, chartOptions: salesFormat">
</canvas>

La idea es definir no uno, sino tres bindings para indicar por separado el tipo de gráfico, los datos que queremos mostrar y las opciones del gráfico. Así podremos enlazarlas por separado y, por ejemplo, permitir al usuario mostrar la misma información como gráfico de tarta o de barras, o actualizar la información en tiempo real según se reciben nuevos datos.

El código para implementar esto es el siguiente:

(function(ko, Chart) {

  ko.bindingHandlers.chartType = {
    init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
      if (!allBindings.has('chartData')) {
        throw Error('chartType must be used in conjunction with chartData and (optionally) chartOptions');
      }
    },
    update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
      var ctx = element.getContext('2d'),
        type = ko.unwrap(valueAccessor()),
        data = ko.unwrap(allBindings.get('chartData')),
        options = ko.unwrap(allBindings.get('chartOptions')) || {};

      if (this.chart) {
        this.chart.destroy();
        delete this.chart;
      }

      this.chart = new Chart(ctx)[type](data, options);
    }
  };

  ko.bindingHandlers.chartData = {
    init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
      if (!allBindings.has('chartType')) {
        throw Error('chartData must be used in conjunction with chartType and (optionally) chartOptions');
      }
    }
  };

  ko.bindingHandlers.chartOptions = {
    init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
      if (!allBindings.has('chartData') || !allBindings.has('chartType')) {
        throw Error('chartOptions must be used in conjunction with chartType and chartData');
      }
    }
  };

})(ko, Chart);

En realidad, todo el proceso se hace a en el binding chartType, desde el cual podemos acceder a los valores enlazados en chartData y chartOptions a través del objeto allBindings. Los otros dos bindings se limitan a validar que la configuración de bindings es correcta para hacer la cosa un poco más amigable para el usuario.

El código es bastante sencillo, aunque tener que usar ko.unwrap para tratar con los observables de Knockout.js lo ensucia un poco (nadie dijo que Knockout.js fuese bonito). Al final todo se reduce a obtener los valores enlazadas e invocar “dinámicamente” el método del objeto Chart correspondiente al valor enlazado como chartType.

Podéis verlo en funcionamiento (y jugar con él) en este jsfiddle:

El código es tan simple que podéis copiarlo del propio post, pero si lo queréis un poco más limpio, aquí tenéis el código fuente completo.

Una nota final

Hay que tener en cuenta que en el diseño del binding ha primado la comodidad de uso frente a la eficiencia. Tal y como está diseñado, cada vez que se modifica algo del gráfico, éste se redibuja por completo, lo que puede ser un problema si estamos actualizándolo en tiempo real. Chart.js expone métodos optimizados para ese caso de uso y, si te enfrentas a ese escenario, sería bueno echarles un vistazo.


Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

*

Puedes usar las siguientes etiquetas y atributos HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>