Clausuras

El gráfico de la sección anterior funciona, pero requiere que el usuario defina todas las variables de configuración del gráfico en el contexto global. Lo ideal sería que esas variables sean internas al gráfico, pero que se puedan configurar adecuadamente.

Scope en JavaScript

En JavaScript, el comportamiento por defecto es que las variables se crean en el contexto global. Por ejemplo, si definimos la variable width, su valor será visible para cualquier función que se invoque en la página.

// Declaramos la variable `width` globalmente var width = 100; console.log('[global] width = ' + width);

Si definimos la función changeWidth en el contexto global, tendrá acceso a todas las variables definidas en este contexto, y por tanto, la función changeWidth puede modificar la variable width globalmente.

function changeWidth() { width = 150; console.log('[changeWidth] width = ' + width); } changeWidth();

El que la variable width se cambie en el contexto global puede causar problemas con otros códigos incluidos en la página.

console.log('[global] width = ' + width);

Esto es una fuente potencial de errores de programación y de conflictos de nombre, sobre todo con nombres de variable comunes.

Clausuras

Lo ideal sería crear estas variables en un contexto privado, no visible desde el scope global. Las funciones declaradas dentro de una función son visibles en todas partes dentro de esta función, pero no fuera de ella. En JavaScript, las funciones tienen una referencia al scope en el que fueron creadas. En el siguiente ejemplo, el scope de la función chart es la función createChart, por tanto chart tiene acceso a todas las variables definidas en createChart.

function createChart() { // Attributos var width = 50; console.log('[createChart] width = ' + width); function chart() { console.log('[chart] width = ' + width); } chart(); } createChart(); console.log('[global] width = ' + width);

Logramos crear la variable width interna a la función, que no interfiere con el valor de la variable global width. Esto nos permitirá crear una función para crear un gráfico usando variables internas, minimizando el riesgo de conflictos con variables definidas en el contexto global.

Ahora, no tenemos acceso a la variable interna width fuera de la función createChart, por lo tanto, la única forma de cambiar su valor es cambiando el código de la función createChart. Queremos ser capaces de usar el gráfico sin tener que modificar el código. Afortunadamente, JavaScript tiene algunas particularidades que nos ayudan con esta situación.

Funciones

En JavaScript, las funciones son objetos de primera clase. Esto significa que son variables como cualquier otra: se pueden pasar como argumento a otras funciones, asignar a variables y se les pueden agregar atributos. También podemos retornar funciones. Ejemplificaremos esto con la función chart:

function createChart() { var width = 50; function chart() { console.log('[chart] width = ' + width); } // Retornamos la función return chart; } // La función `createChart` retorna la función `chart` var grafico = createChart(); // La variable global `width` no se altera console.log('[global] width = ' + width);

La función grafico es una referencia a la función chart. Podemos ahora invocar a la función grafico, lo que tendrá el mismo efecto que invocar la función chart dentro de createChart.

// Invocamos a la función `chart` grafico(); // La variable global `width` no se altera console.log('[global] width = ' + width);

Como las funciones son objetos como cualquier otro, podemos agregar atributos a una función. Por ejemplo, vamos a agregar una etiqueta a la función chart.

function createChart() { var width = 50; function chart() { console.log('[chart] width = ' + width); } // Agregamos un atributo a la función chart.label = 'awesome!'; return chart; } var grafico = createChart(); // Tenemos acceso al atributo `label` console.log('etiqueta = ' + grafico.label); // La variable global `width` no se altera console.log('[global] width = ' + width);

Pero, además de agregar atributos simples a una función, también podemos agregarle funciones. Estas funciones tendrán acceso al scope de la función chart, y por tanto, a todas las variables definidas en createChart.

function createChart() { var width = 50; function chart() { console.log('[chart] width = ' + width); } chart.setWidth = function(newWidth) { width = newWidth; }; return chart; }

Ahora podemos cambiar el valor de la variable width interna a la función createChart sin modificar el código de la función.

var grafico = createChart(); // Invocamos el método/atributo `setWidth` de la función `grafico` grafico.setWidth(500); // Ahora invocamos la función `grafico`. grafico(); // La variable global `width` no se altera console.log('[global] width = ' + width);

Además, podemos hacer una función que permite obtener y definir un valor al mismo tiempo, ahorrándonos la necesidad de crear funciones get y set para cada atributo de la función.

function createChart() { var width = 50; function chart() { console.log('[chart] width = ' + width); } chart.width = function(newWidth) { if (!arguments.length) { return width; } width = newWidth; }; return chart; } var chart = createChart(); // Obtenemos el ancho console.log('chart width = ' + chart.width()); // Definimos un nuevo ancho chart.width(1000); // Obtenemos el nuevo ancho console.log('chart width = ' + chart.width());

Podemos definir este valor antes o después de invocar la función chart. En ambos casos el efecto es el mismo. Mediante un último detalle, podemos agregar soporte para method chaining, permitiendo configurar varios atributos en cadena.

function createChart() { var width = 50, height = 50; function chart() { console.log('chart size = ' + width + 'x' + height); } chart.width = function(newWidth) { if (!arguments.length) { return width; } width = newWidth; return chart; }; chart.height = function(newHeight) { if (!arguments.length) { return height; } height = newHeight; return chart; }; return chart; }

Por ejemplo, podemos definir el alto y ancho del gráfico en una línea. Este patrón es bastante usado en JavaScript.

// Creamos la función `chart` var chart = createChart(); // Obtenemos el ancho y alto console.log('chart size = ' + chart.width() + 'x' + chart.height()); // Definimos un nuevo ancho chart.width(1000).height(200); // Obtenemos los valores ancho y alto actualizados console.log('chart size = ' + chart.width() + 'x' + chart.height());

Creando un gráfico reutilizable

Esta construcción nos permitirá crear un gráfico reutilizable, con métodos para configurar la apariencia (ancho, alto, márgenes) y las funciones de acceso a las variables. Usando la estructura que hemos construido, sólo nos faltaría modificar la función chart para que reciba una selección.

function createListItems() { var color = 'blue', label = function(d) { return d.label; }; function chart(selection) { selection.each(function(data) { var ul = d3.select(this), li = ul.selectAll('li').data(data); li.enter().append('li') .html(function(d) { return label(d); }) .style('color', color); li .style('color', color) .html(function(d) { return label(d); }); }); } chart.label = function(labelAccessor) { if (!arguments.length) { return label; } label = labelAccessor; return chart; }; chart.color = function(newColor) { if (!arguments.length) { return color; } color = newColor; return chart; }; return chart; }

Podemos usar este gráfico básico para crear elementos de lista usando cualquier arreglo de datos:

var data = [ {nombre: 'Jorge', profesion: 'DT'}, {nombre: 'Manuel', profesion: 'Ingeniero'}, {nombre: 'Gary', profesion: 'Futbolista'}, ]; var grafico = createListItems() .label(function(d) { return d.nombre; }); d3.select('#ejemplo-b01 ul') .data([data]) .call(grafico);

    Además, podemos actualizar los atributos del gráfico y cargar datos nuevos.

    var data2 = [ {nombre: 'Jorge', profesion: 'DT'}, {nombre: 'Gary', profesion: 'Futbolista'}, {nombre: 'Luiz Felipe', profesion: 'Cesante'}, {nombre: 'Joachim', profesion: 'Genio'} ]; var grafico = createListItems() .label(function(d) { return d.nombre + ', ' + d.profesion + '.'; }) .color('red'); d3.select('#ejemplo-b01 ul') .data([data2]) .call(grafico);