Căn giữa một bản đồ trong d3 với một đối tượng GeoJSON


140

Hiện tại trong d3 nếu bạn có một đối tượng GeoJSON mà bạn sẽ vẽ, bạn phải mở rộng nó và dịch nó để có được kích thước mà người ta muốn và dịch nó để tập trung vào nó. Đây là một nhiệm vụ rất khó khăn của thử nghiệm và lỗi, và tôi đã tự hỏi liệu có ai biết một cách tốt hơn để có được các giá trị này không?

Vì vậy, ví dụ nếu tôi có mã này

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

Làm thế quái nào tôi có được .scale (8500) và .translate ([0, -1200]) mà không đi từng chút một?


Câu trả lời:


134

Sau đây dường như làm khoảng những gì bạn muốn. Các tỷ lệ có vẻ là ok. Khi áp dụng nó vào bản đồ của tôi có một sự bù đắp nhỏ. Sự bù trừ nhỏ này có lẽ là do tôi sử dụng lệnh dịch để căn giữa bản đồ, trong khi tôi có lẽ nên sử dụng lệnh trung tâm.

  1. Tạo một phép chiếu và d3.geo.path
  2. Tính giới hạn của hình chiếu hiện tại
  3. Sử dụng các giới hạn này để tính toán tỷ lệ và dịch
  4. Tái tạo hình chiếu

Trong mã:

  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
Này Jan van der Laan Tôi chưa bao giờ cảm ơn bạn vì câu trả lời này. Đây là một phản ứng thực sự tốt nếu tôi có thể tách ra tiền thưởng tôi sẽ làm. Cảm ơn vì điều đó!
leo núi

Nếu tôi áp dụng điều này, tôi nhận được giới hạn = vô hạn. Bất kỳ ý tưởng về làm thế nào điều này có thể được giải quyết?
Simke Nys

@SimkeNys Đây có thể là vấn đề tương tự như được đề cập ở đây stackoverflow.com/questions/23953366/ Đá Hãy thử giải pháp được đề cập ở đó.
Jan van der Laan

1
Xin chào Jan, cảm ơn mã của bạn. Tôi đã thử ví dụ của bạn với một số dữ liệu GeoJson nhưng nó không hoạt động. Bạn có thể cho tôi biết những gì tôi đang làm sai? :) Tôi đã tải lên dữ liệu GeoJson
user2644964

1
Trong phép chiếu D3 v4 là một phương thức tích hợp: projection.fitSize([width, height], geojson)( tài liệu API ) - xem câu trả lời của @dnltsk bên dưới.
Florian Ledermann

173

Câu trả lời của tôi gần với Jan van der Laan, nhưng bạn có thể đơn giản hóa mọi thứ một chút vì bạn không cần phải tính toán trọng tâm địa lý; bạn chỉ cần hộp giới hạn. Và, bằng cách sử dụng một phép chiếu đơn vị chưa được phân loại, chưa được dịch, bạn có thể đơn giản hóa toán học.

Phần quan trọng của mã này là:

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

Sau comping các đối tượng địa lý hộp bounding trong chiếu đơn vị, bạn có thể tính toán phù hợp quy mô bằng cách so sánh tỷ lệ khía cạnh của khung giới hạn ( b[1][0] - b[0][0]b[1][1] - b[0][1]) với tỷ lệ khía cạnh của vải ( widthheight). Trong trường hợp này, tôi cũng đã thu nhỏ hộp giới hạn đến 95% khung vẽ, thay vì 100%, do đó, có thêm một khoảng trống ở các cạnh để vuốt và các tính năng xung quanh hoặc phần đệm.

Sau đó, bạn có thể tính toán bản dịch bằng cách sử dụng trung tâm của khung giới hạn ( (b[1][0] + b[0][0]) / 2(b[1][1] + b[0][1]) / 2) và trung tâm của khung vẽ ( width / 2height / 2). Lưu ý rằng vì hộp giới hạn nằm trong tọa độ của phép chiếu đơn vị, nên nó phải được nhân với tỷ lệ ( s).

Ví dụ : bl.ocks.org/4707858 :

dự án đến hộp giới hạn

Có một câu hỏi liên quan trong đó là làm thế nào để phóng to một tính năng cụ thể trong bộ sưu tập mà không điều chỉnh phép chiếu, nghĩa là kết hợp phép chiếu với một biến đổi hình học để phóng to và thu nhỏ. Điều đó sử dụng các nguyên tắc giống như trên, nhưng toán học hơi khác một chút vì biến đổi hình học (thuộc tính "biến đổi" SVG) được kết hợp với phép chiếu địa lý.

Ví dụ : bl.ocks.org/4699541 :

phóng to đến khung giới hạn


