Zentrieren Sie eine Karte in d3 mit einem geoJSON-Objekt

140

Derzeit müssen Sie in d3 ein geoJSON-Objekt, das Sie zeichnen möchten, skalieren und übersetzen, um es auf die gewünschte Größe zu bringen, und es übersetzen, um es zu zentrieren. Dies ist eine sehr mühsame Aufgabe des Versuchs und Irrtums, und ich habe mich gefragt, ob jemand einen besseren Weg kennt, um diese Werte zu erhalten?

Also zum Beispiel, wenn ich diesen Code habe

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

Wie zum Teufel erhalte ich .scale (8500) und .translate ([0, -1200]), ohne nach und nach zu gehen?

Climboid
quelle
Siehe
Hugolpz

Antworten:

134

Das Folgende scheint ungefähr das zu tun, was Sie wollen. Die Skalierung scheint in Ordnung zu sein. Beim Anwenden auf meine Karte gibt es einen kleinen Versatz. Dieser kleine Versatz wird wahrscheinlich verursacht, weil ich den Befehl translate verwende, um die Karte zu zentrieren, während ich wahrscheinlich den Befehl center verwenden sollte.

  1. Erstellen Sie eine Projektion und d3.geo.path
  2. Berechnen Sie die Grenzen der aktuellen Projektion
  3. Verwenden Sie diese Grenzen, um den Maßstab und die Übersetzung zu berechnen
  4. Erstellen Sie die Projektion neu

In 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
quelle
9
Hey Jan van der Laan, ich habe dir nie für diese Antwort gedankt. Dies ist übrigens eine wirklich gute Antwort, wenn ich das Kopfgeld aufteilen könnte, das ich würde. Danke dafür!
Climboid
Wenn ich das anwende, bekomme ich Grenzen = unendlich. Irgendeine Idee, wie dies gelöst werden kann?
Simke Nys
@ SimkeNys Dies könnte das gleiche Problem sein wie hier erwähnt stackoverflow.com/questions/23953366/… Probieren Sie die dort erwähnte Lösung aus.
Jan van der Laan
1
Hallo Jan, danke für deinen Code. Ich habe Ihr Beispiel mit einigen GeoJson-Daten ausprobiert, aber es hat nicht funktioniert. Kannst du mir sagen, was ich falsch mache? :) Ich habe die GeoJson-Daten hochgeladen: onedrive.live.com/…
user2644964
1
In D3 v4 ist die Projektionsanpassung eine integrierte Methode: projection.fitSize([width, height], geojson)( API-Dokumente ) - siehe die Antwort von @dnltsk unten.
Florian Ledermann
173

Meine Antwort ist nahe an der von Jan van der Laan, aber Sie können die Dinge leicht vereinfachen, da Sie den geografischen Schwerpunkt nicht berechnen müssen. Sie brauchen nur den Begrenzungsrahmen. Mithilfe einer nicht skalierten, nicht übersetzten Einheitenprojektion können Sie die Mathematik vereinfachen.

Der wichtige Teil des Codes ist folgender:

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

Nachdem Sie den Begrenzungsrahmen des Features in der Einheitenprojektion berechnet haben, können Sie den entsprechenden Maßstab berechnen, indem Sie das Seitenverhältnis des Begrenzungsrahmens ( b[1][0] - b[0][0]und b[1][1] - b[0][1]) mit dem Seitenverhältnis der Leinwand ( widthund height) vergleichen. In diesem Fall habe ich den Begrenzungsrahmen auch auf 95% der Leinwand anstatt auf 100% skaliert, sodass an den Rändern etwas mehr Platz für Striche und umgebende Merkmale oder Polsterungen vorhanden ist.

Anschließend können Sie die Übersetzung anhand der Mitte des Begrenzungsrahmens ( (b[1][0] + b[0][0]) / 2und (b[1][1] + b[0][1]) / 2) und der Mitte der Leinwand ( width / 2und height / 2) berechnen . Da sich der Begrenzungsrahmen in den Koordinaten der Einheitsprojektion befindet, muss er mit der Skala ( s) multipliziert werden .

Zum Beispiel bl.ocks.org/4707858 :

Projekt zum Begrenzungsrahmen

Es gibt eine verwandte Frage, wo man auf ein bestimmtes Feature in einer Sammlung zoomen kann, ohne die Projektion anzupassen, dh die Projektion mit einer geometrischen Transformation zu kombinieren, um hinein- und herauszuzoomen. Dies verwendet die gleichen Prinzipien wie oben, aber die Mathematik unterscheidet sich geringfügig, da die geometrische Transformation (das SVG-Attribut "transform") mit der geografischen Projektion kombiniert wird.

Zum Beispiel bl.ocks.org/4699541 :

Zoom auf Begrenzungsrahmen

mbostock
quelle
2
Ich möchte darauf hinweisen, dass der obige Code einige Fehler enthält, insbesondere in den Indizes der Grenzen. Es sollte so aussehen: s = (0,95 / Math.max ((b [1] [0] - b [0] [0]) / Breite, (b [1] [1] - b [0] [0] ) / Höhe)) * 500, t = [(Breite - s * (b [1] [0] + b [0] [0])) / 2, (Höhe - s * (b [1] [1] + b [0] [1])) / 2];
iros
2
@iros - Sieht so aus, als wäre das * 500hier irrelevant ... b[1][1] - b[0][0]sollte auch b[1][1] - b[0][1]in der Skalenberechnung sein.
Nrabinowitz
2
Fehlerbericht
Paulo Scardine
5
Also: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
Aufgrund einer solchen Community ist es eine große Freude, mit D3 zu arbeiten. Genial!
Arunkjn
54

Mit d3 v4 oder v5 wird es viel einfacher!

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

und schlussendlich

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

Viel Spaß, Prost

dnltsk
quelle
2
Ich hoffe, dass diese Antwort mehr abgestimmt wird. Ich arbeite schon d3v4eine Weile mit und habe gerade diese Methode entdeckt.
Mark
2
Woher kommt gdas? Ist das der SVG-Container?
Tschallacka
1
Tschallacka gsollte <g></g>tag
giankotarola
1
Schade, das ist so weit unten und nach 2 Qualitätsantworten. Es ist leicht zu übersehen und es ist offensichtlich viel einfacher als die anderen Antworten.
Kurt
1
Danke dir. Funktioniert auch in v5!
Chaitanya Bangera
53

Ich bin neu in d3 - werde versuchen zu erklären, wie ich es verstehe, aber ich bin nicht sicher, ob ich alles richtig gemacht habe.

Das Geheimnis ist zu wissen, dass einige Methoden im kartografischen Raum (Breite, Länge) und andere im kartesischen Raum (x, y auf dem Bildschirm) funktionieren. Der kartografische Raum (unser Planet) ist (fast) sphärisch, der kartesische Raum (Bildschirm) ist flach - um einen übereinander abzubilden, benötigen Sie einen Algorithmus, der als Projektion bezeichnet wird . Dieser Raum ist zu kurz, um tief in das faszinierende Thema der Projektionen einzudringen und wie sie geografische Merkmale verzerren, um sphärisch in eben zu verwandeln. Einige sind so konzipiert, dass Winkel erhalten bleiben, andere Entfernungen usw. - es gibt immer einen Kompromiss (Mike Bostock hat eine riesige Sammlung von Beispielen ).

Geben Sie hier die Bildbeschreibung ein

In d3 hat das Projektionsobjekt eine Center-Eigenschaft / einen Setter, die in Karteneinheiten angegeben ist:

projection.center ([Ort])

Wenn Mitte angegeben ist, wird die Mitte der Projektion auf die angegebene Position festgelegt, ein Array mit zwei Elementen aus Längen- und Breitengrad in Grad, und die Projektion wird zurückgegeben. Wenn Mitte nicht angegeben ist, wird die aktuelle Mitte zurückgegeben, die standardmäßig ⟨0 °, 0 °⟩ ist.

Es gibt auch die Übersetzung in Pixel - wobei das Projektionszentrum relativ zur Leinwand steht:

projection.translate ([point])

Wenn point angegeben ist, wird der Translationsversatz der Projektion auf das angegebene Array mit zwei Elementen [x, y] festgelegt und die Projektion zurückgegeben. Wenn point nicht angegeben ist, wird der aktuelle Übersetzungsoffset zurückgegeben, der standardmäßig [480, 250] ist. Der Translationsversatz bestimmt die Pixelkoordinaten des Projektionszentrums. Der Standard-Translationsversatz platziert ⟨0 °, 0 °⟩ in der Mitte eines 960 × 500-Bereichs.

Wenn ich ein Feature auf der Leinwand zentrieren möchte, möchte ich das Projektionszentrum auf die Mitte des Feature-Begrenzungsrahmens setzen. Dies funktioniert bei Verwendung von mercator (WGS 84, in Google Maps verwendet) für mein Land (Brasilien). nie mit anderen Vorsprüngen und Halbkugeln getestet. Möglicherweise müssen Sie Anpassungen für andere Situationen vornehmen, aber wenn Sie diese Grundprinzipien festlegen, wird es Ihnen gut gehen.

Beispiel: Projektion und Pfad:

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

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

Die boundsMethode von pathgibt den Begrenzungsrahmen in Pixel zurück . Verwenden Sie diese Option, um den richtigen Maßstab zu finden, indem Sie die Größe in Pixel mit der Größe in Karteneinheiten vergleichen (0,95 ergibt einen Rand von 5% gegenüber der besten Anpassung für Breite oder Höhe). Grundlegende Geometrie hier, Berechnung der Rechteckbreite / -höhe bei diagonal gegenüberliegenden Ecken:

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

Geben Sie hier die Bildbeschreibung ein

Verwenden Sie die d3.geo.boundsMethode, um den Begrenzungsrahmen in Karteneinheiten zu finden:

