- PVSM.RU - https://www.pvsm.ru -

Интерактивная SVG картограмма с помощью d3.js

Приветствую вас, читатели! Сегодня я расскажу вам как сделать интерактивную d3js.org [1], о возможностях этой JavaScript библиотеки в общем, а также придётся немного разобраться в том как и где лучше хранить геоинформацию для веба. В финале мы получим следующее:
Картограмма
Начать сие увлекательное путешествие можно под катом.

Дела картографические.

В принципе, этот раздел можно пропустить, если не интересно, ссылка на нужный файл в самом конце раздела. Кому интересно, разбираемся дальше. Что такое карта, по сути это информация о геометрии некоего объекта с привязкой к координатам. В shapefile'ы [2]. Мы будем рисовать карту России, скорее всего поиск приведёт вас туда же куда и меня, а именно на GIS-Lab [3]. Я выбрал проекцию Albers-Siberia. Скачиваем. Первым делом нам нужно преобразовать наш shapefile по стандарту WGS 84 [4] (оно же EPSG:4326). Для этого необходимо создать файл проекции [5], например Albers_Siberia.prj со следующим содержимым :

+proj=aea +lat_1=52 +lat_2=64 +lat_0=0 +lon_0=105 +x_0=18500000 +y_0=0 +ellps=krass +units=m +towgs84=28,-130,-95,0,0,0,0 +no_defs

Затем при помощи GDAL [6], а точнее одной из его библиотек OGR [7] выполним преобразование. Я для этих нужд скачал Quantum GIS [8], который уже содержит всё что нужно и даже больше. После установки у вас появится несколько ярлыков, ищем среди них OSGeo4W, жмакаем, переходим в каталог с нашими файлами и вводим команду следующего вида:

  ogr2ogr -f 'ESRI Shapefile' -s_srs Albers_Siberia.prj -t_srs EPSG:4326 input-fixed.shp input.shp

Таким образом мы получили нужный нам shapefile, хотя как нужный, для веба он нам совсем не подходит, поэтому теперь сгенерируем GeoJSON [9] файл на основе наших данных. Затем из GeoJSON'а сгенерируем TopoJSON [10], который-то и нужен для нашей картограммы. Такие вот дела, но вы не расстраивайтесь, GeoJSON- штука тож полезная, авось пригодится. Итак, идём опять в консоль и пишем примерно следующее:

  ogr2ogr -f GeoJSON output.json input.shp

Получаем наш GeoJSON файл, открываем его и видим сюрприз от GIS-Lab.

Шифровка от GIS-Lab

Вообще говоря, эта фича присутствует в shapefile изначально, но заметил я этот «приятный» сюрприз только на этом этапе. Фича в том, что все названия регионов зашифрованы от врагов Родины и отображаются крякозябрами. Но наш то человек знает где искать шифровальную книгу [11]. Но не тут-то было. Ни одна кириллическая кодировка не подошла (на DOS-866 были осмысленные названия, но часть букв отображались различными квадратиками), тут я призадумался и пошёл искать истину в интернете, может искал я плохо, но на форумах GIS-Lab, да и в других местах ничего по поводу кракозябр на этой карте не было вообще (а карта от 2010 года, как я понимаю) тут я совсем отчаялся, открыл снова EditPad (там, по-моему, больше всего кодировок представлено, да и вообще он весьма удобен для работы с текстом и регулярками) и начал перебирать все кодировки подряд, и, о чудо, при выборе кодировки MIK: Bulgarian (!?) получил почти что хотел, а именно названия регионов. Правда все буквы в названиях были разделены досовским символом ├, ну простенькая регулярка решила эту проблему довольно быстро. Хотя почему проблему, это ведь шифр и мы прошли проверку свой-чужой =). Да кстати есть у этого файла ещё одна фича, о которой правда сразу упоминают на GIS-Lab, а именно: на карте отсутствуют границы Чеченской и Ингушской республик, по причине отсутствия по ним данных Росреестра (ну не хотят они туда в командировку ехать, и всё тут). Ну да в принципе не страшно (хотя кому конечно), но неприятный осадочек остался.

Теперь перейдём к генерации TopoJSON, это позволит нам уменьшить размер файла. Вообще TopoJSON является оптимизацией GeoJSON'а в плане топологий, он убирает избыточную информацию, например убирает дублирование общих границ у соседних регионов. Но мы можем уменьшить размер файла ещё больше, упростив геометрию. Итак, приступим! Запускаем командную строку Node.js [12] (он нужен для имплементации TopoJSON [13]) и пишем следующее:

   topojson -o output_topo.json -p -s 1e-7 -- name=input_geo.json

