Centra una mappa in d3 dato un oggetto geoJSON


140

Attualmente in d3 se si dispone di un oggetto geoJSON che si desidera disegnare, è necessario ridimensionarlo e tradurlo per portarlo nella dimensione desiderata e tradurlo per centrarlo. Questo è un compito molto noioso di tentativi ed errori, e mi chiedevo se qualcuno sapesse un modo migliore per ottenere questi valori?

Quindi, ad esempio, se ho questo codice

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

Come diavolo posso ottenere .scale (8500) e .translate ([0, -1200]) senza andare a poco a poco?


Risposte:


134

Quanto segue sembra fare approssimativamente quello che vuoi. Il ridimensionamento sembra essere ok. Quando lo si applica alla mia mappa c'è un piccolo offset. Questo piccolo offset è probabilmente causato perché uso il comando translate per centrare la mappa, mentre probabilmente dovrei usare il comando center.

  1. Crea una proiezione e d3.geo.path
  2. Calcola i limiti della proiezione corrente
  3. Utilizzare questi limiti per calcolare la scala e la traduzione
  4. Ricrea la proiezione

Nel codice:

  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
Ehi Jan van der Laan, non ti ho mai ringraziato per questa risposta. Questa è una risposta davvero buona a proposito se potessi dividere la taglia che vorrei. Grazie per questo!
climboid

Se lo applico ottengo limiti = infinito. Qualche idea su come risolvere questo problema?
Simke Nys,

@SimkeNys Potrebbe trattarsi dello stesso problema menzionato qui stackoverflow.com/questions/23953366/… Prova la soluzione qui menzionata.
Jan van der Laan,

1
Ciao Jan, grazie per il tuo codice. Ho provato il tuo esempio con alcuni dati GeoJson ma non ha funzionato. Puoi dirmi cosa sto facendo di sbagliato? :) Ho caricato i dati GeoJson: onedrive.live.com/…
user2644964

1
In D3 v4 il raccordo di proiezione è un metodo incorporato: projection.fitSize([width, height], geojson)( Documenti API ) - vedere la risposta di @dnltsk di seguito.
Florian Ledermann,

173

La mia risposta è vicina a quella di Jan van der Laan, ma puoi semplificare leggermente le cose perché non hai bisogno di calcolare il centroide geografico; hai solo bisogno del rettangolo di selezione. E, usando una proiezione unitaria non graduata, non tradotta, puoi semplificare la matematica.

La parte importante del codice è questa:

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

Dopo aver calcolato il riquadro di delimitazione della funzione nella proiezione dell'unità, è possibile calcolare la scala appropriata confrontando le proporzioni del riquadro di delimitazione ( b[1][0] - b[0][0]e b[1][1] - b[0][1]) con le proporzioni dell'area di disegno ( widthe height). In questo caso, ho anche ridimensionato il rettangolo di selezione al 95% della tela, piuttosto che al 100%, quindi c'è un po 'di spazio in più sui bordi per tratti e funzioni circostanti o imbottitura.

Quindi puoi calcolare la traduzione usando il centro del rettangolo di selezione ( (b[1][0] + b[0][0]) / 2e (b[1][1] + b[0][1]) / 2) e il centro dell'area di disegno ( width / 2e height / 2). Si noti che poiché il rettangolo di selezione si trova nelle coordinate della proiezione dell'unità, deve essere moltiplicato per la scala ( s).

Ad esempio, bl.ocks.org/4707858 :

progetto al riquadro di selezione

C'è una domanda correlata su quale sia lo zoom su una funzione specifica in una raccolta senza regolare la proiezione, ovvero combinando la proiezione con una trasformazione geometrica per ingrandire e rimpicciolire. Questo utilizza gli stessi principi di cui sopra, ma la matematica è leggermente diversa perché la trasformazione geometrica (l'attributo "trasforma" SVG) è combinata con la proiezione geografica.

Ad esempio, bl.ocks.org/4699541 :

zoom sulla casella di selezione


2
Voglio sottolineare che ci sono alcuni errori nel codice sopra, in particolare negli indici dei limiti. Dovrebbe apparire come: s = (0.95 / Math.max ((b [1] [0] - b [0] [0]) / larghezza, (b [1] [1] - b [0] [0] ) / altezza)) * 500, t = [(larghezza - s * (b [1] [0] + b [0] [0])) / 2, (altezza - s * (b [1] [1] + b [0] [1])) / 2];
Iros,

2
@iros - Sembra che * 500sia estraneo qui ... anche, b[1][1] - b[0][0]dovrebbe essere b[1][1] - b[0][1]nel calcolo della scala.
nrabinowitz,


5
Quindi: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 il

3
È a causa di una comunità come questa che D3 è una tale gioia con cui lavorare. Eccezionale!
Arunkjn,

55

Con d3 v4 o v5 diventa sempre più facile!

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

e infine

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

Buon divertimento


2
Spero che questa risposta venga votata di più. Ho lavorato d3v4per un po 'e ho appena scoperto questo metodo.
Segna il

2
Da dove gviene È quello il contenitore svg?
Tschallacka,

1
Tschallacka gdovrebbe essere <g></g>tag
giankotarola il

1
Peccato che sia così in basso e dopo 2 risposte di qualità. È facile perdere questo ed è ovviamente molto più semplice delle altre risposte.
Kurt,

1
Grazie. Funziona anche in v5!
Chaitanya Bangera,

53

Sono nuovo di d3 - proverò a spiegare come lo capisco ma non sono sicuro di aver fatto tutto bene.

Il segreto sta nel sapere che alcuni metodi opereranno sullo spazio cartografico (latitudine, longitudine) e altri sullo spazio cartesiano (x, y sullo schermo). Lo spazio cartografico (il nostro pianeta) è (quasi) sferico, lo spazio cartesiano (schermo) è piatto - per mappare l'uno sull'altro è necessario un algoritmo, chiamato proiezione . Questo spazio è troppo breve per approfondire l'affascinante soggetto delle proiezioni e il modo in cui distorcono le caratteristiche geografiche per trasformare il piano sferico in piano; alcuni sono progettati per conservare gli angoli, altri conservano le distanze e così via - c'è sempre un compromesso (Mike Bostock ha una vasta collezione di esempi ).

inserisci qui la descrizione dell'immagine

In d3, l'oggetto di proiezione ha una proprietà / setter centrale, espressa in unità cartografiche:

projection.center ([percorso])

Se viene specificato center, imposta il centro della proiezione nella posizione specificata, una matrice a due elementi di longitudine e latitudine in gradi e restituisce la proiezione. Se il centro non è specificato, restituisce il centro corrente che per impostazione predefinita è ⟨0 °, 0 °⟩.

C'è anche la traduzione, data in pixel - dove si trova il centro di proiezione rispetto alla tela:

projection.translate ([punto])

Se viene specificato point, imposta l'offset di traslazione della proiezione sull'array a due elementi specificato [x, y] e restituisce la proiezione. Se il punto non è specificato, restituisce l'offset di traduzione corrente, il cui valore predefinito è [480, 250]. L'offset di traduzione determina le coordinate dei pixel del centro della proiezione. L'offset di traduzione predefinito posiziona ⟨0 °, 0 °⟩ al centro di un'area 960 × 500.

Quando voglio centrare una funzione nell'area di disegno, mi piace impostare il centro di proiezione al centro del riquadro di delimitazione della funzione: questo funziona per me quando utilizzo mercator (WGS 84, utilizzato in google maps) per il mio paese (Brasile), mai testato utilizzando altre proiezioni ed emisferi. Potrebbe essere necessario apportare modifiche per altre situazioni, ma se inchiodi questi principi di base andrà bene.

Ad esempio, dati una proiezione e un percorso:

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

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

Il boundsmetodo da pathrestituisce il rettangolo di selezione in pixel . Usalo per trovare la scala corretta, confrontando la dimensione in pixel con la dimensione in unità della mappa (0,95 ti dà un margine del 5% sulla misura migliore per larghezza o altezza). Geometria di base qui, calcolando la larghezza / altezza del rettangolo dati gli angoli diagonalmente opposti:

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

inserisci qui la descrizione dell'immagine

