Gráficas de D3 en el servidor con Node.js

Hoy por hoy es muy común oír hablar de D3 para la creación de gráficas en SVG de una forma llamativa y relativamente sencilla, D3 al igual que Google Charts genera gráficas en SVG, la diferencia esta en que D3 es una librería para manejar documentos(SVG's) basados en datos, es decir, te ayuda a crear tus gráficas pero no las hace por ti.

D3 funciona perfectamente para mostrar gráficas en los navegadores de nuestros usuarios, existen muchos ejemplos en la pagina web del proyecto, pero gracias a Node.js/io.js es posible generar estas gráficas del lado del servidor sin necesidad de un navegador web y en este post explicaremos como hacerlo mediante una API REST.

Antes que nada necesitaremos instalar Node.js o io.js, uno u otro, no los 2 juntos.

Es una muy buena práctica que nuestros proyectos de Node.js/io.js los creemos en base a un generador de Yeoman , ver como instalarlo, en mi caso me gusta usar un generador llamado bangular, lo instalamos con el siguiente comando:

sudo npm i -g generator-bangular

Crearemos un directorio para nuestro proyecto, llamado nod3 :

mkdir nod3 && cd nod3

y ejecutamos el generador :

yo bangular

Contestamos las preguntas y al final tendremos creado el esqueleto de un proyecto basado en Angular y Express,realmente solo necesitaremos Express.

Adicionalmente debemos instalar como dependencia D3 y JSDOM para comenzar a crear nuestras gráficas, para ello ejecutaremos el siguiente comando :

npm i --save d3 && npm i --save jsdom

Ahora ya tenemos instalado todo lo necesario para comenzar a trabajar en nuestras gráficas.

Primero que nada crearemos un endpoint para nuestra API llamado chart, una vez mas , corremos el siguiente comando:

yo bangular:api chart

Nuestro directorio server se vería así :

server/
├── api
│   └── chart
│       ├── chart.controller.js
│       ├── chart.data.json
│       └── index.js
├── config
│   ├── environment
│   │   ├── development.js
│   │   ├── index.js
│   │   ├── production.js
│   │   └── test.js
│   └── express.js
├── routes.js
└── server.js

Para facilitarnos la explicación utilizaremos como base el código de ejemplo para una gráfica de pie que se encuentra en esta página, el cual almacenaremos en un archivo llamado chart.pie.js y lo adaptaremos para recibir un JSON y un contenedor como parámetros en una función llamada draw que retornara un SVG en formato de cadena de texto,comencemos :

//http://bl.ocks.org/mbostock/3887235
var d3 = require('d3');

En la primera linea podemos observar que estamos importando el modulo de D3 que anteriormente habíamos instalado como dependencia.

exports.draw = function (data,container){

Después declaramos nuestra función draw con dos parámetros, data que sera la información de nuestra gráfica en formato JSON y container que es un contenedor virtual del DOM.

  var style="\n/* <![CDATA[ */\nbody {\n  font: 10px sans-serif;\n}\n.arc path {\n  stroke: #fff;\n}\n  /* ]]> */";

Declaramos una variable llamada style con los estilos de la gráfica comprimidos en una cadena de texto, al que adicionalmente le agregamos la etiqueta <![CDATA , esto es por que en los archivos SVG los estilos deben encontrarse dentro de una etiqueta style y esta a su vez en la etiqueta CDATA, puedes encontrar mas información aquí y aquí.

  var width = 960,
  height = 500,
  radius = Math.min(width, height) / 2;

  var color = d3.scale.ordinal()
  .range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]);

  var arc = d3.svg.arc()
  .outerRadius(radius - 10)
  .innerRadius(0);

  var pie = d3.layout.pie()
  .sort(null)
  .value(function(d) { return d.population; });

  var svg = d3.select(container.body).append("svg")
  .attr("width", width)
  .attr("height", height)
  .attr("version", 1.1)
  .attr("xmlns", "http://www.w3.org/2000/svg");

Al declara la variable svg modificamos el selector de D3 para que apunte al body de nuestro contenedor de jsdom y además agregamos los atributos version y xmlns para que nuestro SVG sea un documento valido(SVG ~ XML).

  svg.append("style")
  .attr("type","text/css")
  .text(style);

  svg = svg.append("g")
  .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");

Antes de la primera etiqueta <g> de nuestro documento es necesario agregar los estilos definidos anteriormente.

  data.forEach(function(d) {
    d.population = +d.population;
  });

  var g = svg.selectAll(".arc")
  .data(pie(data))
  .enter().append("g")
  .attr("class", "arc");

  g.append("path")
  .attr("d", arc)
  .style("fill", function(d) { return color(d.data.age); });

  g.append("text")
  .attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
  .attr("dy", ".35em")
  .style("text-anchor", "middle")
  .text(function(d) { return d.data.age; });

Después simplemente eliminamos la petición al CSV para cargar la información e incorporamos el cuerpo de la función de regreso al flujo normal de nuestra función principal.

  return d3.select(container.body).html();
};

Finalmente retornamos el cuerpo de el contenedor, el cual ya debe tener nuestro SVG, puedes ver el código completo aquí.

Lo siguiente es crear un archivo llamado chart.generator.js el cual tendrá el siguiente código :

'use strict';

var pie = require('./chart.pie');
var jsdom = require('jsdom');

exports.pie = function(data){
  var container = jsdom.jsdom();
  return pie.draw(data,container);
};

La función de este modulo es generar un DOM virtual , recibir la información en formato JSON y regresar la gráfica resultante dentro de una función llamada pie.

En nuestro chart.controller.js agregaremos el siguiente código :

var generator = require('./chart.generator');

