Pusatkan peta di d3 diberi objek geoJSON


140

Saat ini dalam d3 jika Anda memiliki objek geoJSON yang akan Anda gambar, Anda harus mengukur dan menerjemahkannya untuk mendapatkan ukuran yang diinginkan dan menerjemahkannya untuk memusatkannya. Ini adalah tugas trial and error yang sangat membosankan, dan saya bertanya-tanya apakah ada yang tahu cara yang lebih baik untuk mendapatkan nilai-nilai ini?

Jadi misalnya jika saya punya kode ini

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

Bagaimana caranya mendapatkan .scale (8500) dan .translate ([0, -1200]) tanpa sedikit demi sedikit?


Jawaban:


134

Berikut ini sepertinya kira-kira apa yang Anda inginkan. Skala tampaknya ok. Saat menerapkannya ke peta saya ada offset kecil. Offset kecil ini mungkin disebabkan karena saya menggunakan perintah translate untuk memusatkan peta, sementara saya mungkin harus menggunakan perintah pusat.

  1. Buat proyeksi dan d3.geo.path
  2. Hitung batas proyeksi saat ini
  3. Gunakan batas ini untuk menghitung skala dan terjemahan
  4. Buat ulang proyeksi

Dalam kode:

  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
Hai Jan van der Laan, saya tidak pernah mengucapkan terima kasih atas tanggapan ini. Ini adalah respon yang sangat baik jika saya bisa membagi hadiah yang saya akan berikan. Terima kasih untuk itu!
memanjat

Jika saya menerapkan ini saya mendapatkan batas = tak terbatas. Adakah ide tentang bagaimana hal ini dapat diselesaikan?
Simke Nys

@SimkeNys Ini mungkin masalah yang sama seperti yang disebutkan di sini stackoverflow.com/questions/23953366/… Cobalah solusi yang disebutkan di sana.
Jan van der Laan

1
Halo Jan, terima kasih atas kodenya. Saya mencoba contoh Anda dengan beberapa data GeoJson tetapi tidak berhasil. Bisakah Anda memberi tahu saya apa yang saya lakukan salah? :) Saya mengunggah data GeoJson
user2644964

1
Dalam D3 v4 fitting fitting adalah metode bawaan : projection.fitSize([width, height], geojson)( API docs ) - lihat jawaban @dnltsk di bawah ini.
Florian Ledermann

173

Jawaban saya dekat dengan jawaban Jan van der Laan, tetapi Anda dapat menyederhanakan banyak hal karena Anda tidak perlu menghitung centroid geografis; Anda hanya perlu kotak pembatas. Dan, dengan menggunakan proyeksi unit yang tidak berskala dan tidak diterjemahkan, Anda dapat menyederhanakan matematika.

Bagian penting dari kode ini adalah ini:

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

Setelah menghitung kotak pembatas fitur dalam proyeksi unit, Anda dapat menghitung skala yang sesuai dengan membandingkan rasio aspek dari kotak pembatas ( b[1][0] - b[0][0]dan b[1][1] - b[0][1]) dengan rasio aspek kanvas ( widthdan height). Dalam hal ini, saya juga telah menskalakan kotak pembatas hingga 95% dari kanvas, daripada 100%, jadi ada sedikit ruang ekstra di bagian tepi untuk goresan dan fitur di sekitarnya atau lapisan.

Kemudian Anda dapat menghitung terjemahan menggunakan pusat kotak pembatas ( (b[1][0] + b[0][0]) / 2dan (b[1][1] + b[0][1]) / 2) dan pusat kanvas ( width / 2dan height / 2). Perhatikan bahwa karena kotak pembatas berada dalam koordinat proyeksi unit, itu harus dikalikan dengan skala ( s).

Misalnya, bl.ocks.org/4707858 :

memproyeksikan ke kotak pembatas

Ada pertanyaan terkait di mana cara memperbesar fitur tertentu dalam koleksi tanpa menyesuaikan proyeksi, yaitu , menggabungkan proyeksi dengan transformasi geometrik untuk memperbesar dan memperkecil. Itu menggunakan prinsip yang sama seperti di atas, tetapi matematika sedikit berbeda karena transformasi geometrik (atribut "transformasi" SVG) dikombinasikan dengan proyeksi geografis.

Misalnya, bl.ocks.org/4699541 :

tampilannya ke kotak pembatas


