Imaginemos que queremos realizar un panel de control. Dentro de este panel de control, queremos una gráfica de líneas que nos vaya mostrando en tiempo real la evolución temporal de un indicador.
El resultado final
El resultado final conseguido es éste:
Podéis abrir este mismo resultado en una ventana independiente aquí.
El código html
Se limita a crear un div
con id graph1
en el que insertaré posteriormente mi gráfica:
1 2 3 4 5 6 7 |
<div class="container-fluid"> <div class="row"> <div class="col-lg-3 col-sm-6" id="graph1"> <p class="title">Gráfica 1</p> </div> </div> </div> |
El generador de datos
Para realizar este experimento, necesito una fuente de datos (para simular al indicador). Así que me he creado un pequeño generador de datos:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
'use strict'; var DataSource = function DataSource(options) { var self = this; this.options = jQuery.extend({ maxValue: 20, initialDate: new Date(2000,0,1), timeInterval: 1000 }, options); this.data = []; this.lastDate = this.options.initialDate; this.callbackList = []; for (var i=0; i<10; i++) { this.data.push(this.generateNewValue()); } window.setInterval(function() { var value = self.generateNewValue(); for (var i=0; i<self.callbackList.length; i++) { self.data.shift(); self.data.push(value); self.callbackList[i](value); } }, self.options.timeInterval); }; DataSource.prototype.getData = function getData() { return this.data; }; DataSource.prototype.generateNewValue = function generateNewValue() { this.lastDate = new Date(this.lastDate.valueOf() + (1000 * 60)); var value = parseInt(Math.random() * this.options.maxValue); return { date: this.lastDate, value: value }; }; DataSource.prototype.onNewValue = function onNewData(callback) { this.callbackList.push(callback); }; |
Básicamente, es una clase que:
- Mantiene un array de 10 valores (podría haber parametrizado el número de valores, pero he sólo he parametrizado lo mínimo que me interesaba para el experimento).
- Cada dato del array, es un objeto con dos propiedades
date
yvalue
. - Las propiedades
date
de cada valor están separadas por 1 minuto de diferencia, partiendo del valor inicialoptions.initialDate
. - Las propiedades
value
de cada valor los calculo aleatoriamente entre 0 yoptions.maxValue
. - Cada intervalo de tiempo
options.timeInteval
(en milisegundos), se genera un nuevo valor. - Usando el método
onNewValue
, se pueden registrar funciones callback que serán invocadas cuando se genere un nuevo valor.
Cuando se genera un nuevo valor:
- El primer valor de la pila es eliminado y se añade este nuevo valor.
- Invoca a las funciones callback registradas.
Un ejemplo de uso de este generador sería:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// creo un nuevo DataSource con un valor máximo de 100 y que genere nuevos valores cada 2 segundos var dataSource = new DataSource({maxValue:100, timeInterval:2000}); // puedo recuperar la referencia a la pila de valores var data = dataSource.getData(); console.debug('El contenido inicial de la pila es:'); for (var i=0; i<data.length; i++) { console.debug('dato ' + i + ': ' + data[i].date.toLocaleDateString() + ' ' + data[i].value); } // registro un callback que será invocado cuando se genere un nuevo valor dataSource.onNewValue(function(newValue) { console.debug('Se ha añadido un nuevo valor : '+ newValue.date.toLocaleDateString() + ' ' + newValue.value); console.debug('El contenido nuevo de la pila es:'); for (var i=0; i<data.length; i++) { console.debug('dato ' + i + ': ' + data[i].date.toLocaleDateString() + ' ' + data[i].value); } }); |
El código javascript para la gráfica
Desde el punto de vista de d3.js, este código es bastante trivial (ya me he extendido bastante más al respecto en mi serie sobre d3.js).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
'use strict'; (function(jQuery, d3, DataSource) { var margins = { top: 10, right: 15, bottom: 20, left: 30 }; var data; var svg = d3.selectAll('div#graph1').append('svg'); var width = parseInt(jQuery('div#graph1').css('width')) * 0.9; var height = Math.min(width, 600); svg.attr('width', width).attr('height', height); var xAxisLine = svg.append('svg:g') .attr('class', 'x axis') .attr('transform', 'translate(0,' + (height - margins.bottom) + ')'); var yAxisLine = svg.append('svg:g') .attr('class', 'y axis left') .attr('transform', 'translate(' + (margins.left) + ',0)'); var graphLine = svg.append('svg:path') .attr('class', 'graph-line'); var xRange = d3.time.scale() .range([margins.left, width - margins.right]); var yRange = d3.scale.linear() .range([height - margins.bottom, margins.top]) .domain([0,100]); var xAxis = d3.svg.axis() .scale(xRange) .tickFormat(d3.time.format('%H:%M')) .tickSize(5) .tickSubdivide(true); var yAxis = d3.svg.axis() .scale(yRange) .tickSize(5) .orient('left') .tickSubdivide(true); var valueFunc = d3.svg.line() .x(function(d) { return xRange(d.date); }) .y(function(d) { return yRange(d.value); }) .interpolate('linear'); var redrawData = function redrawData() { xRange.domain([d3.min(data, function(d) { return d.date; }), d3.max(data, function(d) { return d.date; })]); xAxisLine.transition().call(xAxis); yAxisLine.transition().call(yAxis); graphLine.attr('d', valueFunc(data)); }; var dataSource = new DataSource({maxValue:100}); data = dataSource.getData(); redrawData(); dataSource.onNewValue(function() { redrawData(); }); })(window.jQuery, window.d3, window.DataSource); |
Define los componentes de la gráfica:
- Genera un elemento
svg
en el que inserta los elementos para los ejes de coordenadas (xAxisLine
yyAxisLine
) y la gráficagraphLine
. - Define una escala horizontal
xRange
que será temporal y una escala verticalyRange
que será lineal. - Define un eje horizontal
xAxis
asociado a la escalaxRange
y al ejexAxisLine
. - Define un eje vertical
yAxis
asociado a la escalyRange
y al ejeyAxisLine
.
La funcion redrawData
será invocada cuando el array data
esté correctamente inicializado:
- Configura la escala horizontal
xRange
en función de los valores mínimo y máximo de la propiedaddate
. - Redibuja los ejes horizontal y vertical.
- Redibuja la gráfica.
La parte realmente interesante, es al final del código:
- Crea un objeto
dataSource
de la claseDataSource.
- Inicializa la variable
data
para que apunte a la propiedaddata
del objetodataSource
. - Le pasamos al evento
dataSource.onNewValue
una función anómima que llamará aredrawData
.
La siguiente parte
Como explicaba antes, este código d3.js es bastante básico (en esta entrada, me he limitado a describirlo por encima, sin entrar en detalles). Pero ¿qué ocurre si en lugar de tener que representar una fuente de datos, tenemos que representar en la misma página más de una fuente de datos, cada una con su propia gráfica? ¿Hacemos un copia-y-pega del código actual? En la siguiente entrada, crearé un plugin jQuery que nos permita modularizar este código.