blog

Create a New York Subway / MTA-style Train Stop list using CSS3 and Javascript

April 17 2010 by Joseph Smith

See the Live example

As a New Yorker, I pretty much have a love/hate/love relationship with the MTA. However, from a design perspective, I do really like the work that has been put into the new cars. There are multiple interactive maps, video screens, and current stop indicators that make navigating the City by train more intuitive.

Working in the field as a Web Developer, the User Experience is always a high priority, so when someone gets UX right, it's always a good idea to analyze the subject matter at hand and figure out WHY it works and HOW... This article is less about the WHY's (although this would make for a good article in the future) and more about duplicating someone else's HOW styled-efforts in a different medium (the Web). My goal is to re-create elements of the NYC R-160 Subway car maps with CSS and JS and sans any images.

Starting out, we need stops. Looking at the R-160's maps, we can glean what kind of data we are dealing with. We have locations (string), transfers (array), and handicap access (which, if this were "real" should be a boolean) in each stop. Placing these in a JSON object is easy enough, so now we have a key "Stops" whose value is an array of, well stops...

{ "Stops" : [ 
          { 
            "slocation" : "Astoria Ditmars Blvd",
            "transfer" : [ "W" ],
            "access" : " "
          },
          { 
            "slocation" : "Astoria Blvd",
            "transfer" : [ "W" ],
            "access" : " "
          },
          { 
            "slocation" : "30th Av",
            "transfer" : [ "W" ],
            "access" : " "
          },
          { 
            "slocation" : "Broadway",
            "transfer" : [ "W" ],
            "access" : " "
          },
          { 
            "slocation" : "36 Av",
            "transfer" : [ "W" ],
            "access" : " "
          },
          { 
            "slocation" : "39 Av",
            "transfer" : [ "W" ],
            "access" : " "
          },
          { 
            "slocation" : "21 St - Queensbridge",
            "transfer" : [ "W" ],
            "access" : "&"
          },
          { 
            "slocation" : "Lexington Av/59 St",
            "transfer" : [ "6", "F", "R", "W" ],
            "access" : " "
          },
          { 
            "slocation" : "5th Av/59 St",
            "transfer" : [ "R", "W" ],
            "access" : " "
          },
          { 
            "slocation" : "57 St/7Av",
            "transfer" : [ "Q", "R", "W" ],
            "access" : " "
          },
          { 
            "slocation" : "49 St",
            "transfer" : [ "1", "2", "3", "7", "A", "C", "E", "Q", "R", "W", "S" ],
            "access" : "&"
          },
          { 
            "slocation" : "34 St - Herald Sq",
            "transfer" : [ "B", "D", "F", "R", "V", "W", "PATH" ],
            "access" : "&"
          },
          { 
            "slocation" : "14 St - Union Sq",
            "transfer" : [ "4", "5", "6", "L", "Q", "R", "W" ],
            "access" : "&"
          },
          { 
             "slocation" : "Canal St",
             "transfer" : [ "4", "6", "J", "M", "Q", "R", "W", "Z" ],
             "access" : " "
           },
          { 
            "slocation" : "Rector St",
            "transfer" : [ "W" ],
            "access" : " "
          },
          { 
             "slocation" : "Whitehall Street",
             "transfer" : [ "1" ],
             "access" : " "
           }
      ]
}

The HTML is simple enough as well. We need only a few divs to act as our containers.

<div id="train-map">
    <div class="stops-wrap">
      <div id="next"></div>
      <hr class="btm"/>
    </div>
    <button id="next-b">Next Stop</button>
    <button id="prev-b">Other Direction</button>
</div>

This leaves the JS which can be taken one block at a time. First we create our mta object with a few properties.

var mta = {}; //simple object w/ a few properties
mta.visibleStops = 10; //10 visible items/stops at time
mta.unformattedStops = []; // will hold the FULL array of stops
mta.formattedStops = []; // will hold the formatted array of stops
mta.sint = 0; //starting int

startTrain() is essentially a wrapper for our other logic and function calls.

mta.startTrain = function(){  
    //format if not already formatted
    if($('#next').children().length === 0){
        for(var i = 0; i < mta.unformattedStops.length; i++){
            mta.formattedStops[i] = mta.formatStop(mta.unformattedStops[i]);
        }
    }
 
    mta.appendStops('forward');
 
    $("#next-b").bind("click", function(e){
        if((mta.unformattedStops.length - mta.sint) > mta.visibleStops ){
            mta.sint++;
            mta.appendStops('forward');
        }
    });
    $("#prev-b").bind("click", function(e){
        mta.appendStops('reverse');
    });
};

appendStops() takes one param, the direction. As you'll see when we reverse the array, we reset the start int ( mta.sint ) to 0. We would probably want to handle this differently by having the train reverse and then start at your current stop (like crossing the platform) but this is a proof of concept, so we'll move along

//creates a temp array and appends it to the dom
mta.appendStops = function(direction){
    if(direction != 'forward'){
        mta.formattedStops.reverse();
        mta.sint = 0;
    }
    var tmpArray = [];
 
    tmpArray = mta.formattedStops.slice(mta.sint,mta.sint+mta.visibleStops);
 
    //make sure we are working w/ a clean slate
    $("#train-map .stops-wrap #next").empty();
 
    for(var i=0; i<mta.visibleStops; i++ ){
        $("#train-map #next").append(tmpArray[i]);
    }
};

