Wyśrodkuj mapę w d3, biorąc pod uwagę obiekt geoJSON

140

Obecnie w d3, jeśli masz obiekt geoJSON, który zamierzasz narysować, musisz go przeskalować i przetłumaczyć, aby uzyskać żądany rozmiar i przetłumaczyć, aby go wyśrodkować. Jest to bardzo żmudne zadanie metodą prób i błędów i zastanawiałem się, czy ktoś zna lepszy sposób uzyskania tych wartości?

Na przykład, jeśli mam ten kod

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

Jak do cholery mam uzyskać .scale (8500) i .translate ([0, -1200]) bez przechodzenia po trochu?

climboid
źródło

Odpowiedzi:

134

Poniższe wydaje się robić mniej więcej to, co chcesz. Skalowanie wydaje się być w porządku. Podczas nakładania go na moją mapę występuje małe przesunięcie. To małe przesunięcie jest prawdopodobnie spowodowane tym, że używam polecenia translate, aby wyśrodkować mapę, podczas gdy powinienem prawdopodobnie użyć polecenia center.

  1. Utwórz rzut i ścieżkę d3.geo.path
  2. Oblicz granice bieżącej projekcji
  3. Użyj tych granic, aby obliczyć skalę i tłumaczenie
  4. Odtwórz projekcję

W kodzie:

  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
źródło
9
Hej Jan van der Laan, nigdy nie podziękowałem ci za tę odpowiedź. Nawiasem mówiąc, to naprawdę dobra odpowiedź, gdybym mógł podzielić nagrodę, którą bym zrobił. Dziękuję za to!
climboid
Jeśli zastosuję to, otrzymam bounds = nieskończoność. Masz jakiś pomysł, jak można to rozwiązać?
Simke Nys,
@SimkeNys To może być ten sam problem, co wspomniany tutaj stackoverflow.com/questions/23953366/… Wypróbuj rozwiązanie tam wymienione.
Jan van der Laan
1
Cześć Jan, dziękuję za twój kod. Wypróbowałem twój przykład z niektórymi danymi GeoJson, ale nie zadziałał. Możesz mi powiedzieć, co robię źle? :) Wgrałem dane GeoJson: onedrive.live.com/…
user2644964
1
W D3 v4 dopasowanie projekcji jest metodą wbudowaną: projection.fitSize([width, height], geojson)( dokumentacja API ) - zobacz odpowiedź @dnltsk poniżej.
Florian Ledermann
173

Moja odpowiedź jest podobna do odpowiedzi Jana van der Laana, ale możesz nieco uprościć sprawę, ponieważ nie musisz obliczać geograficznego centroidu; potrzebujesz tylko obwiedni. Używając nieskalowanej, nieprzetłumaczonej projekcji jednostek, możesz uprościć obliczenia.

Ważną częścią kodu jest to:

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

Po obliczeniu obwiedni obiektu w rzucie jednostkowym można obliczyć odpowiednią skalę , porównując współczynnik kształtu obwiedni ( b[1][0] - b[0][0]i b[1][1] - b[0][1]) ze współczynnikiem kształtu obszaru roboczego ( widthi height). W tym przypadku przeskalowałem również obwiednię do 95% płótna, a nie 100%, więc na krawędziach jest trochę więcej miejsca na pociągnięcia i otaczające elementy lub wypełnienie.

Następnie możesz obliczyć tłumaczenie, używając środka obwiedni ( (b[1][0] + b[0][0]) / 2i (b[1][1] + b[0][1]) / 2) i środka płótna ( width / 2i height / 2). Zwróć uwagę, że skoro obwiednia znajduje się we współrzędnych odwzorowania jednostek, musi zostać pomnożona przez skalę ( s).

Na przykład bl.ocks.org/4707858 :

rzut do obwiedni

Istnieje pokrewne pytanie, w jaki sposób powiększyć określoną funkcję w kolekcji bez dostosowywania odwzorowania, tj. Połączyć rzutowanie z transformacją geometryczną w celu powiększenia i zmniejszenia. Wykorzystuje to te same zasady co powyżej, ale matematyka jest nieco inna, ponieważ transformacja geometryczna (atrybut SVG „transform”) jest połączona z odwzorowaniem geograficznym.

Na przykład bl.ocks.org/4699541 :

powiększ do obwiedni

mbostock
źródło
2
Chcę zwrócić uwagę, że w powyższym kodzie jest kilka błędów, szczególnie w indeksach granic. Powinien wyglądać następująco: s = (0.95 / Math.max ((b [1] [0] - b [0] [0]) / width, (b [1] [1] - b [0] [0] ) / wysokość)) * 500, t = [(szerokość - s * (b [1] [0] + b [0] [0])) / 2, (wysokość - s * (b [1] [1]) + b [0] [1])) / 2];
iros
2
@iros - Wygląda na * 500to, że jest tu obcy ... również b[1][1] - b[0][0]powinien być uwzględniony b[1][1] - b[0][1]w obliczeniach skali.
nrabinowitz
2
Raport o błędzie
Paulo Scardine
5
A więc: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
To właśnie z powodu takiej społeczności praca z D3 to ogromna przyjemność. Niesamowite!
arunkjn
54

Z d3 v4 lub v5 staje się o wiele łatwiejsze!

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

i w końcu

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

Ciesz się, na zdrowie

dnltsk
źródło
2
Mam nadzieję, że ta odpowiedź będzie bardziej głosowana. Pracowałem d3v4przez jakiś czas i właśnie odkryłem tę metodę.
Mark
2
Skąd się gbierze? Czy to jest kontener SVG?
Tschallacka
1
Tschallacka gpowinna być <g></g>tagiem
giankotarola
1
Szkoda, że ​​jest tak daleko i po 2 jakościowych odpowiedziach. Łatwo to przeoczyć i jest oczywiście o wiele prostsze niż inne odpowiedzi.
Kurt
1
Dziękuję Ci. Działa też w wersji 5!
Chaitanya Bangera
53

Jestem nowy w d3 - spróbuję wyjaśnić, jak to rozumiem, ale nie jestem pewien, czy wszystko jest w porządku.

Sekret tkwi w świadomości, że niektóre metody będą działać na przestrzeni kartograficznej (szerokość, długość), a inne na przestrzeni kartezjańskiej (x, y na ekranie). Przestrzeń kartograficzna (nasza planeta) jest (prawie) sferyczna, przestrzeń kartezjańska (ekran) jest płaska - do odwzorowania jednej na drugiej potrzebny jest algorytm, który nazywa się projekcją . Przestrzeń ta jest zbyt krótka, aby zagłębić się w fascynujący temat projekcji i tego, jak zniekształcają one cechy geograficzne, aby zmienić sferę w płaszczyznę; niektóre mają na celu zachowanie kątów, inne zachowanie odległości itd. - zawsze istnieje kompromis (Mike Bostock ma ogromny zbiór przykładów ).

wprowadź opis obrazu tutaj

W d3 rzutowany obiekt ma właściwość środkową / ustawiającą, podaną w jednostkach mapy:

projection.center ([lokalizacja])

Jeśli określono środek, ustawia środek odwzorowania na określone położenie, dwuelementową tablicę długości i szerokości geograficznej w stopniach i zwraca rzut. Jeśli środek nie jest określony, zwraca bieżące centrum, które domyślnie wynosi ⟨0 °, 0 °⟩.

Jest też tłumaczenie, podane w pikselach - gdzie środek projekcji stoi względem płótna:

projection.translate ([punkt])

Jeśli określono punkt, ustawia przesunięcie przesunięcia odwzorowania na określoną dwuelementową tablicę [x, y] i zwraca rzut. Jeśli nie określono punktu, zwraca bieżące przesunięcie tłumaczenia, które domyślnie wynosi [480, 250]. Przesunięcie przesunięcia określa współrzędne pikseli środka projekcji. Domyślne przesunięcie przesunięcia umieszcza ⟨0 °, 0 °⟩ w ​​środku obszaru 960 × 500.

Kiedy chcę wyśrodkować obiekt na płótnie, lubię ustawić środek projekcji na środek obwiedni obiektu - to działa, gdy używam mercator (WGS 84, używany w Google Maps) dla mojego kraju (Brazylia), nigdy nie testowano przy użyciu innych wypustów i półkul. Być może będziesz musiał wprowadzić poprawki w innych sytuacjach, ale jeśli zastosujesz się do tych podstawowych zasad, wszystko będzie dobrze.

Na przykład, biorąc pod uwagę rzut i ścieżkę:

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

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

boundsMetoda od pathzwrotów obwiedni w pikselach . Użyj go, aby znaleźć odpowiednią skalę, porównując rozmiar w pikselach z rozmiarem w jednostkach mapy (0,95 daje 5% margines nad najlepszym dopasowaniem szerokości lub wysokości). Tutaj podstawowa geometria, obliczająca szerokość / wysokość prostokąta dla przeciwległych narożników po przekątnej:

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

wprowadź opis obrazu tutaj

Użyj d3.geo.boundsmetody, aby znaleźć obwiednię w jednostkach mapy:

b = d3.geo.bounds(feature);

Ustaw środek rzutowania na środek obwiedni:

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

Użyj tej translatemetody, aby przenieść środek mapy na środek obszaru roboczego:

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

Do tej pory powinieneś mieć powiększoną funkcję w środku mapy z 5% marginesem.

Paulo Scardine
źródło
Czy jest gdzieś bloki?
Hugolpz
Przepraszam, bez bloków lub sedna, co próbujesz zrobić? Czy to coś w rodzaju kliknięcia, aby powiększyć? Opublikuj to, a ja przyjrzę się Twojemu kodowi.
Paulo Scardine
Odpowiedź i obrazy Bostock zawierają linki do przykładów bl.ocks.org, które pozwalają mi skopiować cały kod inżynierowi. Zadanie wykonane. +1 i dziękujemy za wspaniałe ilustracje!
Hugolpz
4

Istnieje metoda center () , której możesz użyć, która akceptuje parę lat / lon.

Z tego, co rozumiem, translate () służy tylko do dosłownego przesuwania pikseli mapy. Nie wiem, jak określić, jaka to skala.

Dave Foster
źródło
8
Jeśli używasz TopoJSON i chcesz wyśrodkować całą mapę, możesz uruchomić topojson z --bbox, aby dołączyć atrybut bbox do obiektu JSON. Współrzędne szerokości / długości środka powinny wynosić [(b [0] + b [2]) / 2, (b [1] + b [3]) / 2] (gdzie b jest wartością bbox).
Paulo Scardine
3

Poza Centrum mapa w d3 podany obiekt GeoJSON pamiętać, że może wolisz fitExtent()ponad fitSize()jeśli chcesz określić wyściółkę wokół granicach swojego przedmiotu. fitSize()automatycznie ustawia to wypełnienie na 0.

Sylvain Lesage
źródło
2

Szukałem w Internecie bezproblemowego sposobu na wyśrodkowanie mapy i zainspirowała mnie odpowiedź Jana van der Laana i mbostocka. Oto łatwiejszy sposób korzystania z jQuery, jeśli używasz kontenera na svg. Utworzyłem granicę 95% dla dopełnienia / obramowania itp.

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

Jeśli szukasz dokładnego skalowania, ta odpowiedź nie zadziała. Ale jeśli tak jak ja chcesz wyświetlić mapę, która jest centralizowana w kontenerze, to powinno wystarczyć. Próbowałem wyświetlić mapę mercator i odkryłem, że ta metoda była przydatna w centralizacji mojej mapy i mogłem łatwo odciąć część Antarktydy, ponieważ jej nie potrzebowałem.

Wei Hao
źródło
1

Aby przesunąć / powiększyć mapę, należy spojrzeć na nakładkę SVG na Leaflet. To będzie o wiele łatwiejsze niż przekształcenie SVG. Zobacz ten przykład http://bost.ocks.org/mike/leaflet/, a następnie Jak zmienić środek mapy w ulotce

Kristofor Carle
źródło
Jeśli dodawanie kolejnej zależności jest problemem, PAN i ZOOM można łatwo wykonać w czystym d3, patrz stackoverflow.com/questions/17093614/ ...
Paulo Scardine
Ta odpowiedź tak naprawdę nie dotyczy d3. Możesz także przesuwać / powiększać mapę w d3, ulotka nie jest potrzebna. (Właśnie zdałem sobie sprawę, że to stary post, właśnie przeglądałem odpowiedzi)
JARRRRG
0

Po odpowiedzi mbostocks i komentarzu Herba Caudilla zacząłem mieć problemy z Alaską, odkąd użyłem projekcji mercator. Powinienem zauważyć, że dla własnych celów staram się projektować i skupiać Stany Zjednoczone. Okazało się, że muszę poślubić te dwie odpowiedzi z odpowiedzią Jana van der Laana z następującym wyjątkiem dla wielokątów, które nakładają się na półkule (wielokąty, które kończą się wartością bezwzględną dla Wschodu - Zachodu większą niż 1):

  1. ustaw prostą projekcję w mercator:

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

  2. utwórz ścieżkę:

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

3. ustaw moje granice:

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. Dodaj wyjątek dla Alaski i stwierdza, że ​​półkule pokrywają się:

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

Mam nadzieję, że to pomoże.

DimeZilla
źródło
0

Dla osób, które chcą wyregulować w pionie i poziomie, oto rozwiązanie:

  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
źródło
0

Jak wyśrodkowałem Topojson, z którego musiałem wyciągnąć tę funkcję:

      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)
Nauka statystyk na przykładzie
źródło