13 июн. 2013 г.

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

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




Как читать эту диаграмму (если не очень понятно): 
  • по оси X - время (в данном случае - с 4.30 утра до 12.00). по оси Y - идут станции Ленинградского направления (от Москвы Ленинградской до Твери). Расстояние между названиями по оси соответствует реальному расстоянию между станциями в километрах. Поэтому некоторые станции почти "налезают" друг на друга  расстояние между ними совсем небольшое (по крайней мере, по тем данным, которые у меня есть). 
  • каждая линия представляет собой поезд, каждая точка на линии - станцию. 
  • по умолчанию выбраны оба направления, но можно выбрать отдельно - "в Москву"/"в область". 
  • при наведении мышкой на станцию появляется маленькое окошко, которое показывает название станции и время прибытия. 
  • чем круче линия, тем быстрее идет данный поезд. 
  • Цветом обозначены типы электричек: черный - ежедневно, рыжий - по выходным, фиолетовый - выходные /кроме субботы/кроме воскресенья, красный - электричка отменилась. 
  • Информация по расписанию взята с tutu.ru и соответствует 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);
      }      
  })



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

Комментариев нет: