Расписание подмосковных электричек на диаграмме Маре (Marey’s Trains) c помощью d3.js

d3
javascript
R
визуализация
не-экономика
Начнем сразу с итогового результата, который выглядит вот так: Для удобства лучше открыть его в полном окне. Как читать эту диаграмму (если не очень понятно): по оси X - время (в данном случае - с…
Автор

Марсель Салихов

Дата публикации

13 июня 2013 г.

Начнем сразу с итогового результата, который выглядит вот так:
Для удобства лучше открыть его в полном окне.

Как читать эту диаграмму (если не очень понятно):

Немного об истории вопроса

Такое представление расписания поездов первым представил в своей книге 1885 года (128 лет назад!) La m éthode graphique французский ученый Этьен-Жюль Маре. Таким образом в книге было изображено расписание поездов между Парижем и Лионом.

Тем, кто занимается графической визуализацией, это представление во многом известно благодаря культовой книге Эдварда Тафте (Edward Tafte) - “The Visual Display of Quantiative Information”. Именно диаграмма Маре вынесена на обложку книги, и Тафте по ходу несколько раз возвращается к этой диаграмме. Такое представление соответствует “принципам графического совершенства” (principles of graphical excellence) от Тафте - хорошая визуальная презентация данных является сочетанием сути вопроса, статистики и дизайна. Сложные идеи представлены ясно, точно и эффективно. Читатель получает максимальное количество идей в минимальный период времени и с минимальным количеством “чернила” на единицу пространства.

Разумеется, с помощью современных технологий подобной репрезентации можно придать интерактивность и дополнительное удобство представления.

К примеру, вот пример расписания пригородных поездов Сан-Франциско в стиле Маре, созданный дизайнером Nicholas Rougeux. Недавно я увидел аналогичный пример, созданный в d3 автором этой удивительной библиотеки (Mike Bostock) и решил сделать аналогичную визуализацию для наших данных в качестве урока по изучению d3, создания визуализации в стиле Тафте, а также потому, что мне кажется, что используемые представления расписания электричек как на самих станциях, так и в Интернете, можно значительно улучшить.

Исходные данные.
Разумеется, сначала необходимо получить исходные данные - расписание движения пригородных поездов в удобном для машинной обработки виде. Как ни удивительно, но официальный перевозчик - ОАО «Центральная пригородная пассажирская компания» - не предоставляет общественности подобной информацииии. Раздел “Он-лайн табло” на официальном сайте находится в “стадии наполнения”.
Почему-то компания, имеющая почти $1 млрд ежегодной выручки и более $100 млн чистой прибыли, не нашла возможности сообщать пассажирам о расписании движения. Правительство г. Москвы также не имеет подобной информации. Широко разрекламированный “Портал открытых данных” содержит информацию о стоимости проезда, но не расписании движения пригородных поездов.

Разумеется, все кто ездил на электричках, знают альтернативные источники информации:) Это конечно, Яндекс и tutu.ru. Обращает кстати внимание, что оба сервиса используют довольно скудное текстовое представление расписания.

Я написал небольшой скрипт в R, который собирает данные с tutu.tu, хотя концептуально это не очень правильно - перевозчик, либо местные власти должны предоставлять подобную информацию в удобном виде.
В нашем случае для Ленинградского направления, но разумеется, можно собрать информацию по и другим направлениям/вокзалам. Причем информацию по расстояниями между станциями пришлось “парсить” тоже с tutu.ru, так как я не смог найти какой-либо официальной информации по этому поводу.

Итоговый файл представляет собой матрицу, в котором по столбцам идут остановки, а по строкам - отдельные поезда. В названии столбцам закодированы расстояния между станциями и зоны оплаты. Выглядит это вот так:

Соответственно мы видим, что на Ленинградском направлении в день проходит 165 поездов (по обоим направлениям), которые останавливаются на 45 различных станциях. Матрица состоит из почти трех тысяч значений, поэтому необходимо специальные средства представления этой информации в графическом виде.

Построение диаграммы в стиле Маре.
Как уже говорилось, я использовал графическую библиотеку Data-Driven Documents (или d3), которая представляет собой нечто среднее между библиотеками готовых графиков и самостоятельным рисованием диаграмм в графическом редакторе вроде Inkscape.
Это мой второй опыт самостоятельного рисования в d3 (первый - карты хороплет для РФ). Поэтому я основывался на готовом примере Майка, но несколько видоизменил его для своих данных и дополнил дополнительными интерактивными элементами.
Вариант с этими “плюшечками” занимает около 300 строк кода, но вполне возможно, потому что я использовал не самые оптимальные конструкции.

Основная “рабочая” функция преобразует матрицу поезда-станции и преобразует ее в JS-объект. На основе этого объекта собственно и строится вся графическая составляющая.

function type(d, i) {

  // Extract the stations from the "stop|*" columns.
  if (!i) for (var k in d) {
    if (/^stop\|/.test(k)) {
      var p = k.split("|");
      stations.push({
        key: k,
        name: p[1],
        distance: +p[2],
        zone: +p[3]
      });
    }
  }

  return {
    number: d.number,
    type: d.type,
    direction: d.direction,
    stops: stations

.map(function(s) { return {station: s, time: parseTime(d[s.key])}; }) .filter(function(s) { return s.time!= null; }) };

Информационное окно

В дополнении примеру Майка я добавил еще всплывающее окошко, которое появляется при наведении мышкой на любую круг - станцию. Делается это с помощью простого event-listener, реагирующего на mouseover.

train.selectAll("circle")

.data(function(d) { return d.stops; }) .enter().append(“circle”) .attr(“transform”, function(d) { return “translate(” + x(d.time) + “,” + y(d.station.distance) + “)”; }) .attr(“r”, 3) .on(“mouseover”, function(d) { var xPosition = x(d.time)+margin.left + margin.right; var yPosition = y(d.station.distance);

      d3.select("#tooltip")

.style(“left”, xPosition + “px”) .style(“top”, yPosition + “px”)
.select(“#stations”) .text(d.station.name); d3.select(“#tooltip”) .select(“#time”) .text(formatTime(d.time)); d3.select(“#tooltip”).classed(“hidden”, false);

            })

.on(“mouseout”, function() { d3.select(“#tooltip”).classed(“hidden”, true); });

Соответственно объект tooltip то появляется и наполняется информационным содержанием (название станции и время прибытия поезда на эту станцию), то исчезает.

Выбор направления

В исходном примере Майка отображается только одно направление, я же хотел, чтобы была возможность посмотреть оба направления одновременно, а также по отдельности. Это делается с помощью отдельной формы и функции, которая реагирует на изменение этой формы.

d3.selectAll("input[name=f_direction]")

.on(“change”, function(){ cur_direction = this.id; if (this.id == ‘moscow’){ d3.selectAll(“.moscow”).classed(“hidden”, false); d3.selectAll(“.oblast”).classed(“hidden”, true); } if (this.id == ‘oblast’){ d3.selectAll(“.moscow”).classed(“hidden”, true); d3.selectAll(“.oblast”).classed(“hidden”, false); } })

Соответственно все объекта класса “oblast” или “moscow” показываются, либо скрываются от зрителя. Переменная cur_direction нужна для того, чтобы правильно реагировать на выбор для недели, когда направление уже выбрано.

Выбор дня недели

Исходные данные имеют для каждого поезда его тип: ежедневно, по выходным, по рабочим, кроме суббот, кроме воскресений, отменен. Но пользователя на самом деле интересует какие поезда идут в конкретный день, поэтому я решил целесообразнее предоставить выбор для недели и в зависимости от этого рисовать нужные поезда. Соответственно, поезд, который ходит в режим “кроме воскресений” не будет отображаться в при выборе воскресенья. При выборе субботы или воскресенья также не будут показываться электрички, которые ходят по рабочим дням.

Реализация этой логики сделана топорным if:) Отмененные электрички (красным цветом) должны отображаться при любом раскладе

d3.selectAll("input[name=type_train]")

.on(“change”, function(){

      if (this.id == 'working'){
        d3.selectAll(".daily"+"."+cur_direction).classed("hidden", false);
        d3.selectAll(".working"+"."+cur_direction).classed("hidden", false);
        d3.selectAll(".weekend"+"."+cur_direction).classed("hidden", true);
        d3.selectAll(".ex_saturday"+"."+cur_direction).classed("hidden", true);
        d3.selectAll(".ex_sunday"+"."+cur_direction).classed("hidden", true);
      }
      if (this.id == 'saturday'){
        d3.selectAll(".daily"+"."+cur_direction).classed("hidden", false);
        d3.selectAll(".working"+"."+cur_direction).classed("hidden", true);
        d3.selectAll(".weekend"+"."+cur_direction).classed("hidden", false);
        d3.selectAll(".ex_saturday"+"."+cur_direction).classed("hidden", true);
        d3.selectAll(".ex_sunday"+"."+cur_direction).classed("hidden", false);
      }  

      if (this.id == 'sunday'){
        d3.selectAll(".daily"+"."+cur_direction).classed("hidden", false);
        d3.selectAll(".working"+"."+cur_direction).classed("hidden", true);
        d3.selectAll(".weekend"+"."+cur_direction).classed("hidden", false);
        d3.selectAll(".ex_saturday"+"."+cur_direction).classed("hidden", false);
        d3.selectAll(".ex_sunday"+"."+cur_direction).classed("hidden", true);
      }  
  })

Вот вроде бы и все. Буду рад услышать любые комментарии по оптимальном представления расписания и на сколько это вообще удобно. Как я понимаю, люди, которые ездят на электричках помнят расписание в пределах нужного часа-двух наизусть и в принципе не слишком нуждаются в графической визуализации. Я, к примеру, езжу на электричках изредка, и оно мне кажется удобным.