Centralizar um mapa no d3 dado um objeto geoJSON

140

Atualmente no d3, se você tiver um objeto geoJSON que você irá desenhar, precisará escalá-lo e traduzi-lo para obter o tamanho desejado e convertê-lo para centralizá-lo. Esta é uma tarefa muito tediosa de tentativa e erro, e eu queria saber se alguém sabia uma maneira melhor de obter esses valores?

Então, por exemplo, se eu tiver esse 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");
});

Como diabos obtenho .scale (8500) e .translate ([0, -1200]) sem ir aos poucos?

climboid
fonte

Respostas:

134

O seguinte parece fazer aproximadamente o que você deseja. A escala parece estar correta. Ao aplicá-lo ao meu mapa, há um pequeno deslocamento. Esse pequeno deslocamento provavelmente é causado porque eu uso o comando translate para centralizar o mapa, enquanto eu provavelmente deveria usar o comando center.

  1. Crie uma projeção e d3.geo.path
  2. Calcular os limites da projeção atual
  3. Use esses limites para calcular a escala e a tradução
  4. Recrie a projeção

Em código:

  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")
    });
Jan van der Laan
fonte
9
Ei, Jan van der Laan, nunca lhe agradeci por esta resposta. Esta é uma resposta muito boa, a propósito, se eu pudesse dividir a recompensa que faria. Obrigado por isso!
Clevoid
Se eu aplicar isso, obtenho limites = infinito. Alguma idéia de como isso pode ser resolvido?
Simke Nys
@SimkeNys Esse pode ser o mesmo problema mencionado aqui stackoverflow.com/questions/23953366/… Experimente a solução mencionada aqui.
Jan van der Laan
1
Olá janeiro, obrigado pelo seu código. Eu tentei o seu exemplo com alguns dados do GeoJson, mas não funcionou. Você pode me dizer o que estou fazendo de errado? :) Fiz upload dos dados do GeoJson: onedrive.live.com/…
user2644964
1
No D3 v4, o ajuste de projeção é um método interno : projection.fitSize([width, height], geojson)( API docs ) - consulte a resposta de @dnltsk abaixo.
Florian Ledermann
173

Minha resposta está próxima da de Jan van der Laan, mas você pode simplificar um pouco as coisas porque não precisa calcular o centróide geográfico; você só precisa da caixa delimitadora. E, usando uma projeção de unidade não dimensionada e não traduzida, você pode simplificar a matemática.

A parte importante do código é 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);

Após compilar a caixa delimitadora do recurso na projeção unitária, é possível calcular a escala apropriada comparando a proporção da caixa delimitadora ( b[1][0] - b[0][0]e b[1][1] - b[0][1]) com a proporção da tela ( widthe height). Nesse caso, também dimensionei a caixa delimitadora para 95% da tela, em vez de 100%, para que haja um pouco de espaço extra nas bordas para traços e recursos ou preenchimento ao redor.

Em seguida, você pode calcular a conversão usando o centro da caixa delimitadora ( (b[1][0] + b[0][0]) / 2e (b[1][1] + b[0][1]) / 2) e o centro da tela ( width / 2e height / 2). Observe que, como a caixa delimitadora está nas coordenadas da projeção da unidade, ela deve ser multiplicada pela escala ( s).

Por exemplo, bl.ocks.org/4707858 :

projeto para caixa delimitadora

Há uma pergunta relacionada sobre qual é o zoom para um recurso específico em uma coleção sem ajustar a projeção, ou seja , combinando a projeção com uma transformação geométrica para aumentar e diminuir o zoom. Isso usa os mesmos princípios acima, mas a matemática é um pouco diferente porque a transformação geométrica (o atributo "transformação" do SVG) é combinada com a projeção geográfica.

Por exemplo, bl.ocks.org/4699541 :

zoom para caixa delimitadora