2
Tôi muốn chỉ ra rằng có một vài lỗi trong đoạn mã trên, cụ thể là trong các chỉ số của giới hạn. Nó sẽ trông giống như: s = (0.95 / Math.max ((b [1] [0] - b [0] [0]) / width, (b [1] [1] - b [0] [0] ) / chiều cao)) * 500, t = [(chiều rộng - s * (b [1] [0] + b [0] [0])) / 2, (chiều cao - s * (b [1] [1] + b [0] [1])) / 2];
iros

2
@iros - Hình như * 500là không liên quan ở đây ... cũng vậy, b[1][1] - b[0][0]nên nằm b[1][1] - b[0][1]trong tính toán tỷ lệ.
nrabinowitz


5
Vì vậy: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
Chính vì một cộng đồng như thế này mà D3 là một niềm vui để làm việc cùng. Tuyệt vời!
arunkjn

54

Với d3 v4 hoặc v5, cách dễ dàng hơn!

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

và cuối cùng

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

Thưởng thức, chúc mừng


2
Tôi hy vọng câu trả lời này được bình chọn nhiều hơn. Đã làm việc với d3v4một thời gian và chỉ cần phát hiện ra phương pháp này.
Đánh dấu

2
Nơi nào gđến từ đâu? Có phải đó là container Svg?
Tschallacka

1
Tschallacka gnên được <g></g>gắn thẻ
giankotarola

1
Thật xấu hổ vì điều này là quá xa và sau 2 câu trả lời chất lượng. Thật dễ dàng để bỏ lỡ điều này và nó rõ ràng đơn giản hơn nhiều so với các câu trả lời khác.
Kurt

1
Cảm ơn bạn. Hoạt động trong v5 quá!
Chaitanya Bangera

53

Tôi mới sử dụng d3 - sẽ cố gắng giải thích cách tôi hiểu nhưng tôi không chắc mình đã làm mọi thứ đúng.

Bí mật là biết rằng một số phương pháp sẽ hoạt động trên không gian bản đồ (vĩ độ, kinh độ) và các phương pháp khác trên không gian cartesian (x, y trên màn hình). Không gian bản đồ (hành tinh của chúng ta) (gần như) hình cầu, không gian cartesian (màn hình) phẳng - để ánh xạ cái này qua cái khác bạn cần một thuật toán, được gọi là phép chiếu . Không gian này quá ngắn để đi sâu vào chủ đề hấp dẫn của các phép chiếu và cách chúng làm biến dạng các đặc điểm địa lý để biến hình cầu thành mặt phẳng; một số được thiết kế để bảo tồn các góc, một số khác bảo tồn khoảng cách và vân vân - luôn có sự thỏa hiệp (Mike Bostock có một bộ sưu tập lớn các ví dụ ).

nhập mô tả hình ảnh ở đây

Trong d3, đối tượng chiếu có thuộc tính / setter trung tâm, được cho theo đơn vị bản đồ:

chiếu.center ([vị trí])

Nếu trung tâm được chỉ định, đặt trung tâm của phép chiếu thành vị trí đã chỉ định, một mảng hai phần tử có kinh độ và vĩ độ tính theo độ và trả về phép chiếu. Nếu trung tâm không được chỉ định, trả về trung tâm hiện tại mặc định là ⟨0 °, 0 °.

Ngoài ra còn có bản dịch, được đưa ra bằng pixel - trong đó trung tâm chiếu tương đối so với khung vẽ:

hình chiếu.transTable ([điểm])

Nếu điểm được chỉ định, đặt phần bù dịch của phép chiếu thành mảng hai phần tử đã chỉ định [x, y] và trả về phép chiếu. Nếu điểm không được chỉ định, trả về giá trị dịch hiện tại mặc định là [480, 250]. Phần bù dịch xác định tọa độ pixel của tâm chiếu. Phần bù dịch mặc định đặt 0 °, 0 ° ở trung tâm của khu vực 960 × 500.

Khi tôi muốn tập trung một tính năng trong canvas, tôi muốn thiết lập các trung tâm dự báo đến trung tâm của tính năng khung giới hạn - công trình này cho tôi khi sử dụng Mercator (WGS 84, được sử dụng trong các bản đồ google) cho đất nước tôi (Brazil), không bao giờ thử nghiệm bằng cách sử dụng các phép chiếu và bán cầu khác. Bạn có thể phải điều chỉnh cho các tình huống khác, nhưng nếu bạn thực hiện những nguyên tắc cơ bản này, bạn sẽ ổn.

Ví dụ, đưa ra một phép chiếu và đường dẫn:

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

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

Các boundsphương pháp từ pathlợi nhuận các khung giới hạn tính theo pixel . Sử dụng nó để tìm tỷ lệ chính xác, so sánh kích thước tính bằng pixel với kích thước tính theo đơn vị bản đồ (0,95 mang lại cho bạn tỷ lệ 5% so với mức phù hợp nhất cho chiều rộng hoặc chiều cao). Hình học cơ bản ở đây, tính toán chiều rộng / chiều cao hình chữ nhật cho các góc đối diện chéo:

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

