Centrer une carte en d3 à partir d'un objet geoJSON

140

Actuellement en d3, si vous avez un objet geoJSON que vous allez dessiner, vous devez le mettre à l'échelle et le traduire afin de l'obtenir à la taille souhaitée et de le traduire afin de le centrer. C'est une tâche très fastidieuse d'essais et d'erreurs, et je me demandais si quelqu'un connaissait un meilleur moyen d'obtenir ces valeurs?

Donc par exemple si j'ai ce code

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

Comment diable puis-je obtenir .scale (8500) et .translate ([0, -1200]) sans aller petit à petit?

climboïde
la source
Voir
Hugolpz

Réponses:

134

Ce qui suit semble faire à peu près ce que vous voulez. La mise à l'échelle semble être correcte. Lors de son application à ma carte, il y a un petit décalage. Ce petit décalage est probablement dû au fait que j'utilise la commande translate pour centrer la carte, alors que je devrais probablement utiliser la commande center.

  1. Créer une projection et d3.geo.path
  2. Calculez les limites de la projection actuelle
  3. Utilisez ces limites pour calculer l'échelle et la translation
  4. Recréez la projection

Dans du code:

  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
la source
9
Hey Jan van der Laan Je ne vous ai jamais remercié pour cette réponse. C'est une très bonne réponse au fait si je pouvais diviser la prime que je le ferais. Merci!
climboid
Si j'applique cela, j'obtiens des bornes = l'infini. Une idée sur la façon dont cela peut être résolu?
Simke Nys
@SimkeNys Cela pourrait être le même problème que celui mentionné ici stackoverflow.com/questions/23953366/... Essayez la solution mentionnée ici.
Jan van der Laan
1
Salut Jan, merci pour ton code. J'ai essayé votre exemple avec des données GeoJson mais cela n'a pas fonctionné. Pouvez-vous me dire ce que je fais de mal? :) J'ai téléchargé les données GeoJson: onedrive.live.com/…
user2644964
1
Dans D3 v4, l'ajustement de projection est une méthode intégrée: projection.fitSize([width, height], geojson)( docs API ) - voir la réponse de @dnltsk ci-dessous.
Florian Ledermann
173

Ma réponse est proche de celle de Jan van der Laan, mais vous pouvez simplifier légèrement les choses car vous n'avez pas besoin de calculer le centre de gravité géographique; vous n'avez besoin que du cadre de sélection. Et, en utilisant une projection unitaire non mise à l'échelle et non traduite, vous pouvez simplifier les calculs.

La partie importante du code est la suivante:

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

Après avoir compilé le cadre de délimitation de l'entité dans la projection unitaire, vous pouvez calculer l' échelle appropriée en comparant le rapport hauteur / largeur du cadre englobant ( b[1][0] - b[0][0]et b[1][1] - b[0][1]) au rapport hauteur / largeur du canevas ( widthet height). Dans ce cas, j'ai également mis à l'échelle le cadre de délimitation à 95% du canevas, plutôt qu'à 100%, il y a donc un peu plus de place sur les bords pour les traits et les caractéristiques environnantes ou le remplissage.

Ensuite, vous pouvez calculer la translation en utilisant le centre du cadre de sélection ( (b[1][0] + b[0][0]) / 2et (b[1][1] + b[0][1]) / 2) et le centre du canevas ( width / 2et height / 2). Notez que puisque la boîte englobante est dans les coordonnées de la projection unitaire, elle doit être multipliée par l'échelle ( s).

Par exemple, bl.ocks.org/4707858 :

projet vers le cadre englobant

Il y a une question connexe: comment zoomer sur une caractéristique spécifique d'une collection sans ajuster la projection, c'est -à- dire combiner la projection avec une transformation géométrique pour effectuer un zoom avant et arrière. Cela utilise les mêmes principes que ci-dessus, mais le calcul est légèrement différent car la transformation géométrique (l'attribut SVG "transform") est combinée avec la projection géographique.

Par exemple, bl.ocks.org/4699541 :

zoom sur le cadre de sélection

mbostock
la source
2
Je tiens à souligner qu'il y a quelques erreurs dans le code ci-dessus, en particulier dans les indices des limites. Cela devrait ressembler à: s = (0,95 / Math.max ((b [1] [0] - b [0] [0]) / largeur, (b [1] [1] - b [0] [0] ) / hauteur)) * 500, t = [(largeur - s * (b [1] [0] + b [0] [0])) / 2, (hauteur - s * (b [1] [1] + b [0] [1])) / 2];
iros
2
@iros - On dirait que * 500c'est étranger ici ... aussi, b[1][1] - b[0][0]devrait être b[1][1] - b[0][1]dans le calcul de l'échelle.
nrabinowitz
2
Rapport de bogue: meta.stackexchange.com/questions/184140/…
Paulo Scardine
5
Donc: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
C'est à cause d'une communauté comme celle-ci que D3 est une telle joie de travailler avec. Impressionnant!
arunkjn
55

Avec d3 v4 ou v5, cela devient beaucoup plus facile!

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

et enfin

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

Appréciez, Cheers

dnltsk
la source
2
J'espère que cette réponse sera votée davantage. Je travaille avec d3v4depuis un moment et je viens de découvrir cette méthode.
Mark
2
D'où gvient-il? Est-ce le conteneur svg?
Tschallacka
1
Tschallacka gdevrait être <g></g>tag
giankotarola
1
Dommage que ce soit si loin et après 2 réponses de qualité. Il est facile de rater cela et c'est évidemment beaucoup plus simple que les autres réponses.
Kurt
1
Je vous remercie. Fonctionne aussi dans la v5!
Chaitanya Bangera
53

Je suis nouveau dans d3 - je vais essayer d'expliquer comment je le comprends mais je ne suis pas sûr d'avoir tout bien compris.

Le secret est de savoir que certaines méthodes opéreront sur l'espace cartographique (latitude, longitude) et d'autres sur l'espace cartésien (x, y sur l'écran). L'espace cartographique (notre planète) est (presque) sphérique, l'espace cartésien (écran) est plat - pour cartographier l'un sur l'autre, vous avez besoin d'un algorithme appelé projection . Cet espace est trop court pour approfondir le sujet fascinant des projections et comment elles déforment les caractéristiques géographiques afin de transformer sphérique en plan; certains sont conçus pour conserver les angles, d'autres pour conserver les distances et ainsi de suite - il y a toujours un compromis (Mike Bostock a une énorme collection d'exemples ).

entrez la description de l'image ici

En d3, l'objet de projection a une propriété / setter center, donnée en unités cartographiques:

projection.center ([emplacement])

Si center est spécifié, définit le centre de la projection sur l'emplacement spécifié, un tableau à deux éléments de longitude et de latitude en degrés et renvoie la projection. Si le centre n'est pas spécifié, renvoie le centre actuel qui est par défaut ⟨0 °, 0 °⟩.

Il y a aussi la translation, donnée en pixels - où le centre de projection se situe par rapport au canevas:

projection.translate ([point])

Si le point est spécifié, définit le décalage de translation de la projection sur le tableau à deux éléments spécifié [x, y] et renvoie la projection. Si le point n'est pas spécifié, renvoie le décalage de traduction actuel qui est par défaut [480, 250]. Le décalage de translation détermine les coordonnées en pixels du centre de la projection. Le décalage de translation par défaut place ⟨0 °, 0 °⟩ au centre d'une zone 960 × 500.

Lorsque je souhaite centrer une entité dans le canevas, j'aime définir le centre de projection au centre de la zone de délimitation de l'entité - cela fonctionne pour moi lorsque j'utilise mercator (WGS 84, utilisé dans Google Maps) pour mon pays (Brésil), jamais testé en utilisant d'autres projections et hémisphères. Vous devrez peut-être faire des ajustements pour d'autres situations, mais si vous appliquez ces principes de base, tout ira bien.

Par exemple, étant donné une projection et un chemin:

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

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

La boundsméthode from pathrenvoie le cadre de sélection en pixels . Utilisez-le pour trouver la bonne échelle, en comparant la taille en pixels avec la taille en unités de la carte (0,95 vous donne une marge de 5% sur le meilleur ajustement pour la largeur ou la hauteur). Géométrie de base ici, calculant la largeur / hauteur du rectangle en fonction des coins diagonalement opposés:

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

entrez la description de l'image ici

Utilisez la d3.geo.boundsméthode pour trouver la boîte englobante en unités de carte:

b = d3.geo.bounds(feature);

Définissez le centre de la projection sur le centre du cadre de sélection:

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

Utilisez la translateméthode pour déplacer le centre de la carte vers le centre du canevas:

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

Vous devriez maintenant avoir la fonction au centre de la carte agrandie avec une marge de 5%.

Paulo Scardine
la source
Y a-t-il des blocs quelque part?
Hugolpz
Désolé, pas de blocages ni d’essentiel, qu’essayez-vous de faire? Est-ce quelque chose comme un clic pour zoomer? Publiez-le et je pourrai jeter un œil à votre code.
Paulo Scardine
La réponse et les images de Bostock fournissent des liens vers des exemples de bl.ocks.org qui me permettent de copier tout un code. Travail accompli. +1 et merci pour vos superbes illustrations!
Hugolpz
4

Il y a un centre () méthode , vous pouvez utiliser une paire qui accepte latitude / longitude.

D'après ce que je comprends, translate () n'est utilisé que pour déplacer littéralement les pixels de la carte. Je ne sais pas comment déterminer quelle est l'échelle.

Dave Foster
la source
8
Si vous utilisez TopoJSON et souhaitez centrer la carte entière, vous pouvez exécuter topojson avec --bbox pour inclure un attribut bbox dans l'objet JSON. Les coordonnées lat / lon du centre doivent être [(b [0] + b [2]) / 2, (b [1] + b [3]) / 2] (où b est la valeur bbox).
Paulo Scardine
3

En plus de Centrer une carte dans d3 à partir d'un objet geoJSON , notez que vous pouvez préférer fitExtent()plutôt que fitSize()si vous souhaitez spécifier un remplissage autour des limites de votre objet. fitSize()définit automatiquement ce remplissage à 0.

Sylvain Lesage
la source
2

Je cherchais sur Internet un moyen simple de centrer ma carte et je me suis inspiré de la réponse de Jan van der Laan et de mbostock. Voici un moyen plus simple d'utiliser jQuery si vous utilisez un conteneur pour le svg. J'ai créé une bordure de 95% pour le rembourrage / bordures, 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 vous recherchez une mise à l'échelle exacte, cette réponse ne fonctionnera pas pour vous. Mais si comme moi, vous souhaitez afficher une carte qui se centralise dans un conteneur, cela devrait suffire. J'essayais d'afficher la carte Mercator et j'ai trouvé que cette méthode était utile pour centraliser ma carte, et je pouvais facilement couper la partie Antarctique puisque je n'en avais pas besoin.

Wei Hao
la source
1

Pour faire un panoramique / zoomer la carte, vous devez regarder superposer le SVG sur Leaflet. Ce sera beaucoup plus facile que de transformer le SVG. Voir cet exemple http://bost.ocks.org/mike/leaflet/ puis Comment changer le centre de la carte dans le dépliant

Kristofor Carle
la source
Si l'ajout d'une autre dépendance est préoccupant, PAN et ZOOM peuvent être effectués facilement en d3 pur, voir stackoverflow.com/questions/17093614/...
Paulo Scardine
Cette réponse ne traite pas vraiment de d3. Vous pouvez également faire un panoramique / zoomer sur la carte en d3, le dépliant n'est pas nécessaire. (Je viens de réaliser que c'est un ancien message, je parcourais juste les réponses)
JARRRRG
0

Avec la réponse de mbostocks et le commentaire de Herb Caudill, j'ai commencé à rencontrer des problèmes avec l'Alaska depuis que j'utilisais une projection Mercator. Je dois noter que pour mes propres besoins, j'essaie de projeter et de centrer les États américains. J'ai trouvé que je devais marier les deux réponses avec la réponse de Jan van der Laan à l'exception suivante pour les polygones qui chevauchent des hémisphères (polygones qui finissent avec une valeur absolue pour Est - Ouest supérieure à 1):

  1. configurer une projection simple dans mercator:

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

  2. créer le chemin:

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

3. fixer mes 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.Ajoutez une exception pour l'Alaska et les États qui chevauchent les hémisphères:

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

J'espère que ça aide.

DimeZilla
la source
0

Pour les personnes qui souhaitent ajuster verticalement et horizontalement, voici la solution:

  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
la source
0

Comment j'ai centré un Topojson, où je devais retirer la fonctionnalité:

      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)
Apprentissage des statistiques par exemple
la source