Práctica 3: Filtrando los datos y optimizando la gráfica
Hasta ahora, en las dos primeras versiones que hemos realizado de nuestra gráfica de los datos del IBEX35 (si estás interesado, puedes consultarlas aquí y aquí) hemos representado el conjunto completo de los datos que hemos recuperado (estamos hablando de datos históricos diarios que abarcan desde el año 1994). Para comenzar a practicar a manejar D3.js, está bien. Pero si queremos estudiar estos datos, esta gráfica puede ser poco práctica. Así que en esta nueva versión vamos a concentrarnos en poder seleccionar qué rango de datos queremos mostrar en pantalla.
Para conseguir el filtro de los datos, teníamos dos alternativas:
- La primera, es conseguir una API en el servidor que permita recuperar sólo ciertos datos, mediante parámetros adicionales.
- La segunda, es continuar recuperando todos los datos y efectuar este filtro en el navegador, mediante javascript.
Para estos ejemplos, voy a emplear la segunda alternativa. Los motivos para esta decisión son dos:
- El primero, para tener la oportunidad de hacer estas operaciones con javascript.
- El segundo motivo, es que prefiero pedir todos los datos para manipularlos en local (conforme avance con futuras versiones, espero aclarar este motivo).
Además, el conjunto de datos no es muy grande y pueden ser recuperados de una sola vez (ése podía ser un motivo para segmentar las peticiones, pero como digo en este caso no es necesario).
El resultado final
El resultado final es el siguiente:
Podéis consultarlo en una ventana independiente pinchando aquí.
El código html
Para incluir los selectores del rango de fecha, nuestro código html se ha complicado un poco. Ha pasado de ser:
1 2 3 4 5 |
<div id="wrapper" class="container-fluid"> <div class="row"> <div id="ibex35"></div> </div> </div> |
a ser:
1 2 3 4 5 6 7 8 9 10 11 |
<div id="wrapper" class="container-fluid"> <div class="row" id="graph-options"> <label>Choose a time period: </label> <div class="input-daterange input-group" id="datepicker"> <input type="text" class="input-sm form-control" name="start"/> <span class="input-group-addon">to</span> <input type="text" class="input-sm form-control" name="end"/> </div> </div> <div id="ibex35"></div> </div> |
Básicamente, he añadido un nuevo div
que contiene los nuevos selectores de fecha.
Para crear los selectores de fecha, he usado bootstrap-datepicker.js (realmente, no me convence mucho el resultado, pero mi objetivo actual es concentrarme con D3.js, así que lo doy por razonable).
El código javascript
Primero, veamos el código javascript 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 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
'use strict'; (function(d3) { jQuery(function() { var data = []; var fromDate, toDate; var width, height; var margins = { top: 20, right: 50, bottom: 20, left: 50 }; var svg, xRange, yRange, xAxis, yAxis, valueFunc; $('#datepicker').datepicker({ format: 'dd/mm/yyyy', language: 'es', autoclose: true }); jQuery('#datepicker input[name=start]').datepicker().on('changeDate', function(e){ if ((e.date - fromDate) !== 0) { fromDate = e.date; redrawData(); } }); jQuery('#datepicker input[name=end]').datepicker().on('changeDate', function(e){ if ((e.date - toDate) !== 0) { toDate = e.date; redrawData(); } }); svg = d3.selectAll('div#ibex35').append('svg'); svg.append('svg:path').attr('class', 'daily-values'); svg.append('svg:g').attr('class', 'x axis'); svg.append('svg:g').attr('class', 'y axis left'); var redrawSvg = function redrawSvg() { jQuery('#ibex35').css('position', 'absolute'); width = parseInt(jQuery('#ibex35').css('width'))-20; height = parseInt(jQuery('#wrapper').css('height')) - parseInt(jQuery('#graph-options').css('height')); jQuery('#ibex35').css('position', 'relative'); svg.attr('width', width).attr('height', height); svg.select('g.x.axis').attr('transform', 'translate(0,' + (height - margins.bottom) + ')'); svg.select('g.y.axis').attr('transform', 'translate(' + (margins.left) + ',0)'); xRange = d3.time.scale() .range([margins.left, width - margins.right]); yRange = d3.scale.linear() .range([height - margins.bottom, margins.top]); xAxis = d3.svg.axis() .scale(xRange) .tickSize(5) .tickSubdivide(true); yAxis = d3.svg.axis() .scale(yRange) .tickSize(5) .orient('left') .tickSubdivide(true); valueFunc = d3.svg.line() .x(function(d) { return xRange(d.date); }) .y(function(d) { return yRange(d.high); }) .interpolate('linear'); svg.append('svg:path') .attr('class', 'daily-values'); svg.append('svg:g') .attr('class', 'x axis') .attr('transform', 'translate(0,' + (height - margins.bottom) + ')'); svg.append('svg:g') .attr('class', 'y axis left') .attr('transform', 'translate(' + (margins.left) + ',0)'); }; var redrawData = function redrawData() { var filteredData = data.filter(function(d) { return (d.date >= fromDate && d.date <= toDate); }); xRange.domain([d3.min(filteredData, function(d) { return d.date; }), d3.max(filteredData, function(d) { return d.date; })]); yRange.domain([d3.min(filteredData, function(d) { return d.high; }), d3.max(filteredData, function(d) { return d.high; })]); svg.select('g.x.axis') .transition() .call(xAxis); svg.select('g.y.axis') .transition() .call(yAxis); svg.select('path.daily-values') .transition() .attr('d', valueFunc(filteredData)); }; redrawSvg(); d3.csv('./data/YAHOO-IBEX.csv') .row(function(d) { return { date: new Date(d.Date), high: +d.High }; }) .get(function(error, rows) { data = rows; fromDate = d3.min(data, function(d) { return d.date; }); toDate = d3.max(data, function(d) { return d.date; }); jQuery('#datepicker input').datepicker('setStartDate', fromDate); jQuery('#dataPicker input').datepicker('setEndDate', toDate); jQuery('#datepicker input[name=start]').datepicker('setDate', fromDate); jQuery('#datepicker input[name=end]').datepicker('setDate', toDate); redrawData(); }); jQuery(window).resize(function() { redrawSvg(); redrawData(); }); }); })(window.d3); |
Y, ahora, vamos a comenzar a estudiarlo, poco a poco:
Definición de variables
La primera parte, la definición de las variables, sigue igual (si alguien tiene interés, puede consultar los artículos anteriores):
1 2 3 4 5 6 7 8 9 10 |
var data = []; var fromDate, toDate; var width, height; var margins = { top: 20, right: 50, bottom: 20, left: 50 }; var svg, xRange, yRange, xAxis, yAxis, valueFunc; |
(Bueno, tenemos dos nuevas variables fromDate
y toDate
que guardarán el rango de fechas a visualizar).
Inicialización y configuración de los selectores del rango de fechas
Ahora, inicializamos los dos nuevos selectores del rango de fecha:
1 2 3 4 5 |
jQuery('#datepicker').datepicker({ format: 'dd/mm/yyyy', language: 'es', autoclose: true }); |
Y configuramos los eventos para cuando el usuario elija un nuevo rango reasigne las variables fromDate
y toDate
y redibuje la gráfica llamando a redrawData
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
jQuery('#datepicker input[name=start]').datepicker().on('changeDate', function(e){ if ((e.date - fromDate) !== 0) { fromDate = e.date; redrawData(); } }); jQuery('#datepicker input[name=end]').datepicker().on('changeDate', function(e){ if ((e.date - toDate) !== 0) { toDate = e.date; redrawData(); } }); |
Creación del objeto svg y los elementos interiores
Como hacíamos en versiones anteriores, vuelvo a crear el elemento svg
que me servirá de contenedor para mi gráfica y guardo una referencia en la variable svg
:
1 2 3 4 |
svg = d3.selectAll('div#ibex35').append('svg'); svg.append('svg:path').attr('class', 'daily-values'); svg.append('svg:g').attr('class', 'x axis'); svg.append('svg:g').attr('class', 'y axis left'); |
Lo que es diferente de las versiones anteriores es que también creo el el elemento path
al que adjuntaré la gráfica y los dos elementos g
a los que adjuntaré los ejes de coordenadas. Los creo sin ningún contenido. Pero ya los tengo creados (más adelante en el código, los recuperaré y terminaré de rellenarlos).
Defino la organización general de la gráfica
Mi función redrawSvg
será la responsable de configurar la gráfica. Primero, calculo la anchura y la altura y se la asigno a mi objeto svg
. Como ya tengo la altura y la anchura, posiciono los dos ejes de coordenadas:
1 2 3 4 5 6 7 8 9 |
var redrawSvg = function redrawSvg() { jQuery('#ibex35').css('position', 'absolute'); width = parseInt(jQuery('#ibex35').css('width'))-20; height = parseInt(jQuery('#wrapper').css('height')) - parseInt(jQuery('#graph-options').css('height')); jQuery('#ibex35').css('position', 'relative'); svg.attr('width', width).attr('height', height); svg.select('g.x.axis').attr('transform', 'translate(0,' + (height - margins.bottom) + ')'); svg.select('g.y.axis').attr('transform', 'translate(' + (margins.left) + ',0)'); |
Esta vez, para calcular la altura disponible para mi gráfica, debo restarle a la altura disponible (para más información, puedes consultar la parte anterior) la altura que ocupan los selectores de rango de fecha. Para que la altura de la propia gráfica no influya en la altura del contenedor, le cambio su position
a absolute
.
Defino las escalas, los ejes y la gráfica
Ya expliqué estas definiciones en las entradas anteriores, así que esta vez me limito a citarlas:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
xRange = d3.time.scale() .range([margins.left, width - margins.right]); yRange = d3.scale.linear() .range([height - margins.bottom, margins.top]); xAxis = d3.svg.axis() .scale(xRange) .tickSize(5) .tickSubdivide(true); yAxis = d3.svg.axis() .scale(yRange) .tickSize(5) .orient('left') .tickSubdivide(true); valueFunc = d3.svg.line() .x(function(d) { return xRange(d.date); }) .y(function(d) { return yRange(d.high); }) .interpolate('linear'); |
Defino la función que dibujará la gráfica
Una vez que recupere los datos, mi función redrawData
será la encargada de filtrarlos (en función del rango de fechas que elija el usuario) y de volcarlos en la gráfica. Vamos a ver paso a paso cómo lo hace.
Primero, filtro los datos:
1 |
var filteredData = data.filter(function(d) { return (d.date >= fromDate && d.date <= toDate); }); |
Para filtrar los datos uso la el método filter
y creo un nuevo array filteredData
.
Una vez tengo los datos filtrados, ya puedo terminar de configurar las escalas horizontal y vertical:
1 2 |
xRange.domain([d3.min(filteredData, function(d) { return d.date; }), d3.max(filteredData, function(d) { return d.date; })]); yRange.domain([d3.min(filteredData, function(d) { return d.high; }), d3.max(filteredData, function(d) { return d.high; })]); |
Por último, dibujo los ejes de coordenadas y la gráfica:
1 2 3 4 5 6 7 8 9 10 11 |
svg.select('g.x.axis') .transition() .call(xAxis); svg.select('g.y.axis') .transition() .call(yAxis); svg.select('path.daily-values') .transition() .attr('d', valueFunc(filteredData)); |
Esta parte del código también es diferente de las dos versiones anteriores. Vamos a estudiarla más detalladamente:
- ¿Recordáis cómo ya insertamos antes los elementos
g
para los ejes de coordenadas, pero vacíos? Pues ahora que tenemos toda la información, los recuperamos y les asignamos los objetosxAxis
eyAxis
que ya tenemos completamente definidos. Para recuperar estos elementosg
, usamos selectores (cuando los insertamos, les asignamos clases que los identificaban perfectamente y nos permite seleccionarlos). - Hacemos algo similar con el elemento
path
para la gráfica: lo seleccionamos y le asociamos los datosfilteredData
y el objetovalueFunc
para crear la gráfica.
¿Y qué diferencia hay entre la versión anterior y esta nueva?:
- En la versión anterior, borrábamos los elementos y los volvíamos a añadir. Esto implicaba la visualización de una gráfica completamente nueva.
- En este nueva versión, no borramos los elementos sino que cambiamos sus valores. Esto obliga a D3.js a realizar un redibujado de dichos elementos.
- Además, antes de asignar a los elementos sus nuevos valores, llamamos al método
transition
. En esta llamada, se muestra la potencia visual de D3.js porque le estamos pidiendo que realice dicho cambio mediante una animación.
Efectivamente, una simple llamada a transition
antes de realizar un cambio sobre un elemento preexistente, consigue efectos visuales espectaculares. Además, tiene muchas más alternativas de configuración (pero eso es tema para otra entrada específica).
Dibujo por primera vez la gráfica y capturo las notificaciones de cambios del rango de fecha
Al final del código, dibujo la gráfica por primera vez:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
redrawSvg(); d3.csv('./data/YAHOO-IBEX.csv') .row(function(d) { return { date: new Date(d.Date), high: +d.High }; }) .get(function(error, rows) { data = rows; fromDate = d3.min(data, function(d) { return d.date; }); toDate = d3.max(data, function(d) { return d.date; }); jQuery('#datepicker input').datepicker('setStartDate', fromDate); jQuery('#dataPicker input').datepicker('setEndDate', toDate); jQuery('#datepicker input[name=start]').datepicker('setDate', fromDate); jQuery('#datepicker input[name=end]').datepicker('setDate', toDate); redrawData(); }); jQuery(window).resize(function() { redrawSvg(); redrawData(); }); |
Esta parte del código, se ha complicado un poco respecto de las versiones anteriores:
- La llamada a
redrawSvg
crea el elementosvg
y su contenido básico. - Cuando termina la carga de los datos
- Calculo las fechas más extremas y las guardo en
fromDate
ytoDate
. - Inicializo con esos valores los selectores de fechas.
- Con los datos ya cargados, llamo a
redrawData
para que dibuje la gráfica.