Utilizzare il d3.geo.boundsmetodo per trovare il rettangolo di selezione nelle unità della mappa:

b = d3.geo.bounds(feature);

Imposta il centro della proiezione al centro del riquadro di selezione:

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

Usa il translatemetodo per spostare il centro della mappa al centro dell'area di disegno:

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

Ormai dovresti avere la funzione al centro della mappa ingrandita con un margine del 5%.


C'è un bl.ocks da qualche parte?
Hugolpz,

Spiacente, niente bl.ocks o gist, cosa stai cercando di fare? È qualcosa come un clic per ingrandire? Pubblicalo e posso dare un'occhiata al tuo codice.
Paulo Scardine,

La risposta e le immagini di Bostock forniscono collegamenti ad esempi di bl.ocks.org che mi hanno permesso di copiare un intero codice. Lavoro fatto. +1 e grazie per le tue fantastiche illustrazioni!
Hugolpz,

4

C'è un metodo center () che puoi usare che accetta una coppia lat / lon.

Da quello che ho capito, translate () è usato solo per spostare letteralmente i pixel della mappa. Non sono sicuro di come determinare quale sia la scala.


8
Se stai usando TopoJSON e vuoi centrare l'intera mappa, puoi eseguire topojson con --bbox per includere un attributo bbox nell'oggetto JSON. Le coordinate lat / lon per il centro dovrebbero essere [(b [0] + b [2]) / 2, (b [1] + b [3]) / 2] (dove b è il valore di bbox).
Paulo Scardine,


2

Stavo cercando su Internet un modo semplice per centrare la mia mappa e mi sono ispirato a Jan van der Laan e alla risposta di Mbostock. Ecco un modo più semplice usando jQuery se stai usando un contenitore per svg. Ho creato un bordo del 95% per imbottitura / bordi ecc.

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 stai cercando il ridimensionamento esatto, questa risposta non funzionerà per te. Ma se come me, desideri visualizzare una mappa centralizzata in un contenitore, questo dovrebbe essere sufficiente. Stavo cercando di visualizzare la mappa del mercatore e ho scoperto che questo metodo era utile per centralizzare la mia mappa, e ho potuto facilmente tagliare la parte antartica poiché non ne avevo bisogno.


1

Per eseguire una panoramica / zoom della mappa, dovresti guardare la sovrapposizione dell'SVG su Leaflet. Sarà molto più semplice che trasformare l'SVG. Vedi questo esempio http://bost.ocks.org/mike/leaflet/ e poi Come modificare il centro della mappa nell'opuscolo


Se l'aggiunta di un'altra dipendenza è di preoccupazione, pan e zoom può essere fatto facilmente in puro d3, vedere stackoverflow.com/questions/17093614/...
Paulo Scardine

Questa risposta in realtà non riguarda d3. Puoi anche eseguire la panoramica / zoom della mappa in d3, non è necessario il volantino. (Ho appena realizzato che questo era un vecchio post, stava solo sfogliando le risposte)
JARRRRG

0

Con la risposta di mbostocks e il commento di Herb Caudill, ho iniziato a incorrere in problemi con l'Alaska da quando stavo usando una proiezione di mercatore. Devo notare che per i miei scopi, sto cercando di progettare e centrare gli Stati Uniti. Ho scoperto che dovevo sposare le due risposte con la risposta di Jan van der Laan con la seguente eccezione per i poligoni che si sovrappongono agli emisferi (poligoni che finiscono con un valore assoluto per Est - Ovest che è maggiore di 1):

  1. impostare una semplice proiezione in mercatore:

    proiezione = d3.geo.mercator (). scale (1) .translate ([0,0]);

  2. crea il percorso:

    path = d3.geo.path (). proiezione (proiezione);

3. imposta i miei limiti:

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.Aggiungi un'eccezione per l'Alaska e afferma che si sovrappongono agli emisferi:

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

Spero che aiuti.


0

Per le persone che vogliono regolare verticalmente e orizzontalmente, ecco la soluzione:

  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

Come ho centrato un Topojson, dove avevo bisogno di estrarre la funzione:

      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)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.