Хочу предложить вашему вниманию, все таки доведенную до определенной точки, свою библиотеку для визуализации данных blackHole.js использующую d3.js.
Данная библиотека позволяет создавать визуализации подобного плана:
картинки кликабельные
или 
Статья будет посвящена примеру использования blackHole.js совместно с leaflet.js и ей подобными типа mapbox.
Но так же будут рассмотрено использование: google maps, leaflet.heat.
Получится вот так =)

Поведение точки зависит от того где я находился по мнению google в определенный момент времени
Посмотрите, а как перемещались вы?...
Пример основан на проекте location-history-visualizer от @theopolisme
В тексте статьи будут разобраны только интересные места весь остальной код вы можете «поковырять» на codepen.io.
В статье
- Подготовка
- Приложение на JS
- Подключение слоя с blackHole.js
- Персонализация и вывод карты Google Maps
- Подключение слоя c heatmap
- Подготовка и обработка данных
- Заключение
Подготовка
Для начала нам понадобится:
- leaflet.js — библиотека с открытым исходным кодом, написанная Владимиром Агафонкиным (CloudMade) на JavaScript, предназначенная для отображения карт на веб-сайтах (© wikipedia).
- Leaflet.heat — легковесный heatmap палгин для leaflet.
- Google Maps Api — для подключения google maps персонализированных карт
- Leaflet-plugins от Павла Шрамова — плагин позволяет подключать к leaflet.js карты google, yandex, bing. Но нам в частности понадобиться только скрипт Google.js
- d3.js — библиотека для работы с данными, обладающая набором средств для манипуляции над ними и набором методов их отображения.
- ну и собственно blackHole.js
- данные о вашей геопозиции собранные бережно за нас Google.
Как выгрузить данныеДля начала, вы должны перейти Google Takeout чтобы скачать информацию LocationHistory. На странице нажмите кнопку Select none, затем найдите в списке «Location History» и отметьте его. Нажмите на кнопку Next и нажмите на кнопку Create archive. Дождитесь завершения работы. Нажмите кнопку Download и распакуйте архив в нужную вам директорию.
Пример состоит из трех файлов index.html, index.css и index.js.
Код первых двух вы можете посмотреть на codepen.io
Но в двух словах могу сказать, что нам потребуется на самом деле вот такая структура DOM:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> </head> <body> <div id="map"></div> <!-- здесь подключаем скрипты --> </body> </html>
Приложение на JS
Само приложение состоит из нескольких частей.
- Подключение слоя с blackHole.js.
- Персонализация и вывод карты Google Maps.
- Подключение слоя c heatmap.
- Подготовка и обработка данных.
Класс обертка для blackHole для leaflet
Для того чтобы нам совместно использовать blackHole.js и leaflet.js, необходимо создать слой обертку для вывода нашей визуализации поверх карты. При этом мы сохраним все механизмы работы с картой и интерактивные возможности библиотеки blackHole.js.
В библиотеке leaflet.js есть необходимые нам средства: L.Class.
В нем нам необходимо «перегрузить» методы: initialize, onAdd, onRemove, addTo.
На самом деле это просто методы для стандартной работы со слоями в leaflet.js.
!function(){ L.BlackHoleLayer = L.Class.extend({ // выполняется при инициализации слоя initialize: function () { }, // когда слой добавляется на карту то вызывается данный метод onAdd: function (map) { // Если слой уже был инициализирован значит, мы его хотим снова показать if (this._el) { this._el.style('display', null); // проверяем не приостановлена ли была визуализация if (this._bh.IsPaused()) this._bh.resume(); return; } this._map = map; //выбираем текущий контейнер для слоев и создаем в нем наш div, //в котором будет визуализация this._el = d3.select(map.getPanes().overlayPane).append('div'); // создаем объект blackHole this._bh = d3.blackHole(this._el); //задаем класс для div var animated = map.options.zoomAnimation && L.Browser.any3d; this._el.classed('leaflet-zoom-' + (animated ? 'animated' : 'hide'), true); this._el.classed('leaflet-blackhole-layer', true); // определяем обработчики для событии map.on('viewreset', this._reset, this) .on('resize', this._resize, this) .on('move', this._reset, this) .on('moveend', this._reset, this) ; this._reset(); }, // соответственно при удалении слоя leaflet вызывает данный метод onRemove: function (map) { // если слой удаляется то мы на самом деле его просто скрываем. this._el.style('display', 'none'); // если визуализация запущена, то ее надо остановить if (this._bh.IsRun()) this._bh.pause(); }, // вызывается для того чтоб добывать данный слой на выбранную карту. addTo: function (map) { map.addLayer(this); return this; }, // внутренний метод используется для события resize _resize : function() { // выполняем масштабирование визуализации согласно новых размеров. this._bh.size([this._map._size.x, this._map._size.y]); this._reset(); }, // внутренний метод используется для позиционирования слоя с визуализацией корректно на экране _reset: function () { var topLeft = this._map.containerPointToLayerPoint([0, 0]); var arr = [-topLeft.x, -topLeft.y]; var t3d = 'translate3d(' + topLeft.x + 'px, ' + topLeft.y + 'px, 0px)'; this._bh.style({ "-webkit-transform" : t3d, "-moz-transform" : t3d, "-ms-transform" : t3d, "-o-transform" : t3d, "transform" : t3d }); this._bh.translate(arr); } }); L.blackHoleLayer = function() { return new L.BlackHoleLayer(); }; }();
Ничего особенного сложного в этом нет, любой плагин, или слой, или элемент управления для leaflet.js создаются подобным образом.
Вот к примеру элементы управления процессом визуализации для blackHole.js.
Персонализация Google Maps
Google Maps API предоставляют возможности для персонализации выводимой карты. Для этого можно почитать документацию. Там очень много параметров и их сочетании, которые дадут вам нужный результат. Но быстрей воспользоваться готовыми наборами.
Давайте теперь создадим карту и запросим тайтлы от google в нужном для нас стиле.
// создаем объект карты в div#map var map = new L.Map('map', { maxZoom : 19, // Указываем максимальный масштаб minZoom : 2 // и минимальный }).setView([0,0], 2); // и говорим сфокусироваться в нужной точке // создаем слой с картой google c типом ROADMAP и параметрами стиля. var ggl = new L.Google('ROADMAP', { mapOptions: { backgroundColor: "#19263E", styles : [ { "featureType": "water", "stylers": [ { "color": "#19263E" } ] }, { "featureType": "landscape", "stylers": [ { "color": "#0E141D" } ] }, { "featureType": "poi", "elementType": "geometry", "stylers": [ { "color": "#0E141D" } ] }, { "featureType": "road.highway", "elementType": "geometry.fill", "stylers": [ { "color": "#21193E" } ] }, { "featureType": "road.highway", "elementType": "geometry.stroke", "stylers": [ { "color": "#21193E" }, { "weight": 0.5 } ] }, { "featureType": "road.arterial", "elementType": "geometry.fill", "stylers": [ { "color": "#21193E" } ] }, { "featureType": "road.arterial", "elementType": "geometry.stroke", "stylers": [ { "color": "#21193E" }, { "weight": 0.5 } ] }, { "featureType": "road.local", "elementType": "geometry", "stylers": [ { "color": "#21193E" } ] }, { "elementType": "labels.text.fill", "stylers": [ { "color": "#365387" } ] }, { "elementType": "labels.text.stroke", "stylers": [ { "color": "#fff" }, { "lightness": 13 } ] }, { "featureType": "transit", "stylers": [ { "color": "#365387" } ] }, { "featureType": "administrative", "elementType": "geometry.fill", "stylers": [ { "color": "#000000" } ] }, { "featureType": "administrative", "elementType": "geometry.stroke", "stylers": [ { "color": "#19263E" }, { "lightness": 0 }, { "weight": 1.5 } ] } ] } }); // добавляем слой на карту. map.addLayer(ggl);
В результате получим вот такую карту

К данному решению пришел после некоторого времени использования в проекте MapBox, которая дает инструмент для удобной стилизации карт и много чего еще, но при большем количестве запросов становится платной.
Теплокарта
Heatmap или теплокарта позволяет отобразить частоту упоминания определенной координаты выделяя интенсивность градиентом цветов и группировать данные при масштабировании. Получается нечто подобное

Для ее построения мы используем плагин leaflet.heatmap. Но существую и иные.
Для того чтобы наша визуализация была всегда поверх других слоев, а в частности поверх heatmap, и не теряла свои интерактивные особенности, необходимо добавлять blackHole.js после того, когда добавлены другие слои плагинов на карту.
// создаем слой с blackHole.js var visLayer = L.blackHoleLayer() , heat = L.heatLayer( [], { // создаем слой с heatmap opacity: 1, // непрозрачность radius: 25, // радиус blur: 15 // и размытие }).addTo( map ) // сперва добавляем слой с heatmap ; visLayer.addTo(map); // а теперь добавляем blackHole.js
Подготовка и визуализация данных
Библиотека готова работать сразу из «коробки» с определенным форматом данных а именно:
var rawData = [ { "key": 237, "category": "nemo,", "parent": { "name": "cumque5", "key": 5 }, "date": "2014-01-30T12:25:14.810Z" }, //... и еще очень много данных ]
Тогда для запуска визуализации потребуется всего ничего кода на js:
var data = rawData.map(function(d) { d.date = new Date(d.date); return d; }) , stepDate = 864e5 , d3bh = d3.blackHole("#canvas") ; d3bh.setting.drawTrack = true; d3bh.on('calcRightBound', function(l) { return +l + stepDate; }) .start(data) ;
подробней в документации
Но сложилось так что мы живем в мире, где идеальных случаев — раз-два и обчелся.
Поэтому библиотека предоставляет программистам возможность подготовить blackHole.js к работе с их форматом данных.
В нашем случаем мы имеем дело с LocationHistory.json от Google.
{ "somePointsTruncated" : false, "locations" : [ { "timestampMs" : "1412560102986", "latitudeE7" : 560532385, "longitudeE7" : 929207681, "accuracy" : 10, "velocity" : -1, "heading" : -1, "altitude" : 194, "verticalAccuracy" : 1 }, { "timestampMs" : "1412532992732", "latitudeE7" : 560513299, "longitudeE7" : 929186602, "accuracy" : 10, "velocity" : -1, "heading" : -1, "altitude" : 203, "verticalAccuracy" : 2 }, //... и тд ]}
Давайте подготовим данные и настроим blackHole.js для работы с ними.
function restart() { bh.stop(); if ( !locations || !locations.length) return; // очищаем старую информацию о позициях на heatmap heat.setLatLngs([]); // запускаем визуализацию с пересчетом всех объектов bh.start(locations, map._size.x, map._size.y, true); visLayer._resize(); }
Теперь парсинг данных
var parentHash; // функция вызывается для когда выбран файл для загрузки. function stageTwo ( file ) { bh.stop(); // останавливаем визуализацию если она была запущена // Значение для конвертации координат из LocationHistory в привычные для leaflet.js var SCALAR_E7 = 0.0000001; // Запускаем чтение файла processFile( file ); function processFile ( file ) { //Создаем FileReader var reader = new FileReader(); reader.onprogress = function ( e ) { // здесь отображаем ход чтения файла }; reader.onload = function ( e ) { try { locations = JSON.parse( e.target.result ).locations; if ( !locations || !locations.length ) { throw new ReferenceError( 'No location data found.' ); } } catch ( ex ) { // вывод ошибки console.log(ex); return; } parentHash = {}; // для вычисления оптимальных границ фокусирования карты var sw = [-Infinity, -Infinity] , se = [Infinity, Infinity]; locations.forEach(function(d, i) { d.timestampMs = +d.timestampMs; // конвертируем в число // преобразуем координаты d.lat = d.latitudeE7 * SCALAR_E7; d.lon = d.longitudeE7 * SCALAR_E7; // формируем уникальный ключ для parent d.pkey = d.latitudeE7 + "_" + d.longitudeE7; // определяем границы sw[0] = Math.max(d.lat, sw[0]); sw[1] = Math.max(d.lon, sw[1]); se[0] = Math.min(d.lat, se[0]); se[1] = Math.min(d.lon, se[1]); // создаем родительский элемент, куда будет лететь святящаяся точка. d.parent = parentHash[d.pkey] || makeParent(d); }); // сортируем согласно параметра даты locations.sort(function(a, b) { return a.timestampMs - b.timestampMs; }); // и формируем id для записей locations.forEach(function(d, i) { d._id = i; }); // устанавливаем отображение карты в оптимальных границах map.fitBounds([sw, se]); // запускаем визуализацию restart(); }; reader.onerror = function () { console.log(reader.error); }; // читаем файл как текстовый reader.readAsText(file); } } function makeParent(d) { var that = {_id : d.pkey}; // создаем объект координат для leaflet that.latlng = new L.LatLng(d.lat, d.lon); // получаем всегда актуальную информацию о позиции объекта на карте // в зависимости от масштаба that.x = { valueOf : function() { var pos = map.latLngToLayerPoint(that.latlng); return pos.x; } }; that.y = { valueOf : function() { var pos = map.latLngToLayerPoint(that.latlng); return pos.y; } }; return parentHash[that.id] = that; }
Благодаря возможности задавать функцию valueOf для получения значения объекта, мы можем всегда получить точные координаты родительских объектов на карте.
// настройка некоторых параметров подробно по каждому в документации bh.setting.increaseChild = false; bh.setting.createNearParent = false; bh.setting.speed = 100; // чем меньше тем быстрее bh.setting.zoomAndDrag = false; bh.setting.drawParent = false; // не показывать parent bh.setting.drawParentLabel = false; // не показывать подпись родителя bh.setting.padding = 0; // отступ от родительского элемента bh.setting.parentLife = 0; // родительский элемент бессмертен bh.setting.blendingLighter = true; // принцип наложения слове в Canvas bh.setting.drawAsPlasma = true; // частицы рисуются как шарики при использовании градиента bh.setting.drawTrack = true; // рисовать треки частицы var stepDate = 1; // шаг визуализации // во все, практически, функции передается исходные обработанные выше элементы (d) bh.on('getGroupBy', function (d) { // параметр по которому осуществляется выборка данных для шага визуализации return d._id //d.timestampMs; }) .on('getParentKey', function (d) { return d._id; // ключи идентификации родительского элемента }) .on('getChildKey', function (d) { return 'me'; // ключ для дочернего элемента, то есть он будет только один }) .on('getCategoryKey', function (d) { return 'me; // ключ для категории дочернего элемента, по сути определяет его цвет }) .on('getCategoryName', function (d) { return 'location'; // наименование категории объекта }) .on('getParentLabel', function (d) { return ''; // подпись родительского элемента нам не требуется }) .on('getChildLabel', function (d) { return 'me'; // подпись дочернего элемента }) .on('calcRightBound', function (l) { // пересчет правой границы для выборки дочерних элементов из набора для шага визуализации. return l + stepDate; }) .on('getVisibleByStep', function (d) { return true; // всегда отображать объект }) .on('getParentRadius', function (d) { return 1; // радиус родительского элемента }) .on('getChildRadius', function (d) { return 10; // радиус летающей точки }) .on('getParentPosition', function (d) { return [d.x, d.y]; // возвращает позицию родительского элемента на карте }) .on('getParentFixed', function (d) { return true; // говорит что родительский объект неподвижен }) .on('processing', function(items, l, r) { // запускаем таймер чтобы пересчитать heatmap setTimeout(setMarkers(items), 10); }) .sort(null) ; // возвращает функцию для пересчета heatmap function setMarkers(arr) { return function() { arr.forEach(function (d) { var tp = d.parentNode.nodeValue; // добавляем координаты родительского объекта в heatmap heat.addLatLng(tp.latlng); }); } }
Как работает библиотека. При запуске она анализирует предоставленные ей данные выявляя родительские и дочерние уникальные элементы. Определяет границы визуализации согласно функции переданной для события getGroupBy. За тем запускает два d3.layout.force один отвечает за расчет позиции родительских элементов, другой за дочерние элементы. К дочерним элементам еще применяются методы для разрешения коллизий и кластеризации согласно родительского элемента.
При нашей настройке, мы получаем следующее поведение:
- На каждом шаге, который наступает по истечении 100 миллисекунд (bh.setting.speed = 100) библиотека выбирает всего один элемент из исходных данных;
- вычисляет его положение относительно родительского элемента;
- начинает отрисовку и переходит к следующему шаг;
Так как дочерний объект у нас один — он начинает летать от одно родителя к другому. И получается картинка, что приведена в самом начале статьи.

Заключение
Библиотека делалась для решения собственных задач, так как после публикации GitHub Visualizer, появилось некоторое кол-во заказов переделать его под различные нужды, а некоторые хотели просто разобраться что да как изменить в нем чтоб решить свою проблему.
В результате я вынес все необходимое для того чтобы создавать визуализации на подобии GitHub Visualizer в отдельную библиотеку и уже сделал ряд проектов один из которых занял первое место на конкурсе ГосЗатраты.
Собственно упрощенный GitHub Visualizer на blackHole.js работающий с xml Файлами полученными при запуске code_swarm можно пощупать тут.
Для генерации файла можно воспользоваться этим руководством
Надеюсь что появятся соавторы которые внесут свои улучшения и поправят мои заблуждения.
На данный момент библиотека состоит из 4 составных частей:
- Parser — создание объектов для визуализации из переданных данных
- Render — занимается отрисовкой картинки
- Processor — вычисление шагов визуализации
- Core — собирает в себя все части, управляет ими и занимается расчетом позиции объектов
В ближайшее время планирую вынести Parser и Render в отдельные классы, чтоб облегчить задачу подготовки данных и предоставить возможность рисовать не только на canvas, но и при желании на WebGL.
Жду полезных комментариев!
Спасибо!
P.S. Друзья прошу писать про ошибки в личные сообщения.

