En la entrada anterior usamos d3.js para definir una gráfica lineal que evolucionaba temporalmente en tiempo real, siguiendo la evolución de una fuente de datos. El código era bastante lineal:
- Simulamos una fuente de datos, creando un objeto de la clase
DataSource
. - En nuestra página, definimos un
div
con idgraph1
. - Usando d3.js, creamos una gráfica que insertamos en el
div
anterior y la refrescábamos cada vez que la fuente de datos tenía variaciones.
Pero, si en lugar de una sola fuente de datos (y una sola gráfica) ¿qué ocurre si tenemos seis fuentes de datos, cada una con su propia gráfica? ¿Cómo podemos modularizar el código anterior, para reutilizarlo?
En esta entrada, vamos a encapsular el código anterior, creando un plugin para jQuery.
El resultado final
El resultado final, es el siguiente:
Podéis abrir este mismo resultado en una ventana independiente aquí.
El código html
Veamos el nuevo código html:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<div class="container-fluid"> <div class="row"> <div id="graph1" class="col-lg-3 col-sm-4 graph" data-max-value="20" data-time-interval="1000"> <p class="title">Graph 1</p> </div> <div id="graph2" class="col-lg-3 col-sm-4 graph" data-max-value="30" data-time-interval="2000"> <p class="title">Graph 2</p> </div> <div id="graph3" class="col-lg-3 col-sm-4 graph" data-max-value="40" data-time-interval="3000"> <p class="title">Graph 3</p> </div> <div id="graph4" class="col-lg-3 col-sm-4 graph" data-max-value="50" data-time-interval="1000"> <p class="title">Graph 4</p> </div> <div id="graph5" class="col-lg-3 col-sm-4 graph" data-max-value="60" data-time-interval="2000"> <p class="title">Graph 5</p> </div> <div id="graph6" class="col-lg-3 col-sm-4 graph" data-max-value="70" data-time-interval="3000"> <p class="title">Graph 6</p> </div> </div> </div> |
- Defino seis
div
contenedores, uno para cada gráfica. - Todos los contenedores, son de la calse
graph.
- Cada
div
tiene dos atributosdata-max-value
ydata-time-interval
. - Cada
div
también tiene unid
único.
El código javascript
Veamos primero el código completo:
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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
'use strict'; (function(jQuery, d3) { jQuery.fn.lineGraph = function(options) { var opts = jQuery.extend({ data : [], maxValue : 100 }, options); var margins = { top: 10, right: 15, bottom: 20, left: 20 }; var svg = d3.selectAll(this).append('svg'); var width = parseInt(jQuery(this).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,opts.maxValue]); 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(opts.data, function(d) { return d.date; }), d3.max(opts.data, function(d) { return d.date; })]); xAxisLine.transition().call(xAxis); yAxisLine.transition().call(yAxis); graphLine.attr('d', valueFunc(opts.data)); }; redrawData(); return { redraw : function() { redrawData(); } }; }; })(window.jQuery, window.d3); (function(jQuery, DataSource) { jQuery(function() { var generateCallback = function(lineGraph, dataSource) { dataSource.onNewValue(function() { lineGraph.redraw(); }); }; jQuery('div.graph').each(function(index, value) { var maxValue = parseInt(jQuery(this).attr('data-max-value')); var timeInterval = parseInt(jQuery(this).attr('data-time-interval')); var dataSource = new DataSource({ maxValue : maxValue, timeInterval : timeInterval }); var lineGraph = jQuery(this).lineGraph({ data: dataSource.getData(), maxValue: maxValue }); dataSource.onNewValue(function() { lineGraph.redraw(); }); }); }); })(window.jQuery, window.DataSource); |
Ahora, vamos a ir estudiándolo, poco a poco.
La activación del plugin
La primera parte del código, es la definición del nuevo plugin. Pero, antes de estudiarla, vamos a ver su activación que está al final del código (creo que es lo más didáctico):
1 2 3 4 5 6 7 8 9 10 11 |
(function(jQuery, DataSource) { jQuery(function() { jQuery('div.graph').each(function(index, value) { var maxValue = parseInt(jQuery(this).attr('data-max-value')); var timeInterval = parseInt(jQuery(this).attr('data-time-interval')); var dataSource = new DataSource({ maxValue : maxValue, timeInterval : timeInterval }); var lineGraph = jQuery(this).lineGraph({ data: dataSource.getData(), maxValue: maxValue }); dataSource.onNewValue(function() { lineGraph.redraw(); }); }); }); })(window.jQuery, window.DataSource); |
- Selecciono los
div
que van a contener mis gráficas conjQuery('div.graph')
. - Para cada
div
encontrado: - Extraigo sus atributos
data-max-value
ydata-time-interval
. - Creo un objeto de la clase
DataSource
. - Activo el nuevo plugin
lineGraph
sobre eldiv
. - Configuro el evento
onNewValue
del objetodataSource
para que refresque la gráfica (llamando al métodoredraw
del plugin que acabo de crear).
Yo creo que el uso del plugin es bastante directo. No le he añadido muchas opciones, porque mi objetivo era conseguir la configuración básica. Pero, aún así, se entiende de que hemos conseguido el objetivo (reutilizar el código encapsulándolo dentro del plugin para crear nuevas gráficas de forma bastante sencilla).
La definición del plugin
Ahora que hemos visto cómo usar el plugin, vamos a ver cómo debemos definirlo para conseguir ese interfase:
1 2 3 4 5 |
jQuery.fn.lineGraph = function(options) { var opts = jQuery.extend({ data : [], maxValue : 100 }, options); |
- Defino el plugin dentro de jQuery.
- Defino las opciones por defecto y la extiendo con las opciones que se le pasen al plugin cuando es activado.
La creación de la gráfica
El código que viene a continuación, prácticamente es el mismo usado en la entrada anterior para crear la gráfica:
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 |
var svg = d3.selectAll(this).append('svg'); var width = parseInt(jQuery(this).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,opts.maxValue]); 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(opts.data, function(d) { return d.date; }), d3.max(opts.data, function(d) { return d.date; })]); xAxisLine.transition().call(xAxis); yAxisLine.transition().call(yAxis); graphLine.attr('d', valueFunc(opts.data)); }; redrawData(); return { redraw : function() { redrawData(); } }; |
Sólo merece la pena comentar:
- La gráfica, es insertada en el objeto
this
que se le pasa al plugin (y que corresponde con el elementodiv
sobre el que se está activando la gráfica). - Usa las opciones
opt.maxValue
yopt.data
para configurar la gráfica (el máximo valor en el eje vertical y los datos para dibujar la gráfica, respectivamente). - Devuelve un objeto con un sólo método
redraw
que será el que invoque para pedir al plugin que redibuje la gráfica.
El código css
Una vez que hemos visto el código html que hemos necesitado, vamos a ver el código css:
1 2 3 4 5 6 7 8 9 10 11 |
p.title { text-align:center; } .axis path, .axis line { fill:none; stroke:#777; shape-rendering:crispEdges; } .axis text { font-family:'Arial'; font-size:9px; } .x.axis text { font-family:'Arial'; font-size:8px; } .tick { stroke-dasharray:1, 2; } .graph-line { stroke:black; stroke-width:1; fill:none; } #graph2 .graph-line { stroke:blue; } #graph3 .graph-line { stroke:red; } #graph4 .graph-line { stroke:green; } #graph5 .graph-line { stroke:purple; } #graph6 .graph-line { stroke:yellow; } |
El detalle más importante es que, como he identificado a cada div
contenedor con su propio id
puedo usar estos id
para dar formatos específicos a cada gráfica individual (en este caso, he dado un color distinto a cada gráfica).
Conclusiones
El plugin que he creado es bastante básico. Pero ha sido suficiente para conseguir encapsular mi código d3.js y poder reutilizarlo de forma muy limpia.
En una próxima entrada, me gustaría volver a encapsular nuevamente este código … pero dentro de un componente angular.js, para estudiar las diferencias entre ambas versiones.