mbostock
fonte
2
Quero ressaltar que existem alguns erros no código acima, especificamente nos índices dos limites. Deve parecer com: s = (0,95 / Math.max ((b [1] [0] - b [0] [0]) / largura, (b [1] [1] - b [0] [0] ) / altura)) * 500, t = [(largura - s * (b [1] [0] + b [0] [0])) / 2, (altura - s * (b [1] [1] + b [0] [1])) / 2];
iros 21/03
2
@iros - Parece que * 500aqui é estranho ... também b[1][1] - b[0][0]deve estar b[1][1] - b[0][1]no cálculo da escala.
Nrabinowitz 04/04
2
Relatório de bug: meta.stackexchange.com/questions/184140/…
Paulo Scardine
5
Então: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
É por causa de uma comunidade como essa que D3 é uma alegria trabalhar com eles. Impressionante!
arunkjn
54

Com d3 v4 ou v5, fica muito mais fácil!

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

e finalmente

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

Aproveite, Felicidades

dnltsk
fonte
2
Espero que esta resposta seja votada mais. Trabalha d3v4há um tempo e acaba de descobrir esse método.
Mark
2
De onde gvem? Esse é o contêiner svg?
Tschallacka
1
Tschallacka gdeve ser <g></g>tag
giankotarola
1
Pena que isso é tão baixo e depois de 2 respostas de qualidade. É fácil perder isso e obviamente é muito mais simples que as outras respostas.
Kurt
1
Obrigado. Também funciona na v5!
Chaitanya Bangera
53

Eu sou novo no d3 - tentarei explicar como eu o entendo, mas não tenho certeza se entendi tudo direito.

O segredo é saber que alguns métodos irão operar no espaço cartográfico (latitude, longitude) e outros no espaço cartesiano (x, y na tela). O espaço cartográfico (nosso planeta) é (quase) esférico, o espaço cartesiano (tela) é plano - para mapear um sobre o outro, você precisa de um algoritmo, chamado de projeção . Esse espaço é muito curto para aprofundar o assunto fascinante das projeções e como elas distorcem as características geográficas para transformar esféricas em plano; alguns são projetados para conservar ângulos, outros conservam distâncias e assim por diante - sempre há um compromisso (Mike Bostock tem uma enorme coleção de exemplos ).

insira a descrição da imagem aqui

Em d3, o objeto de projeção possui uma propriedade / setter central, dada em unidades de mapa:

projection.center ([local])

Se o centro for especificado, define o centro da projeção no local especificado, uma matriz de dois elementos de longitude e latitude em graus e retorna a projeção. Se o centro não for especificado, retornará o centro atual com o padrão ⟨0 °, 0 °⟩.

Há também a tradução, dada em pixels - onde o centro da projeção fica em relação à tela:

projection.translate ([point])

Se point for especificado, define o deslocamento da conversão da projeção para a matriz de dois elementos especificada [x, y] e retorna a projeção. Se o ponto não for especificado, retornará o deslocamento atual da conversão, cujo padrão é [480, 250]. O deslocamento da translação determina as coordenadas do pixel do centro da projeção. O deslocamento de conversão padrão coloca ⟨0 °, 0 °⟩ no centro de uma área de 960 × 500.

Quando quero centralizar um recurso na tela, gosto de definir o centro de projeção no centro da caixa delimitadora de recursos - isso funciona para mim ao usar o mercator (WGS 84, usado no google maps) para o meu país (Brasil), nunca testado usando outras projeções e hemisférios. Pode ser necessário fazer ajustes para outras situações, mas se você seguir esses princípios básicos, ficará bem.

Por exemplo, dada uma projeção e caminho:

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

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

O boundsmétodo from pathretorna a caixa delimitadora em pixels . Use-o para encontrar a escala correta, comparando o tamanho em pixels com o tamanho nas unidades do mapa (0,95 fornece uma margem de 5% sobre o melhor ajuste para largura ou altura). Geometria básica aqui, calculando a largura / altura do retângulo, dada os cantos opostos na diagonal:

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); 

insira a descrição da imagem aqui

Use o d3.geo.boundsmétodo para encontrar a caixa delimitadora nas unidades de mapa:

b = d3.geo.bounds(feature);

Defina o centro da projeção no centro da caixa delimitadora:

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

Use o translatemétodo para mover o centro do mapa para o centro da tela:

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