Здесь параметр -p отвечает за сохранение feature properties, а -s 1e-7 за упрощение геометрии, 1e-7 это порог в стерадианах [14] чем меньше, тем точнее геометрия: 1e-3 это Швейцария относительно карты мира, а 1e-9 футбольное поле. Для чего это может быть нужно — если вы захотите сделать возможность зума на вашей карте. Разделитель -- это просто разделитель (ваш К.О.) выходного и входного файлов, а префикс russia задаёт имя объекта, если его не указывать, то в качестве имени будет использоваться имя входного файла, что не всегда удобно (может быть громоздким). В полученном файле я заменил названия регионов на коды в соответствии с ISO 3166-2:RU [15]. Всё. Файл можно взять на GitHub [16].

Рисуем картограмму

Карту отрисовывать будем вообще или как?! Теперь у нас есть всё, что необходимо для отрисовки карты средствами d3.js. Скопируйте следующий шаблон и приступим:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Accidents on the Road - Choropleth</title>
  <script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
  <script type="text/javascript" src="http://d3js.org/queue.v1.min.js"></script>
  <script type="text/javascript" src="http://d3js.org/topojson.v0.min.js"></script>
  <!-- <script type="text/javascript" src="http://d3js.org/topojson.v1.min.js"></script> -->
</head>
<style>
  your awesome CSS       
</style>
<body>
  <h1>Cool Header</h1>
  <script type="text/javascript">
    Your awesome d3.js code
  </script>
</body>
</html>

Сначала зададим размеры нашей SVG карты.

  var width = 960,
  height = 500;

Зададим домен цветов для картограммы, домен для легенды и подписи к легенде.

  var color_domain = [50, 150, 350, 750, 1500]
  var ext_color_domain = [0, 50, 150, 350, 750, 1500]
  var legend_labels = ["< 50", "50+", "150+", "350+", "750+", "> 1500"]              
  var color = d3.scale.threshold()
  .domain(color_domain)
  .range(["#adfcad", "#ffcb40", "#ffba00", "#ff7d73", "#ff4e40", "#ff1300"]);

Добавим в документ элемент и класс tooltip.

  var div = d3.select("body").append("div")   
  .attr("class", "tooltip")               
  .style("opacity", 0);

Добавим SVG с атрибутами, задающими размер.

  var svg = d3.select("body").append("svg")
  .attr("width", width)
  .attr("height", height);

Зададим параметры проецирования (вспоминаем/смотрим Albers_Siberia.prj [17] из начала статьи):

  var projection = d3.geo.albers()
  .rotate([-105, 0])
  .center([-10, 65])
  .parallels([52, 64])
  .scale(700)
  .translate([width / 2, height / 2]);

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

Читаем данные.

  queue()
  .defer(d3.json, "/d/5685937/russia_1e-7sr.json")
  .defer(d3.csv, "Accidents.csv")
  .await(ready);

Начинаем отрисовку. Создаем объекты для пар код региона: кол-во смертей и код региона: название региона.

  function ready(error, map, data) {
   var rateById = {};
   var nameById = {};

   data.forEach(function(d) {
    rateById[d.RegionCode] = +d.Deaths;
    nameById[d.RegionCode] = d.RegionName;
  });

Отрисовка и раскраска картограммы.

  svg.append("g")
  .attr("class", "region")
  .selectAll("path")
  .data(topojson.object(map, map.objects.russia).geometries)
  //.data(topojson.feature(map, map.objects.russia).features) <-- in case topojson.v1.js
  .enter().append("path")
  .attr("d", path)
  .style("fill", function(d) {
    return color(rateById[d.properties.region]); 
  })
  .style("opacity", 0.8)

Обрабатываем события: меняем яркость региона (для подсветки) и выводим в tooltip'е название региона и точное численное значение.

  .on("mouseover", function(d) {
    d3.select(this).transition().duration(300).style("opacity", 1);
    div.transition().duration(300)
    .style("opacity", 1)
    div.text(nameById[d.properties.region] + " : " + rateById[d.properties.region])
    .style("left", (d3.event.pageX) + "px")
    .style("top", (d3.event.pageY -30) + "px");
  })
  .on("mouseout", function() {
    d3.select(this)
    .transition().duration(300)
    .style("opacity", 0.8);
    div.transition().duration(300)
    .style("opacity", 0);
  })

Теперь хотелось бы научиться добавлять чего-нибудь на эту самую карту, я решил добавить города-милионники России, для этого нам собственно нужен сам город и его координаты (широта и долгота в десятичных градусах), к сожалению найти геокодер вроде этого gpsvisualizer.com/geocoder [18], чтобы он понимал русский язык- я не смог (может кто знает?), а лезть в API Яндекс.Карт не хотелось, тем более что список маленький. Хорошо бы они сами колдунчик такой сделали, ну да ладно, отвлекся я. В итоге получили список следующего вида:

City  lat lon
Москва  55.7522200  37.6155600
Санкт-Петербург 59.8944400  30.2641700

Ну собственно и добавим их, группой: точка-подпись.

  d3.tsv("cities.tsv", function(error, data) {
    var city = svg.selectAll("g.city")
    .data(data)
    .enter()
    .append("g")
    .attr("class", "city")
    .attr("transform", function(d) { return "translate(" + projection([d.lon, d.lat]) + ")"; });

    city.append("circle")
    .attr("r", 3)
    .style("fill", "lime")
    .style("opacity", 0.75);

    city.append("text")
    .attr("x", 5)
    .text(function(d) { return d.City; });
  });
  };

Тут хотелось бы добавить, что вместо точек можно добавить, например, круговую/кольцевую диаграмму, увеличив тем самым информационную нагрузку на нашу картограмму. Вообще возможностей масса, всё ограничивается вашими задачами, воображением и целесообразностью с точки зрения UI/UX.

Ну и под конец добавим легенду нашей картограмме:

  var legend = svg.selectAll("g.legend")
  .data(ext_color_domain)
  .enter().append("g")
  .attr("class", "legend");

  var ls_w = 20, ls_h = 20;

  legend.append("rect")
  .attr("x", 20)
  .attr("y", function(d, i){ return height - (i*ls_h) - 2*ls_h;})
  .attr("width", ls_w)
  .attr("height", ls_h)
  .style("fill", function(d, i) { return color(d); })
  .style("opacity", 0.8);

  legend.append("text")
  .attr("x", 50)
  .attr("y", function(d, i){ return height - (i*ls_h) - ls_h - 4;})
  .text(function(d, i){ return legend_labels[i]; });

Вот в принципе и всё, я старался показать основные моменты на простом примере, надеюсь что мне это удалось. Код конечно не идеален (идеального ничего не бывает), но цель, повторюсь, была сделать его понятным, а не супер универсальным/эффективным. Исходники можно найти на GitHub [19], а пощупать результат можно через сервис bl.ocks.org [20]. Да, CSS мы тут не рассматривали, но там всё тривиально.

Итоги. Что дальше?

Ну вот мы и создали простенькую картограмму, без возможности приближения, сложной анимации и прочих наворотов, но при желании добавить их сюда не составит большого труда. Вообще данная библиотека имеет широчайшие возможности для визуализации всего и вся: графики, диаграммы, картограммы, деревья, графы, чарты, тепловые карты…. Такой вот DataViz комбайн, всё что нужно можно найти через сайт d3js.org [1]. Mike Bostock активно развивает проект, почти каждый день выкладывает новые примеры (один минус, почти все без комментариев), на stackoverflow [21] тоже много чего и отвечают там оперативно, в том числе и сам Майк. Так что дерзайте, имхо эта библиотека, являющаяся по сути основным инструментом визуализации за бугром, у нас несправедливо обделена вниманием. Ну и я, если это будет интересно хабросообществу, буду периодически разбирать интересные примеры. Собственно всё, комментарии, вопросы и предложения приветствуются.

P.S. Чуть не забыл самое главное, будьте осторожнее на дорогах, особенно если вы не с Чукотки, уродов всяких у нас хватает и масса видео с регистраторов тому подтверждение! А вообще это печально, почти 28 тысяч смертей за год, ужас просто (данные брал с офф. сайта ГИБДД).

Автор: KoGor

Источник [22]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/veb-razrabotka/35693

Ссылки в тексте:

[1] d3js.org: http://d3js.org

[2] shapefile'ы: http://ru.wikipedia.org/wiki/Shapefile

[3] GIS-Lab: http://gis-lab.info/qa/rusbounds-rosreestr.html

[4] WGS 84: http://ru.wikipedia.org/wiki/WGS_84

[5] проекции: http://gis-lab.info/qa/gis-lab-projections.html

[6] GDAL: http://www.gdal.org

[7] OGR: http://www.gdal.org/ogr2ogr.html

[8] Quantum GIS: http://www.qgis.org

[9] GeoJSON: http://geojson.org/

[10] TopoJSON: https://github.com/mbostock/topojson/wiki

[11] шифровальную книгу: http://habrahabr.ru/post/147843/

[12] Node.js: http://nodejs.org/

[13] TopoJSON: https://npmjs.org/package/topojson

[14] стерадианах: http://en.wikipedia.org/wiki/Steradian

[15] ISO 3166-2:RU: http://ru.wikipedia.org/wiki/ISO_3166-2:RU

[16] GitHub: https://github.com/KoGor/Maps.GeoInfo

[17] Albers_Siberia.prj: #prj_param

[18] gpsvisualizer.com/geocoder: http://www.gpsvisualizer.com/geocoder/

[19] GitHub: https://gist.github.com/KoGor/5685876

[20] bl.ocks.org: http://bl.ocks.org/KoGor/5685876

[21] stackoverflow: http://stackoverflow.com/questions/tagged/d3.js

[22] Источник: http://habrahabr.ru/post/181766/