Centrar un mapa en d3 dado un objeto geoJSON


140

Actualmente en d3 si tiene un objeto geoJSON que va a dibujar, debe escalarlo y traducirlo para obtener el tamaño que desee y traducirlo para centrarlo. Esta es una tarea muy tediosa de prueba y error, y me preguntaba si alguien sabía una mejor manera de obtener estos valores.

Entonces, por ejemplo, si tengo este código

var path, vis, xy;
xy = d3.geo.mercator().scale(8500).translate([0, -1200]);

path = d3.geo.path().projection(xy);

vis = d3.select("#vis").append("svg:svg").attr("width", 960).attr("height", 600);

d3.json("../../data/ireland2.geojson", function(json) {
  return vis.append("svg:g")
    .attr("class", "tracts")
    .selectAll("path")
    .data(json.features).enter()
    .append("svg:path")
    .attr("d", path)
    .attr("fill", "#85C3C0")
    .attr("stroke", "#222");
});

¿Cómo diablos obtengo .scale (8500) y .translate ([0, -1200]) sin ir poco a poco?


Respuestas:


134

Lo siguiente parece hacer aproximadamente lo que quieres. La escala parece estar bien. Al aplicarlo a mi mapa hay un pequeño desplazamiento. Este pequeño desplazamiento probablemente se deba a que uso el comando traducir para centrar el mapa, mientras que probablemente debería usar el comando central.

  1. Crea una proyección y d3.geo.path
  2. Calcular los límites de la proyección actual.
  3. Use estos límites para calcular la escala y la traducción.
  4. Recrear la proyección

En codigo:

  var width  = 300;
  var height = 400;

  var vis = d3.select("#vis").append("svg")
      .attr("width", width).attr("height", height)

  d3.json("nld.json", function(json) {
      // create a first guess for the projection
      var center = d3.geo.centroid(json)
      var scale  = 150;
      var offset = [width/2, height/2];
      var projection = d3.geo.mercator().scale(scale).center(center)
          .translate(offset);

      // create the path
      var path = d3.geo.path().projection(projection);

      // using the path determine the bounds of the current map and use 
      // these to determine better values for the scale and translation
      var bounds  = path.bounds(json);
      var hscale  = scale*width  / (bounds[1][0] - bounds[0][0]);
      var vscale  = scale*height / (bounds[1][1] - bounds[0][1]);
      var scale   = (hscale < vscale) ? hscale : vscale;
      var offset  = [width - (bounds[0][0] + bounds[1][0])/2,
                        height - (bounds[0][1] + bounds[1][1])/2];

      // new projection
      projection = d3.geo.mercator().center(center)
        .scale(scale).translate(offset);
      path = path.projection(projection);

      // add a rectangle to see the bound of the svg
      vis.append("rect").attr('width', width).attr('height', height)
        .style('stroke', 'black').style('fill', 'none');

      vis.selectAll("path").data(json.features).enter().append("path")
        .attr("d", path)
        .style("fill", "red")
        .style("stroke-width", "1")
        .style("stroke", "black")
    });

9
Hola, Jan van der Laan, nunca te agradecí esta respuesta. Por cierto, esta es una buena respuesta si pudiera dividir la recompensa que haría. ¡Gracias por eso!
Climboid

Si aplico esto obtengo límites = infinito. ¿Alguna idea de cómo se puede resolver esto?
Simke Nys

@SimkeNys Este podría ser el mismo problema que se menciona aquí stackoverflow.com/questions/23953366/... Pruebe la solución mencionada allí.
Jan van der Laan

1
Hola Jan, gracias por tu código. Probé su ejemplo con algunos datos de GeoJson pero no funcionó. ¿Puedes decirme qué estoy haciendo mal? :) Subí los datos de GeoJson: onedrive.live.com/…
user2644964

1
En D3 v4, el ajuste de proyección es un método incorporado: projection.fitSize([width, height], geojson)( documentos de API ) - vea la respuesta de @dnltsk a continuación.
Florian Ledermann

173

Mi respuesta es cercana a la de Jan van der Laan, pero puedes simplificar las cosas ligeramente porque no necesitas calcular el centroide geográfico; solo necesitas el cuadro delimitador. Y, al usar una proyección de unidad no traducida y sin escala, puede simplificar las matemáticas.

La parte importante del código es esta:

// Create a unit projection.
var projection = d3.geo.albers()
    .scale(1)
    .translate([0, 0]);

// Create a path generator.
var path = d3.geo.path()
    .projection(projection);

// Compute the bounds of a feature of interest, then derive scale & translate.
var b = path.bounds(state),
    s = .95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height),
    t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];

