Marey's Schedule Chart main source: ··· Home

Notes:

  • Added titles with information in stops and trains
  • Add box shadow
  • Add range input

Data: data.tsv

Code:

  • mareys-schedule.ejs

    <%  demo = {category: "d3js", key: "mareys-schedule", files: files}; %>
    
    <%- partial("../mixins/_demo_title", demo) %>
    
    <p><div class="slider"></div></p>
    <div id="chart" class="<%= demo.key %>-chart" style="margin-top: 30px;"></div>
    
    <script type="text/javascript" src="/vendors/bower/d3/d3.min.js"></script>
    <script type="text/javascript" src="/vendors/bower/jquery-ui/jquery-ui.min.js"></script>
    <script type="text/javascript" src="/vendors/bower/jquery-ui/ui/slider.js"></script>
    <link rel="stylesheet" type="text/css" href="/vendors/bower/jquery-ui/themes/base/theme.css">
    <link rel="stylesheet" type="text/css" href="/vendors/bower/jquery-ui/themes/base/slider.css">
    <script type="text/javascript" src="/js/d3js-utils.js"></script>
    <script type="text/javascript" src="/js/d3js/<%= demo.key %>.js"></script>
    
    <%- partial("../mixins/_notes", demo) %>
    <%- partial("../mixins/_sources_data", demo) %>
    <%- partial("../mixins/_code", demo) %>
  • mareys-schedule.coffee

    ready = ->
      trains = false
      stations = []
      margin = {top: 80, right: 50, bottom: 50, left: 120}
      width =  $('#chart').innerWidth() - margin.left - margin.right
      height = 600 - margin.top - margin.bottom
    
      formatTime = d3.time.format('%I:%M%p')
    
      parseTime = ((s)->
        t = formatTime.parse(s)
        if t isnt null and t.getHours() < 3 then t.setDate(t.getDate() + 1)
        return t
      )
    
      type = ((d, i)->
        if not i
          for k of d
            if /^stop\|/.test(k)
              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((s)-> {station: s, time: parseTime(d[s.key])})
            .filter((s)-> s.time != null)
        }
      )
    
      formatAMPM = ((date)->
        hours = date.getHours()
        minutes = date.getMinutes()
        ampm = if hours >= 12 then 'PM' else 'AM'
        hours = hours % 12
        hours = if hours then hours else 12
        minutes = if minutes < 10 then '0' + minutes else minutes
        strTime = hours + ':' + minutes + ' ' + ampm
        strTime
      )
    
      redraw = ( (time_range)->
        x = d3.time.scale().domain([parseTime(time_range[0]), parseTime(time_range[1])])
          .range([0, width])
        y = d3.scale.linear().range([0, height])
        xAxis = d3.svg.axis().scale(x).ticks(8).tickFormat(formatTime)
    
        svg = d3utils.svg('#chart', width, height, margin)
        d3utils.middleTitle(svg, width, 'E.J. Marey’s graphical train schedule ' + \
          ' (4:30AM - 1:30AM)', -40)
        d3utils.filterBlackOpacity('trains', svg, 2, .2)
    
        svg.append('defs').append('clipPath').attr('id', 'clip')
          .append('rect').attr('y', -margin.top).attr('width', width)
          .attr('height', height + margin.top + margin.bottom)
    
        y.domain(d3.extent(stations, (d)-> d.distance))
        station = svg.append('g').attr('class', 'station').selectAll('g')
          .data(stations).enter().append('g')
          .attr('transform', (d)-> 'translate(0,' + y(d.distance) + ')')
    
        station.append('text').attr('x', -6).attr('dy', '.35em').text((d)-> d.name)
        station.append('line').attr('x2', width)
        
        svg.append('g').attr('class', 'x top axis').call(xAxis.orient('top'))
    
        svg.append('g').attr('class', 'x bottom axis')
          .attr('transform', 'translate(0,' + height + ')').call(xAxis.orient('bottom'))
    
        mouseover = ((d)->
          d3.select('.train-' + d.index).select('path').style({'stroke-width': '5px'})
        )
        mouseleave = ((d)->
          d3.select('.train-' + d.index).select('path').style({'stroke-width': '2.5px'})
        )
    
        train = svg.append('g').attr('class', 'train').attr('clip-path', 'url(#clip)')
          .selectAll('g').data(trains.filter((d)-> /[NLB]/.test(d.type)))
          .enter().append('g').attr('class', (d,i)-> d.type + ' train-' + d.index)
          .on('mouseover', mouseover).on('mouseleave', mouseleave)
        
        line = d3.svg.line().x((d)-> x(d.time)).y((d)-> y(d.station.distance))
        
        trainTitle = ((train)->
          if train.direction is 'S'
            title = train.stops[0].station.name + ' -> ' + _.last(train.stops).station.name
          else
            title =  _.last(train.stops).station.name + ' -> ' + train.stops[0].station.name
          title
        )
    
        train.append('path').attr('d', (d)-> line(d.stops))
          .append('title').text((d)-> trainTitle(d) )
    
        train.selectAll('circle').data((d)-> d.stops).enter().append('circle')
          .attr('transform', (d)-> 'translate(' + x(d.time) + ',' + y(d.station.distance) + ')')
          .style({filter: 'url(#drop-shadow-trains)'})
          .attr('r', '5px').append('title').text((d)->
            trainTitle(trains[d.train_index]) + '\n' + d.station.name + " at #{formatAMPM(d.time)}"
          )
      )
    
      slider = $('.slider')
    
      # It starts at 4:30AM and ends at 1:30AM
      convertHour = ((val)->
        times = []
        _.each(slider.slider('values'), (slider_value)->
          whole_minutes = slider_value / 100 * 1200
          fragment = 'AM'
          hours = Math.floor( whole_minutes / 60)
          minutes = Math.floor(whole_minutes % 60)
          if minutes > 30
            minutes = minutes - 30
            hours = hours + 1
          else
            minutes = minutes + 30
          hours = hours + 4
          if hours > 23
            if hours is 24
              hours = hours - 12
            else
              hours = hours - 24
          else if hours > 11
            fragment = 'PM'
            hours = hours - 12 if hours isnt 12
          minutes = '0' + String(minutes) if minutes < 10
          final_time = String(hours) + ':' + minutes + fragment
          times.push final_time
        )
        redraw(times)
      )
    
      slider.slider({
        range: true
        change: convertHour
      })
    
      d3.tsv('/data/d3js/mareys-schedule/data.tsv', type, (error, data)->
        trains = data
        _.each(trains, (train, index)->
          train.index = index
          _.each(train.stops, (stop)-> stop.train_index = index)
        )
        slider.slider('values', [10,50])
      )
    
    $(document).ready(ready)
  • _mareys-schedule.styl

    .mareys-schedule-chart
      svg {
        font: 10px sans-serif;
      }
    
      .axis path {
        display: none;
      }
    
      .axis line {
        stroke: #000;
        shape-rendering: crispEdges;
      }
    
      .chart-title { font-size: 15px; }
    
      .station line {
        stroke: #ddd;
        stroke-dasharray: 1,1;
        shape-rendering: crispEdges;
      }
    
      .station text {
        text-anchor: end;
      }
    
      .train path {
        fill: none;
        stroke-width: 2.5px;
      }
    
      .train circle {
        stroke: #fff;
      }
    
      .train .N path { stroke: rgb(34,34,34); }
      .train .N circle { fill: rgb(34,34,34); }
    
      .train .L path { stroke: rgb(183,116,9); }
      .train .L circle { fill: rgb(183,116,9); }
    
      .train .B path { stroke: rgb(192,62,29); }
      .train .B circle { fill: rgb(192,62,29); }
  • d3js-utils.coffee

    d3utils = {
      middleTitle: ((svg, width, text, top = -15)->
        element = svg.append('text').attr({class: 'chart-title', 'text-anchor': 'middle', \
          transform: 'translate(' + String(width / 2) + ',' + top + ')'})
          .text(text).style({'font-weight': 'bold'})
      )
    
      svg: ((selector, width, height, margin)->
        d3.select(selector).text('').append('svg')
          .attr({width: width + margin.left + margin.right, \
            height: height + margin.top + margin.bottom})
          .append('g').attr({transform: 'translate(' + margin.left + ',' + margin.top + ')'})
      )
    
      filterBlackOpacity: ((id, svg, deviation, slope)->
        defs = svg.append('defs')
        filter = defs.append('filter').attr({id: 'drop-shadow-' + id, width: '500%', height: '500%', \
          x: '-200%', y: '-200%'})
        filter.append('feGaussianBlur').attr({in: 'SourceAlpha', stdDeviation: deviation})
        filter.append('feOffset').attr({dx: 1, dy: 1})
        filter.append('feComponentTransfer').append('feFuncA').attr({type: 'linear', slope: slope})
        feMerge = filter.append('feMerge')
        feMerge.append('feMergeNode')
        feMerge.append('feMergeNode').attr('in', 'SourceGraphic')
      )
    
      filterColor: ((id, svg, deviation, slope, extra = false)->
        defs = svg.append('defs')
        filter = defs.append('filter').attr({id:'drop-shadow-' + id})
        filter.attr({width: '500%', height: '500%', x: '-200%', y: '-200%'}) if extra
        filter.append('feOffset').attr({result: 'offOut', in: 'SourceGraphic', dx: .5, dy: .5})
        filter.append('feGaussianBlur')
          .attr({result: 'blurOut', in: 'offOut', stdDeviation: deviation})
        filter.append('feBlend').attr({in: 'SourceGraphic', in2: 'blurOut', mode: 'normal'})
        filter.append('feComponentTransfer').append('feFuncA').attr({type: 'linear', slope: slope})
      )
    
    
    
      tooltip: ((selector, customOpts = {})->
        defaultOpts = {
          followMouse: false, followElement: false, elementSelector: ''
          leftOffst: 60, topOffst: 40
          tOpts: {container: 'body', viewport: {selector: '#chart svg'}}
        }
        opts = _.merge(defaultOpts, customOpts)
    
        # Bootstrap tooltip.
        $(selector).tooltip(opts.tOpts)
    
        # For SVG forms, it is better to position the tooltip manually.
        if opts.followMouse
          $(selector).hover((e)->
            $('.tooltip')
              .css({top: String(e.pageY - opts.topOffst) + 'px', \
                left: String(e.pageX - opts.leftOffst) + 'px'})
          )
        else if opts.followElement
          $(selector).hover((e)->
            $('.tooltip')
              .css({top: String($(opts.elementSelector).position().top - opts.topOffst) + 'px', \
                left: String($(opts.elementSelector).position().left - opts.leftOffst) + 'px'})
          )
      )
    
    
      colorsScale: ((colors, extent)->
        c = d3.scale.linear().domain(extent).range([0,1])
        colorScale = d3.scale.linear()
          .domain(d3.range(0, 1, 1.0 / (colors.length))).range(colors)
        return ((p)-> colorScale(c(p)))
      )
    
    }
    
    window.d3utils = d3utils

Home