Відцентруйте карту в d3, задавши об’єкт geoJSON


140

Наразі в d3, якщо у вас є об’єкт geoJSON, який ви збираєтеся намалювати, ви повинні його масштабувати та перекладати, щоб отримати його до потрібного розміру та перекласти його, щоб централізувати його. Це дуже виснажливе завдання спроб і помилок, і мені було цікаво, чи хтось знає кращий спосіб отримати ці значення?

Наприклад, якщо у мене є цей код

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

Як, на біса, я отримую .scale (8500) та .translate ([0, -1200]), не переходячи потроху?


Відповіді:


134

Наступне, здається, робить приблизно те, що ви хочете. Масштабування здається нормальним. При застосуванні його до моєї карти є невеликий зміщення. Це невелике зміщення, ймовірно, викликане тим, що я використовую команду translate для центрування карти, тоді як, мабуть, я повинен використовувати команду center.

  1. Створіть проекцію та d3.geo.path
  2. Обчисліть межі поточної проекції
  3. Використовуйте ці межі для обчислення масштабу та перекладу
  4. Відтворити проекцію

У коді:

  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
Гей, Ян ван дер Лаан, я ніколи не дякував вам за цю відповідь. Це, до речі, справді хороша відповідь, якби я міг розколоти щедроту. Дякую за це!
альпініст

Якщо я застосую це, я отримаю межі = нескінченність. Будь-яка ідея про те, як це можна вирішити?
Сімке Найс

@SimkeNys Це може бути та сама проблема, що згадується тут stackoverflow.com/questions/23953366/… Спробуйте вказане там рішення.
Ян ван дер Лаан

1
Привіт, Ян, дякую за Ваш код. Я спробував ваш приклад з деякими даними GeoJson, але це не спрацювало. Чи можете ви сказати мені, що я роблю неправильно? :) Я завантажив дані GeoJson: onedrive.live.com/…
user2644964

1
У проекторі D3 v4 підгонка - це вбудований метод: projection.fitSize([width, height], geojson)( Документи API ) - див. Відповідь @dnltsk нижче.
Флоріан Ледерманн

173

Моя відповідь близька до Яну ван дер Лаана, але ви можете трохи спростити речі, оскільки вам не потрібно обчислювати географічний центроїд; вам потрібен лише обмежувальний ящик. І, використовуючи немасштабну неперекладену проекцію одиниці, ви можете спростити математику.

Важлива частина коду:

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

Після порівняння обмежувального вікна функції в проекційному блоці ви можете обчислити відповідну шкалу , порівнявши співвідношення сторін обмежувальної рамки ( b[1][0] - b[0][0]і b[1][1] - b[0][1]) із співвідношенням сторін полотна ( widthі height). У цьому випадку я також масштабував обмежувальний ящик на 95% полотна, а не на 100%, тому на краях є трохи додаткового місця для штрихів та оточуючих особливостей або підкладки.

Потім ви можете обчислити переклад, використовуючи центр обмежувального поля ( (b[1][0] + b[0][0]) / 2і (b[1][1] + b[0][1]) / 2) та центр полотна ( width / 2і height / 2). Зауважте, що оскільки обмежувальне поле знаходиться в координатах проекції одиниці, його необхідно помножити на масштаб ( s).

Наприклад, bl.ocks.org/4707858 :

проект до обмежувальної коробки

Є пов’язане питання, де полягає в тому, як збільшити масштаб певної функції в колекції без коригування проекції, тобто поєднуючи проекцію з геометричним перетворенням для збільшення та зменшення масштабу. Тут використовуються ті ж принципи, що і вище, але математика дещо відрізняється, оскільки геометричне перетворення (атрибут SVG "перетворення") поєднується з географічною проекцією.

Наприклад, bl.ocks.org/4699541 :

збільшення до обмежувального поля