// Update the projection to use computed scale & translate.
projection
    .scale(s)
    .translate(t);

Después de compilar el cuadro delimitador de la entidad en la proyección de la unidad, puede calcular la escala apropiada comparando la relación de aspecto del cuadro delimitador ( b[1][0] - b[0][0]y b[1][1] - b[0][1]) con la relación de aspecto del lienzo (width y height). En este caso, también he escalado el cuadro delimitador al 95% del lienzo, en lugar del 100%, por lo que hay un poco de espacio extra en los bordes para los trazos y las características circundantes o el relleno.

Luego, puede calcular la traducción utilizando el centro del cuadro delimitador ( (b[1][0] + b[0][0]) / 2y (b[1][1] + b[0][1]) / 2) y el centro del lienzo ( width / 2y height / 2). Tenga en cuenta que dado que el cuadro delimitador está en las coordenadas de la proyección de la unidad, debe multiplicarse por la escala ( s).

Por ejemplo, bl.ocks.org/4707858 :

proyecto a cuadro delimitador

Hay una pregunta relacionada sobre cuál es cómo hacer zoom a una característica específica en una colección sin ajustar la proyección, es decir , combinar la proyección con una transformación geométrica para acercar y alejar. Utiliza los mismos principios que el anterior, pero las matemáticas son ligeramente diferentes porque la transformación geométrica (el atributo "transformar" SVG) se combina con la proyección geográfica.

Por ejemplo, bl.ocks.org/4699541 :

acercar al cuadro delimitador


2
Quiero señalar que hay algunos errores en el código anterior, específicamente en los índices de los límites. Debería verse así: s = (0.95 / Math.max ((b [1] [0] - b [0] [0]) / width, (b [1] [1] - b [0] [0] ) / altura)) * 500, t = [(ancho - s * (b [1] [0] + b [0] [0])) / 2, (altura - s * (b [1] [1] + b [0] [1])) / 2];
iros

2
@iros - Parece que * 500aquí es extraño ... también, b[1][1] - b[0][0]debería estar b[1][1] - b[0][1]en el cálculo de la escala.
nrabinowitz


55
Entonces:b.s = b[0][1]; b.n = b[1][1]; b.w = b[0][0]; b.e = b[1][0]; b.height = Math.abs(b.n - b.s); b.width = Math.abs(b.e - b.w); s = .9 / Math.max(b.width / width, (b.height / height));
Herb Caudill

3
Debido a una comunidad como esta, D3 es un placer trabajar con él. ¡Increíble!
arunkjn

54

Con d3 v4 o v5, ¡es mucho más fácil!

var projection = d3.geoMercator().fitSize([width, height], geojson);
var path = d3.geoPath().projection(projection);

y finalmente

g.selectAll('path')
  .data(geojson.features)
  .enter()
  .append('path')
  .attr('d', path)
  .style("fill", "red")
  .style("stroke-width", "1")
  .style("stroke", "black");

Disfruta, salud


2
Espero que esta respuesta se vote más. He estado trabajando d3v4durante un tiempo y acabo de descubrir este método.
Mark

2
De donde gviene ¿Es ese el contenedor de svg?
Tschallacka

1
Tschallacka gdebería ser <g></g>etiquetada
giankotarola

1
Es una pena que esto esté tan lejos y después de 2 respuestas de calidad. Es fácil pasar por alto esto y obviamente es mucho más simple que las otras respuestas.
Kurt

1
Gracias. Funciona en v5 también!
Chaitanya Bangera

53

Soy nuevo en d3: intentaré explicar cómo lo entiendo, pero no estoy seguro de haberlo entendido bien

El secreto es saber que algunos métodos funcionarán en el espacio cartográfico (latitud, longitud) y otros en el espacio cartesiano (x, y en la pantalla). El espacio cartográfico (nuestro planeta) es (casi) esférico, el espacio cartesiano (pantalla) es plano; para mapear uno sobre el otro necesita un algoritmo, que se llama proyección . Este espacio es demasiado corto para profundizar en el fascinante tema de las proyecciones y en cómo distorsionan las características geográficas para convertir el esférico en plano; algunos están diseñados para conservar ángulos, otros conservan distancias, etc. Siempre hay un compromiso (Mike Bostock tiene una gran colección de ejemplos ).

ingrese la descripción de la imagen aquí

En d3, el objeto de proyección tiene una propiedad central / setter, dada en unidades de mapa:

projection.center ([ubicación])

Si se especifica el centro, establece el centro de la proyección en la ubicación especificada, una matriz de dos elementos de longitud y latitud en grados y devuelve la proyección. Si no se especifica el centro, devuelve el centro actual cuyo valor predeterminado es ⟨0 °, 0 °⟩.

También está la traducción, dada en píxeles, donde el centro de proyección se encuentra en relación con el lienzo:

projection.translate ([punto])

Si se especifica el punto, establece el desplazamiento de la traducción de la proyección en la matriz de dos elementos especificada [x, y] y devuelve la proyección. Si no se especifica el punto, devuelve el desplazamiento de la traducción actual que por defecto es [480, 250]. El desplazamiento de la traducción determina las coordenadas de píxel del centro de la proyección. El desplazamiento de traducción predeterminado coloca ⟨0 °, 0 °⟩ en el centro de un área de 960 × 500.

Cuando quiero centrar una característica en el lienzo, me gusta establecer el centro de proyección en el centro del cuadro delimitador de características; esto funciona para mí cuando uso mercator (WGS 84, utilizado en google maps) para mi país (Brasil), nunca probado usando otras proyecciones y hemisferios. Puede que tenga que hacer ajustes para otras situaciones, pero si logra estos principios básicos, estará bien.

Por ejemplo, dada una proyección y ruta:

var projection = d3.geo.mercator()
    .scale(1);

var path = d3.geo.path()
    .projection(projection);

El boundsmétodo de pathdevuelve el cuadro delimitador en píxeles . Úselo para encontrar la escala correcta, comparando el tamaño en píxeles con el tamaño en unidades de mapa (0.95 le da un margen de 5% sobre el mejor ajuste para ancho o alto). Geometría básica aquí, calculando el ancho / alto del rectángulo dados las esquinas diagonalmente opuestas:

var b = path.bounds(feature),
    s = 0.9 / Math.max(
                   (b[1][0] - b[0][0]) / width, 
                   (b[1][1] - b[0][1]) / height
               );
projection.scale(s); 

ingrese la descripción de la imagen aquí

Use el d3.geo.boundsmétodo para encontrar el cuadro delimitador en unidades de mapa:

b = d3.geo.bounds(feature);

Establezca el centro de la proyección en el centro del cuadro delimitador:

projection.center([(b[1][0]+b[0][0])/2, (b[1][1]+b[0][1])/2]);

Use el translatemétodo para mover el centro del mapa al centro del lienzo:

projection.translate([width/2, height/2]);

En este momento, debería tener la función en el centro del mapa ampliada con un margen del 5%.


¿Hay un bl.ocks en alguna parte?
Hugolpz

Lo siento, no hay bl.ocks o gist, ¿qué estás tratando de hacer? ¿Es algo así como un clic para hacer zoom? Publícalo y puedo ver tu código.
Paulo Scardine

La respuesta y las imágenes de Bostock proporcionan enlaces a ejemplos de bl.ocks.org que me permiten copiar un código completo de ingeniería. Trabajo hecho. +1 y gracias por tus excelentes ilustraciones!
Hugolpz

4

Hay un método center () que puede usar que acepta un par lat / lon.

Por lo que entiendo, translate () solo se usa para mover literalmente los píxeles del mapa. No estoy seguro de cómo determinar qué escala es.


8
Si está utilizando TopoJSON y desea centrar todo el mapa, puede ejecutar topojson con --bbox para incluir un atributo bbox en el objeto JSON. Las coordenadas lat / lon para el centro deben ser [(b [0] + b [2]) / 2, (b [1] + b [3]) / 2] (donde b es el valor de bbox).
Paulo Scardine


2

Estaba buscando en Internet una manera fácil de centrar mi mapa y me inspiré en la respuesta de Jan van der Laan y mbostock. Aquí hay una manera más fácil de usar jQuery si está utilizando un contenedor para el svg. Creé un borde del 95% para relleno / bordes, etc.

var width = $("#container").width() * 0.95,
    height = $("#container").width() * 0.95 / 1.9 //using height() doesn't work since there's nothing inside

var projection = d3.geo.mercator().translate([width / 2, height / 2]).scale(width);
var path = d3.geo.path().projection(projection);

var svg = d3.select("#container").append("svg").attr("width", width).attr("height", height);

Si busca una escala exacta, esta respuesta no funcionará para usted. Pero si, como yo, desea mostrar un mapa que se centralice en un contenedor, esto debería ser suficiente. Estaba tratando de mostrar el mapa de mercator y descubrí que este método era útil para centralizar mi mapa, y podía cortar fácilmente la porción antártica ya que no lo necesitaba.


1

Para desplazar / hacer zoom en el mapa, debe mirar superponer el SVG en el folleto. Eso será mucho más fácil que transformar el SVG. Vea este ejemplo http://bost.ocks.org/mike/leaflet/ y luego Cómo cambiar el centro del mapa en el folleto


Si agregar otra dependencia es motivo de preocupación, PAN y ZOOM se pueden hacer fácilmente en d3 puro, consulte stackoverflow.com/questions/17093614/…
Paulo Scardine

Esta respuesta realmente no trata con d3. También puede desplazar / ampliar el mapa en d3, no es necesario el folleto. (Acabo de
darme

0

Con la respuesta de mbostocks y el comentario de Herb Caudill, comencé a tener problemas con Alaska ya que estaba usando una proyección de mercator. Debo señalar que, para mis propios fines, estoy tratando de proyectar y centrar los Estados Unidos. Descubrí que tenía que casar las dos respuestas con la respuesta de Jan van der Laan con la siguiente excepción para los polígonos que se superponen a los hemisferios (polígonos que terminan con un valor absoluto para Este - Oeste que es mayor que 1):

  1. configurar una proyección simple en mercator:

    proyección = d3.geo.mercator (). scale (1) .translate ([0,0]);

  2. crea el camino:

    ruta = d3.geo.path (). proyección (proyección);

3. configurar mis límites:

var bounds = path.bounds(topoJson),
  dx = Math.abs(bounds[1][0] - bounds[0][0]),
  dy = Math.abs(bounds[1][1] - bounds[0][1]),
  x = (bounds[1][0] + bounds[0][0]),
  y = (bounds[1][1] + bounds[0][1]);

4. Agregue una excepción para Alaska y estados que se superponen a los hemisferios:

if(dx > 1){
var center = d3.geo.centroid(topojson.feature(json, json.objects[topoObj]));
scale = height / dy * 0.85;
console.log(scale);
projection = projection
    .scale(scale)
    .center(center)
    .translate([ width/2, height/2]);
}else{
scale = 0.85 / Math.max( dx / width, dy / height );
offset = [ (width - scale * x)/2 , (height - scale * y)/2];

// new projection
projection = projection                     
    .scale(scale)
    .translate(offset);
}

Espero que esto ayude.


0

Para las personas que desean ajustar verticalmente y horizontalmente, esta es la solución:

  var width  = 300;
  var height = 400;

  var vis = d3.select("#vis").append("svg")
      .attr("width", width).attr("height", height)

  d3.json("nld.json", function(json) {
      // create a first guess for the projection
      var center = d3.geo.centroid(json)
      var scale  = 150;
      var offset = [width/2, height/2];
      var projection = d3.geo.mercator().scale(scale).center(center)
          .translate(offset);

      // create the path
      var path = d3.geo.path().projection(projection);

      // using the path determine the bounds of the current map and use 
      // these to determine better values for the scale and translation
      var bounds  = path.bounds(json);
      var hscale  = scale*width  / (bounds[1][0] - bounds[0][0]);
      var vscale  = scale*height / (bounds[1][1] - bounds[0][1]);
      var scale   = (hscale < vscale) ? hscale : vscale;
      var offset  = [width - (bounds[0][0] + bounds[1][0])/2,
                        height - (bounds[0][1] + bounds[1][1])/2];

      // new projection
      projection = d3.geo.mercator().center(center)
        .scale(scale).translate(offset);
      path = path.projection(projection);

      // adjust projection
      var bounds  = path.bounds(json);
      offset[0] = offset[0] + (width - bounds[1][0] - bounds[0][0]) / 2;
      offset[1] = offset[1] + (height - bounds[1][1] - bounds[0][1]) / 2;

      projection = d3.geo.mercator().center(center)
        .scale(scale).translate(offset);
      path = path.projection(projection);

      // add a rectangle to see the bound of the svg
      vis.append("rect").attr('width', width).attr('height', height)
        .style('stroke', 'black').style('fill', 'none');

      vis.selectAll("path").data(json.features).enter().append("path")
        .attr("d", path)
        .style("fill", "red")
        .style("stroke-width", "1")
        .style("stroke", "black")
    });

0

Cómo centré un Topojson, donde necesitaba sacar la función:

      var projection = d3.geo.albersUsa();

      var path = d3.geo.path()
        .projection(projection);


      var tracts = topojson.feature(mapdata, mapdata.objects.tx_counties);

      projection
          .scale(1)
          .translate([0, 0]);

      var b = path.bounds(tracts),
          s = .95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height),
          t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];

      projection
          .scale(s)
          .translate(t);

        svg.append("path")
            .datum(topojson.feature(mapdata, mapdata.objects.tx_counties))
            .attr("d", path)
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.