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

в 13:01, , рубрики: d3.js, dataviz, svg, Веб-разработка, векторная графика, Инфографика, метки: , ,

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

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

В принципе, этот раздел можно пропустить, если не интересно, ссылка на нужный файл в самом конце раздела. Кому интересно, разбираемся дальше. Что такое карта, по сути это информация о геометрии некоего объекта с привязкой к координатам. В GIS системах для этих целей обычно используют shapefile'ы. Мы будем рисовать карту России, скорее всего поиск приведёт вас туда же куда и меня, а именно на GIS-Lab. Я выбрал проекцию Albers-Siberia. Скачиваем. Первым делом нам нужно преобразовать наш shapefile по стандарту WGS 84 (оно же EPSG:4326). Для этого необходимо создать файл проекции, например 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, а точнее одной из его библиотек OGR выполним преобразование. Я для этих нужд скачал Quantum GIS, который уже содержит всё что нужно и даже больше. После установки у вас появится несколько ярлыков, ищем среди них OSGeo4W, жмакаем, переходим в каталог с нашими файлами и вводим команду следующего вида:

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

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

  ogr2ogr -f GeoJSON output.json input.shp

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

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

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

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

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

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

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

Карту отрисовывать будем вообще или как?! Теперь у нас есть всё, что необходимо для отрисовки карты средствами 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 из начала статьи):

  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, чтобы он понимал русский язык- я не смог (может кто знает?), а лезть в 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, а пощупать результат можно через сервис bl.ocks.org. Да, CSS мы тут не рассматривали, но там всё тривиально.

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

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

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

Автор: KoGor

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js