Até agora você deve ter o recurso no centro do mapa ampliado com uma margem de 5%.

Paulo Scardine
fonte
Existe um bloco em algum lugar?
Hugolpz
Desculpe, sem bloqueios ou essência, o que você está tentando fazer? É algo como um clique para ampliar? Publique e eu posso dar uma olhada no seu código.
Paulo Scardine
A resposta e as imagens de Bostock fornecem links para exemplos do bl.ocks.org que permitem copiar um código inteiro para o engenheiro. Tarefa concluída. +1 e obrigado por suas ótimas ilustrações!
Hugolpz
4

Existe um método center () que você pode usar que aceita um par lat / lon.

Pelo que entendi, translate () é usado apenas para mover literalmente os pixels do mapa. Não sei como determinar o que é escala.

Dave Foster
fonte
8
Se você estiver usando o TopoJSON e quiser centralizar o mapa inteiro, poderá executar o topojson com --bbox para incluir um atributo bbox no objeto JSON. As coordenadas lat / lon para o centro devem ser [(b [0] + b [2]) / 2, (b [1] + b [3]) / 2] (onde b é o valor de bbox).
Paulo Scardine
3

Além Centro um mapa em d3 dado um objeto GeoJSON , nota que você pode preferir fitExtent()mais fitSize()se você quiser especificar um preenchimento ao redor dos limites do seu objeto. fitSize()define automaticamente este preenchimento como 0.

Sylvain Lesage
fonte
2

Eu estava procurando na Internet uma maneira fácil de centralizar meu mapa e fui inspirado pela resposta de Jan van der Laan e mbostock. Aqui está uma maneira mais fácil de usar o jQuery se você estiver usando um contêiner para o svg. Criei uma borda de 95% para estofamento / bordas 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);

Se você estiver procurando por escala exata, esta resposta não funcionará para você. Mas, como eu, você deseja exibir um mapa centralizado em um contêiner, isso deve ser suficiente. Eu estava tentando exibir o mapa mercator e descobri que esse método era útil para centralizar meu mapa, e eu poderia facilmente cortar a parte da Antártica, pois não precisava dele.

Wei Hao
fonte
1

Para deslocar / ampliar o mapa, você deve sobrepor o SVG no Leaflet. Isso será muito mais fácil do que transformar o SVG. Veja este exemplo http://bost.ocks.org/mike/leaflet/ e, em seguida, Como alterar o centro do mapa no folheto

Kristofor Carle
fonte
Se a adição de uma outra dependência é preocupante, PAN e ZOOM pode ser feito facilmente em d3 puro, ver stackoverflow.com/questions/17093614/...
Paulo Scardine
Esta resposta realmente não lida com d3. Você também pode aplicar zoom / zoom no mapa em d3, pois o folheto não é necessário. (Só percebi isso um post antigo, foi apenas navegando as respostas)
JARRRRG
0

Com a resposta da mbostocks e o comentário de Herb Caudill, comecei a ter problemas com o Alasca desde que usava uma projeção mercator. Devo observar que, para meus próprios propósitos, estou tentando projetar e centralizar os Estados Unidos. Eu descobri que tinha que me casar com as duas respostas com a resposta de Jan van der Laan, com a seguinte exceção para polígonos que se sobrepõem aos hemisférios (polígonos que acabam com um valor absoluto para Leste-Oeste maior que 1):

  1. configure uma projeção simples no mercator:

    projeção = d3.geo.mercator (). scale (1) .translate ([0,0]);

  2. crie o caminho:

    caminho = d3.geo.path (). projeção (projeção);

3. configurar meus limites:

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. Adicione uma exceção ao Alasca e aos estados que se sobrepõem aos hemisférios:

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);
}

Eu espero que isso ajude.

DimeZilla
fonte
0

Para as pessoas que desejam ajustar verticalmente e horizontalmente, eis a solução:

  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")
    });
pcmanprogrammeur
fonte
0

Como eu centralizei um Topojson, onde eu precisava extrair o recurso:

      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)
Estatísticas de aprendizado por exemplo
fonte