exports.generate = function (req,res) {
  var chart = generator[req.params.name](JSON.parse(req.body.data));

  res.status(200).send( chart );
};

Aquí importamos nuestro modulo generator y creamos una función llamada generate la cual recibe una petición con un parámetro de Express llamado name y buscamos en nuestro modulo una función con el mismo nombre recibido mediante dicho parámetro para pasarle los datos que están en el cuerpo de la petición en la variable data y el resultado lo almacenamos en una variable llamada chart que posteriormente la regresamos en el response de la petición con estatus de OK(200).

Llegando a este punto podemos realizar peticiones a nuestro server para probar que nuestra gráfica se genera correctamente.

Para levantar nuestra aplicación corremos el comando gulp en la carpeta de nuestro proyecto y enviamos una petición por post a la ruta http://localhost:3000/api/charts/pie , para realizar la petición de prueba utilizo un plugin de Firefox llamado RESTClient, además es necesario pasar los datos de prueba que se encuentran en el ejemplo de la gráfica de pie de csv a json con la ayuda de codebeautify, los datos de prueba serán estos :

[
    {
        "age": "<5",
        "population": "2704659"
    },
    {
        "age": "5-13",
        "population": "4499890"
    },
    {
        "age": "14-17",
        "population": "2159981"
    },
    {
        "age": "18-24",
        "population": "3853788"
    },
    {
        "age": "25-44",
        "population": "14106543"
    },
    {
        "age": "45-64",
        "population": "8819342"
    },
    {
        "age": "≥65",
        "population": "612463"
    }
]

Al final en el body de nuestra peticion crearemos una variable data y le asignaremos nuestro json de esta forma

data=[     {         "age": "<5",         "population": "2704659"     },     {         "age": "5-13",         "population": "4499890"     },     {         "age": "14-17",         "population": "2159981"     },     {         "age": "18-24",         "population": "3853788"     },     {         "age": "25-44",         "population": "14106543"     },     {         "age": "45-64",         "population": "8819342"     },     {         "age": "≥65",         "population": "612463"     } ]

También tendremos que agregar un custom header con el nombre de Content-Type y el valor de application/x-www-form-urlencoded, como se ve en esta imagen:

Esta es una captura de pantalla de mi petición con RESTClient:

En la respuesta recibimos nuestro SVG como cadena de texto el cual se debería de ver así :

    <svg width="960" height="500" version="1.1" xmlns="http://www.w3.org/2000/svg"><style type="text/css">
    /* <![CDATA[ */
    body {
      font: 10px sans-serif;
    }
    .arc path {
      stroke: #fff;
    }
      /* ]]> */</style><g transform="translate(480,250)"><g class="arc"><path d="M1.469576158976824e-14,-240A240,240 0 0,1 107.0492887034484,-214.803281604555L0,0Z" style="fill: #98abc5;"></path><text transform="translate(27.493663849391726,-116.80795541458916)" dy=".35em" style="text-anchor: middle;">&lt;5</text></g><g class="arc"><path d="M107.0492887034484,-214.803281604555A240,240 0 0,1 226.32103905324743,-79.86731047092076L0,0Z" style="fill: #8a89a6;"></path><text transform="translate(89.91089051610481,-79.47346580212175)" dy=".35em" style="text-anchor: middle;">5-13</text></g><g class="arc"><path d="M226.32103905324743,-79.86731047092076A240,240 0 0,1 239.89217619670538,7.193316315084522L0,0Z" style="fill: #7b6888;"></path><text transform="translate(118.56810142542317,-18.48256812162655)" dy=".35em" style="text-anchor: middle;">14-17</text></g><g class="arc"><path d="M239.89217619670538,7.193316315084522A240,240 0 0,1 185.29089011249533,152.53617945038215L0,0Z" style="fill: #6b486b;"></path><text transform="translate(112.33465286267969,42.201016175220815)" dy=".35em" style="text-anchor: middle;">18-24</text></g><g class="arc"><path d="M185.29089011249533,152.53617945038215A240,240 0 0,1 -239.7935922865823,9.951537484044003L0,0Z" style="fill: #a05d56;"></path><text transform="translate(-38.16160101213167,113.77034854561566)" dy=".35em" style="text-anchor: middle;">25-44</text></g><g class="arc"><path d="M-239.7935922865823,9.951537484044003A240,240 0 0,1 -25.080788568417415,-238.68588991556737L0,0Z" style="fill: #d0743c;"></path><text transform="translate(-90.82228799185823,-78.43030029219551)" dy=".35em" style="text-anchor: middle;">45-64</text></g><g class="arc"><path d="M-25.080788568417415,-238.68588991556737A240,240 0 0,1 -2.5725010549733476e-13,-240L0,0Z" style="fill: #ff8c00;"></path><text transform="translate(-6.278797857311532,-119.83562365785485)" dy=".35em" style="text-anchor: middle;">≥65</text></g></g></svg>

Si guardamos el código generado en un archivo con extención svg podremos visualizar nuestra gráfica en Gimp o algún programa similar.

Este es el archivo resultante:

Notas

Algunos puntos importantes que debemos recalcar es que estas gráficas son archivos SVG y tienen algunas limitaciones:

  • No es posible usar la función transition de D3 ya que por alguna razón corrompe el SVG.

  • Es muy importante que los estilos tengan el atributo type="text/css".

  • Las animaciónes no funcionaran.

Conclusión

Creo que tener un servicio externo el cual genera gráficas con tan solo enviarle la información en un determinado formato es de mucha utilidad, además de que podemos agregar todas las gráficas que deseemos y se ha comprobado que D3 también es funcional en el servidor.

El código de este ejemplo lo podemos encontrar en github en el siguiente link aquí, se aceptan PR.

Fuentes y enlaces de interes