2
Хочу зазначити, що в наведеному вище коді, зокрема в індексах меж, є кілька помилок. Він повинен виглядати так: s = (0,95 / Math.max ((b [1] [0] - b [0] [0]) / ширина, (b [1] [1] - b [0] [0] ) / висота)) * 500, t = [(ширина - s * (b [1] [0] + b [0] [0])) / 2, (висота - s * (b [1] [1] + b [0] [1])) / 2];
iros

2
@iros - Схоже на те * 500, що тут стороннє ... також b[1][1] - b[0][0]має бути b[1][1] - b[0][1]в розрахунку шкали.
nrabinowitz

2
Повідомлення про помилку: meta.stackexchange.com/questions/184140/…
Пауло Скардін

5
Отже: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
Саме завдяки такій спільноті D3 така радість працювати. Дивовижно!
arunkjn

54

З d3 v4 або v5 простеження стає простіше!

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

і, нарешті

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

Насолоджуйтесь, ура


2
Я сподіваюся, що за цю відповідь проголосується більше. Деякий час працювали з d3v4ним і щойно відкрили цей метод.
Марк

2
Звідки береться g? Це контейнер svg?
Цхаллачка

1
Tschallacka gмає бути <g></g>тегом
giankotarola

1
Сором, це поки що вниз і після 2 якісних відповідей. Це легко пропустити, і це, очевидно, простіше, ніж інші відповіді.
Курт

1
Дякую. Працює і в v5!
Чайтанья Бангера

53

Я новачок у d3 - спробую пояснити, наскільки я це розумію, але я не впевнений, що все в мене правильно.

Секрет полягає в тому, що деякі методи будуть діяти на картографічному просторі (широта, довгота), а інші - на декартовому просторі (x, y на екрані). Картографічний простір (наша планета) є (майже) сферичним, декартовий простір (екран) плоский - для того, щоб відобразити один над одним потрібний алгоритм, який називається проекцією . Цей простір занадто короткий, щоб заглибитись у захоплюючий предмет проекцій і як вони спотворюють географічні особливості, щоб перетворити сферичну в площину; деякі призначені для збереження кутів, інші - заощадження відстаней тощо - завжди є компроміс (Майк Босток має величезну колекцію прикладів ).

введіть тут опис зображення

У d3 об’єкт проекції має властивість / сеттер центра, задану в одиницях карти:

projection.center ([розташування])

Якщо центр вказаний, встановлює центр проекції на вказане місце, двоелементний масив довготи та широти у градусах та повертає проекцію. Якщо центр не вказано, повертає поточний центр, який за замовчуванням становить ⟨0 °, 0 °⟩.

Існує також переклад, поданий у пікселях - там, де центр проекції стоїть відносно полотна:

projection.translate ([точка])

Якщо вказана точка, встановлюється зміщення перекладу проекції на вказаний двоелементний масив [x, y] і повертає проекцію. Якщо точка не вказана, повертає поточний зсув перекладу, який за замовчуванням дорівнює [480, 250]. Зсув перекладу визначає піксельні координати центру проекції. Зсув перекладу за замовчуванням розміщує ⟨0 °, 0 °⟩ в центрі області 960 × 500.

Коли я хочу зосередити функцію на полотні, мені подобається встановити проектний центр на центр вікна, що обмежує функцію - це працює для мене, коли для моєї країни (Бразилія) використовується Меркатор (WGS 84, який використовується в картах Google), ніколи не тестували за допомогою інших проекцій і півкуль. Можливо, вам доведеться вносити корективи в інші ситуації, але якщо ви дотримуєтесь цих основних принципів, вам буде добре.

Наприклад, задані проекція та шлях:

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

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

boundsМетод з pathповертає обмежує прямокутник в пікселях . Використовуйте його, щоб знайти правильну шкалу, порівнюючи розмір у пікселях із розміром у одиницях карти (0,95 дає 5% -ний запас над найкращим розміром для ширини чи висоти). Основна геометрія тут, обчислення ширини / висоти прямокутника, заданих діагонально протилежними кутами:

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

введіть тут опис зображення

Використовуйте d3.geo.boundsметод для пошуку обмежувального поля в одиницях карти:

b = d3.geo.bounds(feature);

Встановіть центр виступу на центр обмежувальної коробки:

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

Використовуйте translateметод, щоб перемістити центр карти до центру полотна:

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

На даний момент у вас має бути функція в центрі карти, збільшена із 5% запасом.


Чи є десь бл.оки?
Гюгольпз

Вибачте, ні бл.окс, ні суть, що ви намагаєтесь зробити? Це щось на кшталт масштабу? Опублікуйте його, і я можу переглянути ваш код.
Пауло Скардін

Відповідь та зображення Бостока містять посилання на приклади bl.ocks.org, які дозволяють мені скопіювати інженер цілий код. Робота виконана. +1 і дякую за ваші чудові ілюстрації!
Гюгольпз

4

Існує метод center (), який можна використовувати, який приймає пару lat / lon.

З того, що я розумію, translate () використовується лише для буквального переміщення пікселів карти. Я не впевнений, як визначити, що таке шкала.


8
Якщо ви використовуєте TopoJSON і хочете централізувати всю карту, можете запустити topojson за допомогою --bbox, щоб включити атрибут bbox в об'єкт JSON. Координати lat / lon для центру повинні бути [(b [0] + b [2]) / 2, (b [1] + b [3]) / 2] (де b - значення bbox).
Пауло Скардін


2

Я дивився в Інтернеті, щоб не суєтним способом зосередити свою карту, і мене надихнуло відповідь Яна ван дер Лаана та mbostock. Ось простіший спосіб використання jQuery, якщо ви використовуєте контейнер для svg. Я створив кордон на 95% для прокладки / бордюрів тощо.

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

Якщо ви шукаєте точне масштабування, ця відповідь не допоможе вам. Але якщо, як я, ви хочете показати карту, яка централізується в контейнері, цього має бути достатньо. Я намагався відобразити карту Меркатора і виявив, що цей метод корисний для централізації моєї карти, і я міг легко відрізати частину Антарктики, оскільки мені вона не потрібна.


1

Для панорамування / масштабування карти слід переглянути накладення SVG на листівці. Це буде набагато простіше, ніж перетворення SVG. Дивіться цей приклад http://bost.ocks.org/mike/leaflet/, а потім Як змінити центр карти в листівці


При додаванні іншу залежність викликає занепокоєння, PAN і ZOOM можна легко зробити в чистому d3 см stackoverflow.com/questions/17093614 / ...
Пауло Scardine

Ця відповідь насправді не стосується d3. Можна також панорамировать / масштабувати карту в d3, листівка не потрібна. (Щойно зрозумів, що це старий пост, просто переглядав відповіді)
JARRRRG

0

З відповіддю mbostocks та коментарем Herb Caudill, я почав стикатися з проблемами на Алясці, оскільки використовував проекцію Меркатора. Слід зазначити, що для власних цілей я намагаюся проектувати та зосереджувати штати США. Я виявив, що мені довелося одружитися з двома відповідями з відповіддю Яна ван дер дер Лаана, за винятком полігонів, які перекриваються півкулі (полігони, які в кінцевому підсумку мають абсолютне значення для Сходу - Заходу, що більше 1):

  1. встановити просту проекцію в Меркатора:

    проекція = d3.geo.mercator (). шкала (1) .переклад ([0,0]);

  2. створити шлях:

    шлях = d3.geo.path (). проекція (проекція);

3.встановити мої межі:

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.Додайте виняток для Аляски та заявляйте, що перекриваються півкулі:

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

Я сподіваюся, що це допомагає.


0

Для людей, які хочуть налаштувати вертикальність та горизонталь, ось таке рішення:

  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

Як я зосереджував Топойсон, де мені потрібно було витягнути цю функцію:

      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)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.