formatStop formats the object mostly, but not purely, using the string Buffer style of concatenation.

//formats array objects
mta.formatStop = function(ob) {
    var html = [];
    html.push('<div class="stop">');
    html.push('<span class="location">'+ob.slocation+'<\/span>');
    html.push('<span class="access">'+ob.access+'<\/span>');
    html.push('<span class="transfer">'+ob.transfer+'<\/span>');
    html.push('<hr />');
    html.push('<\/div>');
 
    return html.join(' ');
};

Once the DOM is ready (by jQuery standards) we fire an ajax call to the file containing our JSON object. Defining the dataType as "text" gives us a chance to parse the string of JSON data manually via JSON.parse (available here http://json.org/json2.js), which has no real security impact here (since we can trust the creator), which returns an object (train, in this case).

$(document).ready(function(){
 
    $.ajax({
        url: "http://www.twohard.com/dev_ex/mta_list_using_css_and_js/subwaystops.js",
        dataType: "text",
        success: function(data, textStatus){
            var train = JSON.parse(data);
            mta.unformattedStops = train.Stops;
            mta.startTrain();
        }
    });
 
});

So the summary so far... we have a JSON object of train stops, we have some Javascript for light processing of data and an html document to inject our train stops into. Now it's time for the final piece. The CSS.

Using -webkit and -moz selectors, we will rotate divs containing each stop -45 degrees. This will successfully target Safari and Chrome (webkit-based) as well as Firefox ( the only Gecko-based browser I have tested this on). Opera has no, and I mean NO support for anything CSS3.

As for IE, IF we wanted to do a straight 90 degree turn IE could play in this arena (surprisingly) via filters, however IE filters only support increments of 90 degrees so our 45 deg turns are a no go.

.grid_12{ 
	background-color:#000
	}
.grid_12 *{
	margin:0px;
	padding:0px; 
	}
#train-map{ 
	display:block;
	position:relative;
	/*background-color:#000;*/
	width:940px;
	height:250px;
	margin:0 auto; 
	font-family: "Helvetica Neue", Arial, Helvetica, Geneva, sans-serif;
	}
.stops-wrap{ 
	display: block;
	width:820px;
	height:200px;
	position:relative;
	margin:0 auto;
	}
.stop{ 
	font-size:12px;
	display:block;
	width:200px;
	padding:1px 0 0 3px;;
	background: #131313;
	position: absolute;
	bottom:70px;
	-webkit-transform: rotate(-45deg);
	-moz-transform: rotate(-46deg);
	border:2px solid #000; 
	background-color: #0c0c0c;
	}
	.stop hr{ 
		display:block;
		border:0px;
		width:20px;
		height:2px;
		background-color:#aaa;
		position:absolute; 
		top: 13px; 
		bottom: 6px; 
		left: -22px;
	}
	.stop:nth-child(1){ 
		left:0px;
		border-color: red;
	}
	.stop:nth-child(1) hr{ 
		-webkit-transform: rotate(90deg);
		-moz-transform: rotate(95deg);  
		top: 36px; 
		left: 1px;
		width:12px
	}
	.stop:nth-child(2){ 
		left:70px;
	}
	.stop:nth-child(3){ 
		left:140px;
	}
	.stop:nth-child(4){ 
		left:210px;
	}
	.stop:nth-child(5){ 
		left:280px;
	}
	.stop:nth-child(6){ 
		left:350px;
	}
	.stop:nth-child(7){ 
		left:420px;
	}
	.stop:nth-child(8){ 
		left:490px;
	}
	.stop:nth-child(9){ 
		left:560px;
	}
	.stop:nth-child(10){ 
		left:630px;
		border-color:#aaa
	}
 
.location{ 
	color: #ffa62e;
	float:left;
 
	}
.access{ 
	color: red;
	float:left;
	padding-left:10px;
	}
.transfer{ 
	color: #007516;
	float:left;
	clear:left;
	}	
 
#keys{ 
	display:block;
	position:absolute; 
	bottom: 0;	
	}
hr.btm{ 
	position:absolute; 
	bottom: 0;
	display:block;
	border:0px;
	width: 589px;
	height:2px;
	margin: 0;
	background-color:#aaa; 
	left: 57px; 
	top: 199px;
	}	
 
.grid_12 button{ 
	display:block;
	background-color: #1d7528;
	color: #c1fba4;
	border: 1px solid #00911c;
	padding:4px;
	margin: 16px 0 1px;
	-webkit-border-radius: 9px;
	-moz-border-radius: 9px;
	position:relative;
	}
.grid_12 button:hover{
    cursor: pointer;
}
.grid_12 button#next-b{
    float:left;
    left:320px;
}
.grid_12 button#prev-b{
    float:right;
    right:370px;
}		

See the Live example

Joseph Smith

Joseph Smith

Joseph currently works full-time as a developer for Legwork Studio in Downtown Denver, CO.

In his past life, he was a touring musician and turned screws on/repaired Apple hardware.

Tags