b = d3.geo.bounds(feature);

Stellen Sie die Mitte der Projektion auf die Mitte des Begrenzungsrahmens ein:

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

Verwenden Sie die translateMethode, um die Mitte der Karte in die Mitte der Leinwand zu verschieben:

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

Inzwischen sollte die Funktion in der Mitte der Karte mit einem Rand von 5% gezoomt sein.

Paulo Scardine
quelle
Gibt es irgendwo einen Block?
Hugolpz
Sorry, keine Blöcke oder Kern, was versuchst du zu tun? Ist es so etwas wie ein Klick zum Zoomen? Veröffentlichen Sie es und ich kann mir Ihren Code ansehen.
Paulo Scardine
Bostocks Antwort und Bilder enthalten Links zu bl.ocks.org-Beispielen, mit denen ich einen ganzen Code kopieren kann. Job erledigt. +1 und danke für deine tollen Illustrationen!
Hugolpz
4

Es gibt eine center () -Methode, die ein Lat / Lon-Paar akzeptiert.

Soweit ich weiß, wird translate () nur verwendet, um die Pixel der Karte buchstäblich zu verschieben. Ich bin mir nicht sicher, wie ich den Maßstab bestimmen soll.

Dave Foster
quelle
8
Wenn Sie TopoJSON verwenden und die gesamte Karte zentrieren möchten, können Sie topojson mit --bbox ausführen, um ein bbox-Attribut in das JSON-Objekt aufzunehmen. Die Lat / Lon-Koordinaten für das Zentrum sollten [(b [0] + b [2]) / 2, (b [1] + b [3]) / 2] sein (wobei b der bbox-Wert ist).
Paulo Scardine
3

Neben Zentrum eine Karte in d3 ein GeoJSON Objekt gegeben , beachten Sie, dass Sie es vorziehen , können fitExtent()über , fitSize()wenn Sie eine Polsterung um die Grenzen des Objekts angeben möchten. fitSize()setzt diese Auffüllung automatisch auf 0.

Sylvain Lesage
quelle
2

Ich habe mich im Internet nach einer unkomplizierten Möglichkeit umgesehen, meine Karte zu zentrieren, und mich von Jan van der Laan und der Antwort von mbostock inspirieren lassen. Hier ist eine einfachere Möglichkeit, jQuery zu verwenden, wenn Sie einen Container für das SVG verwenden. Ich habe einen Rand von 95% für Polsterung / Ränder usw. erstellt.

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

Wenn Sie nach einer genauen Skalierung suchen, funktioniert diese Antwort bei Ihnen nicht. Wenn Sie jedoch wie ich eine Karte anzeigen möchten, die in einem Container zentralisiert ist, sollte dies ausreichen. Ich habe versucht, die Mercator-Karte anzuzeigen, und festgestellt, dass diese Methode bei der Zentralisierung meiner Karte hilfreich ist und ich den antarktischen Teil leicht abschneiden kann, da ich ihn nicht benötige.

Wei Hao
quelle
1

Um die Karte zu schwenken / zu zoomen, sollten Sie die SVG-Datei auf der Broschüre überlagern. Das ist viel einfacher als die Transformation der SVG. Sehen Sie sich dieses Beispiel an: http://bost.ocks.org/mike/leaflet/ und dann So ändern Sie das Kartenzentrum in der Broschüre

Kristofor Carle
quelle
Wenn das Hinzufügen einer weiteren Abhängigkeit von Belang ist, können PAN und ZOOM problemlos in reinem d3 ausgeführt werden, siehe stackoverflow.com/questions/17093614/…
Paulo Scardine
Diese Antwort befasst sich nicht wirklich mit d3. Sie können die Karte auch in d3 schwenken / zoomen, eine Broschüre ist nicht erforderlich. (
Ich habe
0

Mit der Antwort von mbostocks und dem Kommentar von Herb Caudill stieß ich auf Probleme mit Alaska, da ich eine Mercator-Projektion verwendete. Ich sollte beachten, dass ich für meine eigenen Zwecke versuche, US-Staaten zu projizieren und zu zentrieren. Ich stellte fest, dass ich die beiden Antworten mit der Antwort von Jan van der Laan heiraten musste, mit der folgenden Ausnahme für Polygone, die Hemisphären überlappen (Polygone, die einen absoluten Wert für Ost - West haben, der größer als 1 ist):

  1. Richten Sie eine einfache Projektion in mercator ein:

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

  2. Erstellen Sie den Pfad:

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

3. Richten Sie meine Grenzen ein:

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. Fügen Sie eine Ausnahme für Alaska und Staaten hinzu, die die Hemisphären überlappen:

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

Ich hoffe das hilft.

DimeZilla
quelle
0

Für Leute, die vertikal und horizontal einstellen möchten, ist hier die Lösung:

  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
quelle
0

Wie ich einen Topojson zentriert habe, wo ich das Feature herausziehen musste:

      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)
Statistiken anhand eines Beispiels lernen
quelle