Podemos escribir una función reusable para dibujar el mapa. Entre los atributos que tendrá el mapa están las dimensiones width y height, la proyección (equirectangular en este caso) y la escala. Notar que la escala se podría configurar directamente en la proyección.
function geojsonMap() {
// Atributos del mapa
var me = {
height: 300,
width: 600,
projection: d3.geo.equirectangular(),
scale: 600 / (2 * Math.PI)
};
function chart(selection) {
selection.each(function(features) {
// Data binding
var div = d3.select(this),
svg = div.selectAll('svg.geojson-map').data([features]);
// Creación del contenedor SVG
svg.enter().append('svg')
.classed('geojson-map', true);
svg.attr('width', me.width).attr('height', me.height);
svg.exit().remove();
// Background de la figura
var background = svg.selectAll('rect.background').data([features]);
background.enter().append('rect')
.classed('background', true);
background
.attr('width', me.width)
.attr('height', me.height);
background.exit().remove();
// Configuramos la proyección
me.projection
.translate([me.width / 2, me.height / 2])
.scale(me.scale);
// Configuramos el generador de paths
var pathGenerator = d3.geo.path()
.projection(me.projection);
// Dibujamos los path que representan los features
var pathFeatures = svg.selectAll('path.feature').data(features);
pathFeatures.enter().append('path')
.classed('feature', true);
pathFeatures.attr('d', pathGenerator);
pathFeatures.exit().remove();
// Creamos las línes de meridianos y paralelos
var graticule = d3.geo.graticule();
var graticuleLines = svg.selectAll('path.graticule').data([graticule()]);
graticuleLines.enter().append('path')
.classed('graticule', true);
graticuleLines
.attr('d', pathGenerator);
graticuleLines.exit().remove();
});
}
// Funciones de acceso
chart.width = function(value) {
if (!arguments.length) { return me.width; }
me.width = value;
return chart;
};
chart.height = function(value) {
if (!arguments.length) { return me.height; }
me.height = value;
return chart;
};
chart.projection = function(value) {
if (!arguments.length) { return me.projection; }
me.projection = value;
return chart;
};
chart.scale = function(value) {
if (!arguments.length) { return me.scale; }
me.scale = value;
return chart;
};
return chart;
}
Ahora, podemos crear una instancia del gráfico, seleccionar un contenedor para el mapa, vincular los datos e invocar el mapa. Notar que el mapa puede ser creado y configurado fuera del callback de la función d3.json.
Ahora que sabemos usar proyecciones y generar formas, podemos usar lo que sabemos de SVG para agregar un poco de interactividad al mapa. Empezamos definiendo algunos parámetros para nuestro mapa
// Parametros del mapa
var width = 600,
height = 300,
zoomScale = 16,
centerX = width / 2,
centerY = height / 2;
// Proyección
var projection = d3.geo.equirectangular()
.scale(width / (2 * Math.PI))
.translate([centerX, centerY]);
// Generador de paths
var pathGenerator = d3.geo.path()
.projection(projection);
Ahora cargamos el archivo GeoJSON con los datos del mundo. Esta vez, vamos a agregar un grupo que va a contener los features. Agregaremos un listener para el evento click en los features. El callback del evento va a trasladar y escalar el grupo de forma que el feature seleccionado quede centrado. Al hacer click de nuevo, el mapa vuelve a su estado original.
d3.json('/src/data/countries.geojson', function(error, data) {
// Data binding
var div = d3.select('#ejemplo-e02'),
svg = div.selectAll('svg').data([data.features]);
svg.enter().append('svg');
svg.attr('width', width).attr('height', height);
svg.exit().remove();
// Creamos el grupo contenedor
var gCountries = svg.selectAll('g.countries').data([data.features]);
gCountries.enter().append('g')
.classed('countries', true);
gCountries.exit().remove();
// Creamos los features dentro del grupo
var pathCountries = gCountries.selectAll('path.country').data(data.features);
pathCountries.enter().append('path')
.classed('country', true);
// Al hacer click, centramos y escalamos el grupo, o lo restablecemos
// según corresponda
pathCountries
.on('click', function(d) {
// Agregamos el atributo `_centered` a cada feature.
d._centered = !d._centered;
// El atributo `_centered` es falso para todos, excepto el feature seleccionado
pathCountries.each(function(u) {
if (u !== d) { u._centered = false; }
});
// Asignamos la clase `centered` sólo al feature seleccionado
pathCountries.classed('centered', false);
d3.select(this).classed('centered', d._centered);
if (d._centered) {
// Calculamos el centroide del feature (lon, lat) y
// calculamos la proyección del centro en pixeles
var centerCoords = d3.geo.centroid(d),
centerPixels = projection(centerCoords);
// Transladamos y escalamos el grupo adecuadamente. Notar que
// las operaciones no son conmutativas
gCountries.transition().duration(2000)
.attr('transform', function() {
var dx = centerX - zoomScale * centerPixels[0],
dy = centerY - zoomScale * centerPixels[1];
var t = 'translate(' + [dx, dy] + ')',
s = 'scale(' + zoomScale + ')';
return t + s;
});
} else {
// Restablecemos la transformación del grupo
gCountries.transition().duration(1500).attr('transform', '');
}
});
pathCountries.attr('d', pathGenerator);
pathCountries.exit().remove();
});