Last active
February 9, 2017 06:04
-
-
Save raheelahmad/85da710762dcb3c5f1b8508769b25d26 to your computer and use it in GitHub Desktop.
[Tufte] Train times on a spine
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
border: no | |
license: mit | |
height: 650 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
node_modules | |
.vscode |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<title>D3 Examples</title> | |
<meta charset="utf-8"> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<link href="https://fonts.googleapis.com/css?family=Fira+Sans+Condensed" rel="stylesheet"> | |
<style> | |
body { | |
background-color: #FFF; | |
font-family: 'Fira Sans Condensed'; | |
font-size: 12px; | |
} | |
.station-name { | |
font-size: 13px; | |
font-weight: 600; | |
fill: #333; | |
} | |
.hour { | |
font-weight: 600; | |
fill: #333; | |
} | |
rect.hour, rect.station-rect { | |
fill: #F3EEE6; | |
stroke: #C9C0C0; | |
stroke-width: 0.25; | |
shape-rendering: crispEdges; | |
} | |
rect.hour-line { | |
fill: #C9C0C0; | |
stroke: 'none'; | |
shape-rendering: crispEdges; | |
} | |
rect.minute { | |
fill: none; | |
stroke: none; | |
shape-rendering: crispEdges; | |
} | |
.minute{ | |
stroke: none; | |
fill: #333; | |
} | |
.busy { | |
fill: #555; | |
shape-rendering: none; | |
stroke: none; | |
stroke-width: 0px; | |
} | |
.central, .superspeedy { | |
fill: none; | |
stroke: #555; | |
stroke-width: 1px; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="graph"></div> | |
<div style="width: 420px; margin-left: 20px; line-height: 17px"> | |
<strong>Visualizing Train Times</strong> | |
From <em>Visualizing Information</em>, a great example of saving "ink". By omitting redundant information, we can let | |
the more interesting, and varying, information come to the surface. The graphic shows train times on two platforms, '5 • 6' and '7 • 8'. Instead of repeating the hours, they are laid out on the spine, so the minutes become more visible. | |
This reproduction also makes a feeble attempt of reproducing the legend markers for some times. | |
</div> | |
<script src="trains.js"></script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"dependencies": { | |
"d3": "^4.4.1" | |
}, | |
"devDependencies": { | |
"browser-sync": "^2.18.7" | |
} | |
} |
We can make this file beautiful and searchable if this error is corrected: It looks like row 2 should actually have 3 columns, instead of 2 in line 1.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Platform,Time,Extras | |
"5 • 6",5:25 | |
"5 • 6",5:47 | |
"5 • 6",6:14 | |
"5 • 6",6:32 | |
"5 • 6",6:54 | |
"5 • 6",7:07 | |
"5 • 6",7:12 | |
"5 • 6",7:24 | |
"5 • 6",7:32 | |
"5 • 6",7:55 | |
"5 • 6",8:01,"central" | |
"5 • 6",8:07 | |
"5 • 6",8:12 | |
"5 • 6",8:21 | |
"5 • 6",8:34 | |
"5 • 6",8:45 | |
"5 • 6",9:02 | |
"5 • 6",9:12 | |
"5 • 6",9:21 | |
"5 • 6",9:32 | |
"5 • 6",9:52 | |
"5 • 6",10:12 | |
"5 • 6",10:28 | |
"5 • 6",10:42 | |
"5 • 6",10:54 | |
"5 • 6",11:12 | |
"5 • 6",11:21 | |
"5 • 6",11:32 | |
"5 • 6",11:51 | |
"5 • 6",12:20 | |
"5 • 6",12:32 | |
"5 • 6",12:42 | |
"5 • 6",12:50 | |
"5 • 6",13:01 | |
"5 • 6",13:30 | |
"5 • 6",13:34 | |
"5 • 6",13:59 | |
"5 • 6",14:10 | |
"5 • 6",14:28 | |
"5 • 6",14:42 | |
"5 • 6",14:51 | |
"5 • 6",14:54 | |
"5 • 6",15:10 | |
"5 • 6",15:15 | |
"5 • 6",15:24,"superspeedy" | |
"5 • 6",15:33 | |
"5 • 6",15:45 | |
"5 • 6",16:01 | |
"5 • 6",16:12 | |
"5 • 6",16:21 | |
"5 • 6",16:29 | |
"5 • 6",16:43 | |
"5 • 6",17:03 | |
"5 • 6",17:21 | |
"5 • 6",17:23 | |
"5 • 6",17:33 | |
"5 • 6",17:42 | |
"5 • 6",17:45 | |
"5 • 6",17:52 | |
"5 • 6",17:59 | |
"5 • 6",18:12 | |
"5 • 6",18:21 | |
"5 • 6",18:29 | |
"5 • 6",18:32 | |
"5 • 6",18:39 | |
"5 • 6",18:49 | |
"5 • 6",18:52 | |
"5 • 6",18:53 | |
"5 • 6",19:12 | |
"5 • 6",19:19 | |
"5 • 6",19:29 | |
"5 • 6",19:32 | |
"5 • 6",19:33 | |
"5 • 6",19:39 | |
"5 • 6",19:42 | |
"5 • 6",19:50 | |
"5 • 6",20:10 | |
"5 • 6",20:14 | |
"5 • 6",20:21 | |
"5 • 6",20:32 | |
"5 • 6",20:52 | |
"5 • 6",20:59 | |
"5 • 6",21:22 | |
"5 • 6",21:32 | |
"5 • 6",21:39 | |
"5 • 6",21:49 | |
"5 • 6",21:52 | |
"5 • 6",21:58 | |
"5 • 6",22:04 | |
"5 • 6",22:14 | |
"5 • 6",22:23 | |
"5 • 6",22:59 | |
"5 • 6",23:12 | |
"5 • 6",23:42 | |
"5 • 6",0:14 | |
"7 • 8",5:25 | |
"7 • 8",5:54 | |
"7 • 8",6:04 | |
"7 • 8",6:09 | |
"7 • 8",6:20 | |
"7 • 8",6:34 | |
"7 • 8",6:38 | |
"7 • 8",6:47 | |
"7 • 8",6:51 | |
"7 • 8",7:11 | |
"7 • 8",7:19 | |
"7 • 8",7:22 | |
"7 • 8",7:32 | |
"7 • 8",7:39 | |
"7 • 8",7:45 | |
"7 • 8",7:49 | |
"7 • 8",7:52 | |
"7 • 8",7:58 | |
"7 • 8",8:01 | |
"7 • 8",8:09 | |
"7 • 8",8:12 | |
"7 • 8",8:21 | |
"7 • 8",9:21 | |
"7 • 8",9:32 | |
"7 • 8",9:52,"busy" | |
"7 • 8",10:12 | |
"7 • 8",10:28 | |
"7 • 8",10:42 | |
"7 • 8",10:54 | |
"7 • 8",11:12 | |
"7 • 8",11:21 | |
"7 • 8",11:32 | |
"7 • 8",11:51 | |
"7 • 8",12:20 | |
"7 • 8",12:32 | |
"7 • 8",12:42 | |
"7 • 8",12:50 | |
"7 • 8",13:01 | |
"7 • 8",13:30 | |
"7 • 8",13:34 | |
"7 • 8",13:59 | |
"7 • 8",14:10 | |
"7 • 8",14:28 | |
"7 • 8",14:42 | |
"7 • 8",14:51 | |
"7 • 8",14:54 | |
"7 • 8",15:10 | |
"7 • 8",15:15 | |
"7 • 8",15:24 | |
"7 • 8",15:33 | |
"7 • 8",15:45 | |
"7 • 8",16:01 | |
"7 • 8",16:12 | |
"7 • 8",16:21 | |
"7 • 8",16:29 | |
"7 • 8",16:43 | |
"7 • 8",17:03 | |
"7 • 8",17:21 | |
"7 • 8",17:23 | |
"7 • 8",17:33 | |
"7 • 8",17:40 | |
"7 • 8",17:52 | |
"7 • 8",17:59 | |
"7 • 8",18:12 | |
"7 • 8",18:21 | |
"7 • 8",18:29 | |
"7 • 8",18:32 | |
"7 • 8",18:39 | |
"7 • 8",18:49 | |
"7 • 8",18:52 | |
"7 • 8",18:53 | |
"7 • 8",19:12 | |
"7 • 8",19:19 | |
"7 • 8",19:29 | |
"7 • 8",19:32,"superspeedy" | |
"7 • 8",19:33 | |
"7 • 8",19:39 | |
"7 • 8",19:42 | |
"7 • 8",19:50 | |
"7 • 8",20:12 | |
"7 • 8",20:14 | |
"7 • 8",20:21 | |
"7 • 8",20:32 | |
"7 • 8",20:52 | |
"7 • 8",20:59 | |
"7 • 8",21:22 | |
"7 • 8",21:32 | |
"7 • 8",21:39 | |
"7 • 8",21:49 | |
"7 • 8",21:52 | |
"7 • 8",21:58 | |
"7 • 8",22:04 | |
"7 • 8",22:14 | |
"7 • 8",22:23 | |
"7 • 8",22:59 | |
"7 • 8",23:12 | |
"7 • 8",23:42 | |
"7 • 8",0:14 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const margin = {top: 20, right: 20, bottom: 20, left:20}, | |
width = 480 - margin.left - margin.right, | |
height = 440 - margin.top - margin.bottom; | |
const svg = d3.select('div#graph') | |
.append('svg') | |
.attr('width', width + margin.left + margin.right) | |
.attr('height', height + margin.top + margin.bottom); | |
const chart = svg.append('g') | |
.attr('transform', `translate(${margin.left}, ${margin.top})`); | |
const hourScale = d3.scaleBand() | |
.range([0, height]) | |
// first station's | |
const leftToRightMinutesScale = d3.scaleBand() | |
// second station's | |
const rightToLeftMinutesScale = d3.scaleBand() | |
d3.csv('trains.csv', data => { | |
const {stationTimes: stationTimes, hours: hours, maxMinutesNum: maxMinutesNum} = processData(data) | |
renderHourSpine(hours) | |
renderStationNames(stationTimes) | |
renderStationTimes(stationTimes, maxMinutesNum) | |
}) | |
function renderStationTimes(stationTimes, maxMinutesNum) { | |
// we want minutes rects to have same width as hour rects | |
const maxMinutesWidth = hourScale.bandwidth() * maxMinutesNum | |
leftToRightMinutesScale | |
.range([maxMinutesWidth, 0]) | |
.domain(d3.range(maxMinutesNum)) | |
rightToLeftMinutesScale | |
.range([hourScale.bandwidth(), maxMinutesWidth]) | |
.domain(d3.range(maxMinutesNum)) | |
// left and right station containers | |
const station = chart.selectAll('g.station-times') | |
.data(stationTimes) | |
.enter().append('g') | |
.attr('transform', (d,i) => { | |
if (i == 0) { | |
return `translate(${width/2-maxMinutesWidth}, 0)` | |
} else { | |
return `translate(${i*width/2}, 0)` | |
} | |
}) | |
.attr('class', 'station-times') | |
// rows for station hour | |
const stationHourRows = station.selectAll('g.station-hour') | |
.data(d => { | |
return d.values | |
}) | |
.enter().append('g') | |
.attr('class', 'station-hour') | |
.attr('transform', d => `translate(0, ${hourScale(+d[0])})`) | |
const minutes = stationHourRows | |
.selectAll('g.minute') | |
.data(d => d[1]) | |
.enter().append('g') | |
.attr('class', 'minute') | |
.attr('transform', (d, i) => { | |
const scale = d.stationIndex == 0 ? leftToRightMinutesScale : rightToLeftMinutesScale | |
return `translate(${scale(i)}, 0)` | |
}) | |
minutes.each(renderStationMinute) | |
} | |
function renderStationMinute(d, i) { | |
const minuteGroup = d3.select(this) | |
const text = minuteGroup.append('text') | |
.text(d => d.minutes) | |
.attr('y', hourScale.bandwidth()/2) | |
.attr('x', leftToRightMinutesScale.bandwidth()/2) | |
.attr('dy', 2.0) | |
.attr('text-anchor', 'middle') | |
if (!d.extras) { return } | |
text.style('font-size', '9px') | |
.attr('dy', 2) | |
if (d.extras.includes("central")) { | |
minuteGroup.append('circle') | |
.attr('class', 'central') | |
.attr('cx', leftToRightMinutesScale.bandwidth()/2) | |
.attr('cy', leftToRightMinutesScale.bandwidth()/2-1) | |
.attr('r', leftToRightMinutesScale.bandwidth()/2-4) | |
} | |
if (d.extras.includes("busy")) { | |
minuteGroup.append('circle') | |
.attr('class', 'busy') | |
.attr('cx', leftToRightMinutesScale.bandwidth()/2) | |
.attr('cy', 3) | |
.attr('r', 2) | |
} | |
if (d.extras.includes('superspeedy')) { | |
minuteGroup.append('rect') | |
.attr('class', 'superspeedy') | |
.attr('x', leftToRightMinutesScale.bandwidth()/2 - 2) | |
.attr('y', 0) | |
.attr('width', 4) | |
.attr('height', 4) | |
} | |
} | |
function renderStationNames(stationTimes) { | |
const station1 = stationTimes[0].key | |
const station2 = stationTimes[1].key | |
chart.append('rect') | |
.attr('class', 'station-rect') | |
.attr('y', -margin.top+1) | |
.attr('width', width) | |
.attr('height', margin.top-1) | |
chart.selectAll('.station-name') | |
.data([station1, station2]) | |
.enter().append('text') | |
.attr('class', 'station-name') | |
.text(d => d) | |
.attr('x', (d, i) => i * (width/2 + hourScale.bandwidth()) + margin.left/2) | |
.attr('dy', -margin.top/4-1) | |
} | |
function renderHourSpine(hours) { | |
hourScale.domain(hours) | |
const hourGroups = chart.selectAll('g.spine') | |
.data(hours) | |
.enter().append('g') | |
.attr('class', 'spine') | |
.attr('transform', d => `translate(${width/2}, ${hourScale(d)})`) | |
hourGroups.append('rect') | |
.attr('class', 'hour') | |
.attr('width', hourScale.bandwidth()) | |
.attr('height', hourScale.bandwidth()) | |
hourGroups.append('rect') | |
.attr('class', 'hour-line') | |
.attr('x', -width/2) | |
.attr('y', hourScale.bandwidth()-1) | |
.attr('width', width) | |
.attr('height', 1) | |
hourGroups.append('text') | |
.text(d => d) | |
.attr('class', 'hour') | |
.attr('text-anchor', 'middle') | |
.attr('dy', 2) | |
.attr('x', hourScale.bandwidth()/2) | |
.attr('y', hourScale.bandwidth()/2) | |
} | |
// --- Data | |
function processData(data) { | |
const stationTimes = d3.nest() | |
.key(d => d.Platform) | |
.entries(data) | |
let hours = [] | |
let maxMinutesNum = 0 | |
stationTimes.forEach((station, idx) => { | |
const byTime = d3.nest() | |
.key(d => d.Time.split(':')[0]) | |
.sortKeys((a, b) => d3.ascending(+a, +b)) // sort by ascending hours | |
.entries(station.values) | |
let byTimeSimplified = [] | |
byTime.forEach((v, k) => { | |
const times = v.values.map(d => ({ | |
hour: k, | |
minutes: +(d.Time.split(':')[1]), | |
stationIndex: idx, | |
extras: !d.Extras ? null : d.Extras.split(',') | |
})) | |
byTimeSimplified.push([v.key, times]) | |
maxMinutesNum = d3.max([maxMinutesNum, times.length]) | |
}) | |
hours = hours.concat(byTime.map(d => d.key)) | |
station.values = byTimeSimplified | |
}) | |
hours = [...new Set(hours)].sort((a, b) => d3.ascending(+a, +b)) | |
hours = hours.slice(1) | |
hours.push('0') | |
return {stationTimes: stationTimes, hours: hours, maxMinutesNum: maxMinutesNum} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment