web-dev-qa-db-fra.com

Centrer une carte en d3 avec un objet geoJSON

Actuellement en d3, si vous avez dessiné un objet geoJSON, vous devez le redimensionner et le traduire afin d'obtenir la taille souhaitée et 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 puis-je obtenir .scale (8500) et .translate ([0, -1200]) sans y aller petit à petit?

133
climboid

Ce qui suit semble faire à peu près ce que vous voulez. La mise à l'échelle semble être ok. Lorsque vous l'appliquez à 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. Calculer les limites de la projection actuelle
  3. Utilisez ces limites pour calculer l’échelle et la translation
  4. Recréer 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")
    });
124
Jan van der Laan

Ma réponse est proche de celle de Jan van der Laan, mais vous pouvez simplifier un peu les choses car vous n’avez pas besoin de calculer le centroïde géographique; vous avez seulement besoin du cadre de sélection. Et, en utilisant une projection d’unités non échelonnée 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é la fonction cadre de sélection dans la projection d'unités, vous pouvez calculer l'échelle appropriée en comparant le rapport de format de la délimitation. boîte (b[1][0] - b[0][0] et b[1][1] - b[0][1]) aux proportions de la toile (width et height). Dans ce cas, j’ai également redimensionné le cadre de sélection à 95% de la toile, au lieu de 100%, de sorte qu’il reste un peu de marge supplémentaire sur les bords pour les traits et les éléments ou le rembourrage environnants.

Ensuite, vous pouvez calculer la traduction en utilisant le centre du cadre de sélection ((b[1][0] + b[0][0]) / 2 et (b[1][1] + b[0][1]) / 2) et le centre de la toile (width / 2 et height / 2). Notez que, comme le cadre de sélection est dans les coordonnées de la projection unitaire, il doit être multiplié par l’échelle (s).

Par exemple, bl.ocks.org/4707858 :

project to bounding box

Il existe une question connexe: comment zoomer sur une fonction spécifique d’une collection sans ajuster la projection, c’est-à-dire, en combinant la projection avec une transformation géométrique pour effectuer un zoom avant ou 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 "transformer" SVG) est combinée à la projection géographique.

Par exemple, bl.ocks.org/4699541 :

zoom to bounding box

164
mbostock

Je suis nouveau sur d3 - je vais essayer d’expliquer comment je le comprends mais je ne suis pas sûr d’avoir tout 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 à l'écran). L’espace cartographique (notre planète) est (presque) sphérique, l’espace cartésien (écran) est plat - pour pouvoir mapper l’un sur l’autre, vous avez besoin d’un algorithme, appelé projection . Cet espace est trop court pour plonger dans le sujet fascinant des projections et de la façon dont elles déforment les caractéristiques géographiques afin de transformer des sphères 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 un énorme collection d'exemples ).

enter image description here

Dans d3, l’objet de projection a une propriété de centre/setter, exprimée en unités de carte:

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 dont la valeur par défaut est ⟨0 °, 0 °.

Il y a aussi la traduction, donnée en pixels - où le centre de projection se situe par rapport à la toile:

projection.translate ([point])

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

Lorsque je veux centrer une caractéristique dans la toile, j'aime bien définir le centre de la projection au centre du cadre de sélection - cela fonctionne pour moi lorsque mercator (WGS 84, utilisé dans Google Maps) pour mon pays (Brésil), jamais testé avec d’autres projections et hémisphères. Vous devrez peut-être faire des ajustements pour d’autres situations, mais si vous définissez 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 méthode bounds de path renvoie le cadre de sélection en pixels . Utilisez-le pour trouver la bonne échelle, en comparant la taille en pixels à la taille en unités de la carte (0,95 vous donne une marge de 5% par rapport au meilleur ajustement en largeur ou en hauteur). Géométrie de base ici, calculant la largeur/hauteur du rectangle en prenant des angles opposés en diagonale:

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

enter image description here

Utilisez le d3.geo.bounds méthode pour trouver le cadre de sélection en unités de la carte:

b = d3.geo.bounds(feature);

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

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

Utilisez la méthode translate pour déplacer le centre de la carte au centre du canevas:

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

Vous devriez maintenant avoir la fonctionnalité au centre de la carte zoomée avec une marge de 5%.

49
Paulo Scardine

Avec d3 v4, c'est de plus en 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");

Profitez, à la vôtre

42
dnltsk

Il existe une méthode center () que vous pouvez utiliser qui accepte une paire lat/lon.

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

4
Dave Foster

Je cherchais sur Internet un moyen facile 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/les 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 constaté que cette méthode était utile pour centraliser ma carte et que je pouvais facilement couper la partie antarctique, car je n'en avais pas besoin.

2
Wei Hao

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

2
Sylvain Lesage

Pour faire un panoramique/zoom de la carte, vous devriez 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/ et ensuite Comment changer le centre de la carte dans la brochure

1
Kristofor Carle

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 de Mercator. Je devrais 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 Jan van der Laan avec l'exception suivante pour les polygones qui chevauchent des hémisphères (les polygones qui se terminent par une valeur absolue supérieure à 1 pour Est-Ouest):

  1. mettre en place une projection simple dans mercator:

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

  2. créer le chemin:

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

3. établir 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.Ajouter 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.

0
DimeZilla

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

Comment j'ai centré un Topojson, où je devais sortir 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)