2
Saya ingin menunjukkan bahwa ada beberapa kesalahan dalam kode di atas, khususnya dalam indeks batas. Seharusnya terlihat seperti: s = (0,95 / Math.max ((b [1] [0] - b [0] [0]) / lebar, (b [1] [1] - b [0] [0] ) / tinggi)) * 500, t = [(lebar - s * (b [1] [0] + b [0] [0])) / 2, (tinggi - s * (b [1] [1] + b [0] [1])) / 2];
iros

2
@ros - Sepertinya * 500ini asing di sini ... juga, b[1][1] - b[0][0]harus b[1][1] - b[0][1]dalam perhitungan skala.
nrabinowitz


5
Jadi: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
Karena komunitas seperti ini maka D3 sangat menyenangkan untuk bekerja dengannya. Luar biasa!
arunkjn

54

Dengan d3 v4 atau v5 semakin mudah!

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

dan akhirnya

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

Selamat menikmati, Cheers


2
Saya harap jawaban ini lebih dipilih. Sudah lama bekerja bersama d3v4dan baru saja menemukan metode ini.
Markus

2
Dari mana datangnya g? Apakah itu wadah svg?
Tschallacka

1
Tschallacka gharus diberi <g></g>tag
giankotarola

1
Malu ini sangat jauh ke bawah dan setelah 2 jawaban berkualitas. Sangat mudah untuk melewatkan ini dan itu jelas jauh lebih sederhana daripada jawaban lainnya.
Kurt

1
Terima kasih. Bekerja di v5 juga!
Chaitanya Bangera

53

Saya baru mengenal d3 - akan mencoba menjelaskan bagaimana saya memahaminya tetapi saya tidak yakin semuanya sudah benar.

Rahasianya adalah mengetahui bahwa beberapa metode akan beroperasi pada ruang kartografi (lintang, bujur) dan lainnya pada ruang kartesius (x, y di layar). Ruang kartografi (planet kita) berbentuk bola (hampir) berbentuk bola, ruang kartesian (layar) datar - untuk memetakan satu di atas yang lain Anda memerlukan algoritma, yang disebut proyeksi . Ruang ini terlalu pendek untuk masuk ke dalam subjek proyeksi yang mempesona dan bagaimana mereka mendistorsi fitur geografis untuk mengubah bola menjadi bidang; beberapa dirancang untuk menghemat sudut, lainnya menghemat jarak dan sebagainya - selalu ada kompromi (Mike Bostock memiliki banyak koleksi contoh ).

masukkan deskripsi gambar di sini

Di d3, objek proyeksi memiliki properti pusat / penyetel, yang diberikan dalam unit peta:

projection.center ([lokasi])

Jika pusat ditentukan, setel pusat proyeksi ke lokasi yang ditentukan, larik dua elemen bujur dan lintang dalam derajat dan mengembalikan proyeksi. Jika pusat tidak ditentukan, kembalikan pusat saat ini yang secara default adalah ⟨0 °, 0 °⟩.

Ada juga terjemahan, yang diberikan dalam piksel - di mana pusat proyeksi relatif terhadap kanvas:

projection.translate ([point])

Jika titik ditentukan, atur offset terjemahan proyeksi ke array dua elemen [x, y] yang ditentukan dan kembalikan proyeksi. Jika titik tidak ditentukan, mengembalikan offset terjemahan saat ini yang defaultnya ke [480, 250]. Offset terjemahan menentukan koordinat piksel dari pusat proyeksi. Terjemahan default mengimbangi ⟨0 °, 0 °⟩ di tengah area 960 × 500.

Ketika saya ingin memusatkan fitur di kanvas, saya ingin mengatur pusat proyeksi ke tengah kotak pembatas fitur - ini berfungsi untuk saya ketika menggunakan mercator (WGS 84, digunakan di peta google) untuk negara saya (Brasil), tidak pernah diuji menggunakan proyeksi dan belahan lainnya. Anda mungkin harus membuat penyesuaian untuk situasi lain, tetapi jika Anda menerapkan prinsip-prinsip dasar ini, Anda akan baik-baik saja.

Misalnya, diberi proyeksi dan jalur:

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

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

The boundsmetode dari pathhasil kotak bounding dalam piksel . Gunakan untuk menemukan skala yang benar, membandingkan ukuran dalam piksel dengan ukuran dalam unit peta (0,95 memberi Anda margin 5% dari yang paling cocok untuk lebar atau tinggi). Geometri dasar di sini, menghitung lebar / tinggi persegi panjang yang diberikan diagonal berlawanan sudut:

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

masukkan deskripsi gambar di sini

Gunakan d3.geo.boundsmetode untuk menemukan kotak pembatas di unit peta:

b = d3.geo.bounds(feature);

Atur pusat proyeksi ke tengah kotak pembatas:

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

Gunakan translatemetode untuk memindahkan pusat peta ke pusat kanvas:

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

Sekarang Anda harus memiliki fitur di tengah peta diperbesar dengan margin 5%.


Apakah ada kesalahan di suatu tempat?
Hugolpz

Maaf, tidak ada masalah atau intinya, apa yang Anda coba lakukan? Apakah ini seperti klik untuk memperbesar? Terbitkan dan saya bisa melihat kode Anda.
Paulo Scardine

Jawaban dan gambar Bostock menyediakan tautan ke contoh bl.ocks.org yang memungkinkan saya menyalin seluruh kode insinyur. Pekerjaan selesai. +1 dan terima kasih atas ilustrasinya yang hebat!
Hugolpz

4

Ada metode center () yang dapat Anda gunakan yang menerima pasangan lat / lon.

Dari apa yang saya mengerti, translate () hanya digunakan untuk memindahkan piksel peta secara harfiah. Saya tidak yakin bagaimana menentukan skala.


8
Jika Anda menggunakan TopoJSON dan ingin memusatkan seluruh peta, Anda bisa menjalankan topojson dengan --bbox untuk memasukkan atribut bbox di objek JSON. Koordinat lat / lon untuk pusat harus [(b [0] + b [2]) / 2, (b [1] + b [3]) / 2] (di mana b adalah nilai bbox).
Paulo Scardine


2

Saya sedang mencari-cari di internet untuk mencari cara bebas repot memusatkan peta saya, dan terinspirasi oleh Jan van der Laan dan jawaban mbostock. Berikut cara yang lebih mudah menggunakan jQuery jika Anda menggunakan wadah untuk svg. Saya membuat batas 95% untuk bantalan / batas dll.

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

Jika Anda mencari penskalaan yang tepat, jawaban ini tidak akan bekerja untuk Anda. Tetapi jika seperti saya, Anda ingin menampilkan peta yang terpusat pada sebuah wadah, ini sudah cukup. Saya mencoba untuk menampilkan peta mercator dan menemukan bahwa metode ini berguna dalam memusatkan peta saya, dan saya dapat dengan mudah memotong bagian Antartika karena saya tidak membutuhkannya.


1

Untuk menggeser / memperbesar peta Anda harus melihat overlay SVG pada Leaflet. Itu akan jauh lebih mudah daripada mengubah SVG. Lihat contoh ini http://bost.ocks.org/mike/leaflet/ dan kemudian Bagaimana mengubah pusat peta di leaflet


Jika menambahkan ketergantungan lain menjadi perhatian, PAN dan ZOOM dapat dilakukan dengan mudah dalam pure d3, lihat stackoverflow.com/questions/17093614/…
Paulo Scardine

Jawaban ini tidak benar-benar berurusan dengan d3. Anda dapat menggeser / memperbesar peta di d3 juga, leaflet tidak diperlukan. (Baru menyadari ini sebuah posting lama, hanya melihat-lihat jawabannya)
JARRRRG

0

Dengan jawaban mbostocks, dan komentar Herb Caudill, saya mulai mengalami masalah dengan Alaska karena saya menggunakan proyeksi mercator. Saya harus mencatat bahwa untuk tujuan saya sendiri, saya mencoba untuk memproyeksikan dan memusatkan Amerika Serikat. Saya menemukan bahwa saya harus mengawinkan kedua jawaban dengan jawaban Jan van der Laan dengan pengecualian berikut untuk poligon yang tumpang tindih dengan belahan (poligon yang berakhir dengan nilai absolut untuk Timur - Barat yang lebih besar dari 1):

  1. buat proyeksi sederhana dalam mercator:

    proyeksi = d3.geo.mercator (). skala (1) .translate ([0,0]);

  2. buat jalan:

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

3. mengatur batas saya:

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.Tambahkan pengecualian untuk Alaska dan negara bagian yang tumpang tindih dengan belahan bumi:

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

Saya harap ini membantu.


0

Bagi orang yang ingin menyesuaikan vertikal dan horizontal, berikut adalah solusinya:

  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

Bagaimana saya memusatkan Topojson, di mana saya harus mengeluarkan fitur:

      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)
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.