Skip to content

Instantly share code, notes, and snippets.

@raheelahmad
Last active February 9, 2017 06:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save raheelahmad/85da710762dcb3c5f1b8508769b25d26 to your computer and use it in GitHub Desktop.
Save raheelahmad/85da710762dcb3c5f1b8508769b25d26 to your computer and use it in GitHub Desktop.
[Tufte] Train times on a spine
border: no
license: mit
height: 650
node_modules
.vscode
<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>
{
"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.
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
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