nhập mô tả hình ảnh ở đây

Sử dụng d3.geo.boundsphương pháp để tìm hộp giới hạn trong các đơn vị bản đồ:

b = d3.geo.bounds(feature);

Đặt tâm của hình chiếu vào giữa hộp giới hạn:

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

Sử dụng translatephương pháp để di chuyển trung tâm bản đồ đến trung tâm của khung vẽ:

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

Đến bây giờ, bạn sẽ có tính năng ở giữa bản đồ được phóng to với tỷ lệ 5%.


Có một bl.ocks ở đâu đó?
Hugolpz

Xin lỗi, không có bl.ocks hay ý chính, bạn đang cố gắng làm gì? Đây có phải là một cái gì đó giống như một nhấp chuột để phóng to? Xuất bản nó và tôi có thể xem mã của bạn.
Paulo Scardine

Câu trả lời và hình ảnh của Bostock cung cấp các liên kết đến các ví dụ bl.ocks.org cho phép tôi sao chép toàn bộ mã. Công việc hoàn thành. +1 và cảm ơn vì minh họa tuyệt vời của bạn!
Hugolpz

4

Có một phương thức centre () mà bạn có thể sử dụng để chấp nhận một cặp lat / lon.

Theo những gì tôi hiểu, dịch () chỉ được sử dụng để di chuyển các pixel của bản đồ theo nghĩa đen. Tôi không chắc làm thế nào để xác định quy mô là gì.


8
Nếu bạn đang sử dụng TopoJSON và muốn căn giữa toàn bộ bản đồ, bạn có thể chạy topojson với --bbox để bao gồm một thuộc tính bbox trong đối tượng JSON. Các tọa độ lat / lon cho tâm phải là [(b [0] + b [2]) / 2, (b [1] + b [3]) / 2] (trong đó b là giá trị bbox).
Paulo Scardine


2

Tôi đang tìm kiếm trên Internet một cách không cầu kỳ để tập trung vào bản đồ của mình và lấy cảm hứng từ câu trả lời của Jan van der Laan và mbostock. Đây là một cách dễ dàng hơn bằng cách sử dụng jQuery nếu bạn đang sử dụng một thùng chứa cho svg. Tôi đã tạo đường viền 95% cho phần đệm / viền, v.v.

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

Nếu bạn đang tìm tỷ lệ chính xác, câu trả lời này sẽ không phù hợp với bạn. Nhưng nếu như tôi, bạn muốn hiển thị một bản đồ tập trung trong một container, điều này là đủ. Tôi đã cố gắng hiển thị bản đồ Mercator và thấy rằng phương pháp này hữu ích trong việc tập trung bản đồ của mình và tôi có thể dễ dàng cắt bỏ phần Nam Cực vì tôi không cần nó.


1

Để xoay / thu phóng bản đồ, bạn nên nhìn vào lớp phủ SVG trên Tờ rơi. Điều đó sẽ dễ dàng hơn rất nhiều so với việc chuyển đổi SVG. Xem ví dụ này http://bost.ocks.org/mike/leaflet/ và sau đó Cách thay đổi trung tâm bản đồ trong tờ rơi


Nếu thêm sự phụ thuộc khác là điều đáng quan tâm, PAN và ZOOM có thể được thực hiện dễ dàng trong d3 thuần túy, xem stackoverflow.com/questions/17093614/
Kẻ

Câu trả lời này không thực sự đối phó với d3. Bạn cũng có thể xoay / phóng to bản đồ trong d3, tờ rơi là không cần thiết. (Chỉ nhận ra đây là một bài viết cũ, chỉ cần duyệt các câu trả lời)
JARRRRG

0

Với câu trả lời của mbostocks và bình luận của Herb Caudill, tôi bắt đầu gặp vấn đề với Alaska kể từ khi tôi đang sử dụng phép chiếu Mercator. Tôi nên lưu ý rằng vì mục đích riêng của mình, tôi đang cố gắng dự án và tập trung vào Hoa Kỳ. Tôi thấy rằng tôi phải kết hôn với hai câu trả lời với câu trả lời của Jan van der Laan ngoại trừ các đa giác trùng với các bán cầu (đa giác kết thúc với giá trị tuyệt đối cho Đông - Tây lớn hơn 1):

  1. thiết lập một phép chiếu đơn giản trong Mercator:

    phép chiếu = d3.geo.mercator (). scale (1) .translate ([0,0]);

  2. tạo đường dẫn:

    path = d3.geo.path (). chiếu (chiếu);

3. thiết lập giới hạn của tôi:

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.Thêm ngoại lệ cho Alaska và các tiểu bang chồng lên các bán cầu:

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

Tôi hi vọng cái này giúp được.


0

Đối với những người muốn điều chỉnh verticaly et heftaly, đây là giải pháp:

  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

Cách tôi tập trung vào một Topojson, nơi tôi cần để rút ra tính năng:

      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)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.