|
<!DOCTYPE html> |
|
<style> |
|
body { |
|
font-family: sans-serif; |
|
} |
|
|
|
#dropdown { |
|
padding: 5px; |
|
width: 100%; |
|
font-size: 12px; |
|
} |
|
|
|
#select { |
|
width: 15%; |
|
float: left; |
|
} |
|
|
|
#chart { |
|
margin-left: 15%; |
|
} |
|
|
|
path.pathAll { |
|
opacity: 1; |
|
stroke: darkred; |
|
stroke-width: 2.5px; |
|
} |
|
|
|
.axis--x path { |
|
display: none; |
|
} |
|
|
|
path.unselected { |
|
stroke: grey; |
|
stroke-width: 1px; |
|
opacity: 0.3; |
|
} |
|
|
|
path.selected { |
|
stroke: black; |
|
stroke-width: 2px; |
|
opacity: 1; |
|
} |
|
|
|
text.unselected { |
|
opacity: 0; |
|
} |
|
|
|
text.selected { |
|
opacity: 1; |
|
} |
|
</style> |
|
<body> |
|
<div id="container"> |
|
<div id="select"><select id="dropdown"></select></div> |
|
<div id="chart"></div> |
|
</div> |
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
<script type="text/javascript"> |
|
var margin = {top: 70, right: 30, bottom: 30, left: 30}, |
|
outerWidth = 800, |
|
outerHeight = 500, |
|
width = outerWidth - margin.left - margin.right, |
|
height = outerHeight - margin.top - margin.bottom; |
|
|
|
var parseTime = d3.timeParse("%Y-%m-%d"); |
|
|
|
var formatPctAxis = x => d3.format("+.0%")(x - 1), |
|
formatPctCaption = x => d3.format(".1%")(Math.abs(x - 1)); |
|
|
|
var buildCaption = (id, routename, initial, final, pct, system) => { |
|
if (!system) { |
|
captionTitle.text("CTA Bus #" + id + " " + routename); |
|
} else { |
|
captionTitle.text("All Bus Routes"); |
|
}; |
|
caption.text("Ridership " + (initial < final ? "increased " : "decreased ") + |
|
formatPctCaption(pct) + " between 2001 and 2016, going from " + initial.toLocaleString() + |
|
" riders in 2001 to " + final.toLocaleString() + " riders in 2016." ); |
|
} |
|
|
|
var x = d3.scaleTime() |
|
.range([0, width]) |
|
.domain([parseTime("2001-01-01"), parseTime("2016-01-01")]); |
|
|
|
var y = d3.scaleLog() |
|
.range([height, 0]) |
|
.domain([0.22,3.5]); |
|
|
|
var xAxis = d3.axisBottom() |
|
.scale(x); |
|
|
|
var yAxis = d3.axisLeft() |
|
.scale(y) |
|
.tickSize(-width, 0) |
|
.tickFormat(formatPctAxis) |
|
.tickSizeOuter(0); |
|
|
|
var line = d3.line() |
|
.x(d => x(d.date)) |
|
.y(d => y(d.pct)); |
|
|
|
var svg = d3.select("div#chart").append("svg") |
|
.attr("width", outerWidth) |
|
.attr("height", outerHeight) |
|
.append("g") |
|
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); |
|
|
|
svg.append("g") |
|
.attr("class", "axis axis--x") |
|
.attr("transform", "translate(0," + height + ")") |
|
.call(xAxis); |
|
|
|
var gY = svg.append("g") |
|
.attr("class", "axis axis--y") |
|
.style("stroke-dasharray", "1 1") |
|
.call(yAxis); |
|
|
|
gY.append("text") |
|
.attr("class", "axis-title") |
|
.attr("transform", "rotate(-90)") |
|
.attr("y", 6) |
|
.attr("dy", ".71em") |
|
.style("fill", "black") |
|
.text("Change in Ridership"); |
|
|
|
var captionTitle = svg.append("text") |
|
.attr("class", "caption-title") |
|
.attr("x", 0) |
|
.attr("y", 0 - margin.top / 2) |
|
.attr("dy", "-1em") |
|
.attr("text-anchor", "left") |
|
.style("font-size", "16px") |
|
.style("font-weight", "bold"); |
|
|
|
var caption = svg.append("text") |
|
.attr("class", "caption") |
|
.attr("x", 0) |
|
.attr("y", 0 - margin.top / 2) |
|
.attr("text-anchor", "left") |
|
.style("font-size", "12px"); |
|
|
|
svg.append("clipPath") |
|
.attr("id", "clip") |
|
.append("rect") |
|
.attr("width", width) |
|
.attr("height", height); |
|
|
|
d3.queue() |
|
.defer(d3.csv, "ridership.csv", type) |
|
.defer(d3.csv, "route_list.csv") |
|
.await(ready); |
|
|
|
function ready(error, data, route_list) { |
|
if (error) throw error; |
|
|
|
baseValues = data[0] |
|
finalValues = data[data.length - 1] |
|
|
|
var busRoutes = data.columns.slice(1).map((id, i) => { |
|
return { |
|
id: id, |
|
routename: route_list[i].routename, |
|
initial: baseValues[id], |
|
final: finalValues[id], |
|
values: data.map((d) => { |
|
return {date: d.date, pct: d[id] / baseValues[id]}; |
|
}) |
|
}; |
|
}); |
|
|
|
values = busRoutes.reduce((a,b) => a.concat(b.values), []); |
|
|
|
d3.select("#dropdown") |
|
.selectAll("option") |
|
.data(busRoutes.sort((a, b) => a.id.localeCompare(b.id, undefined, {numeric: true, sensitivity: 'base'}))) |
|
.enter().append("option") |
|
.attr("value", (d, i) => i) |
|
.text(d => d.id + " " + d.routename); |
|
|
|
var dropDown = d3.select("#dropdown"); |
|
|
|
dropDown.on("change", () => { |
|
var route = busRoutes[d3.event.target.value] |
|
|
|
d3.selectAll(".selected") |
|
.attr("class", "unselected"); |
|
|
|
d3.select("path#path" + route.id) |
|
.attr("class", "selected"); |
|
|
|
d3.select("#text" + route.id) |
|
.attr("class", "selected"); |
|
|
|
if (route.id != "All Buses") { |
|
buildCaption(route.id, route.routename, route.initial, route.final, route.values[route.values.length - 1].pct, false); |
|
} else { |
|
buildCaption(route.id, route.routename, route.initial, route.final, route.values[route.values.length - 1].pct, true); |
|
}; |
|
}); |
|
|
|
d = busRoutes[busRoutes.length - 1]; |
|
buildCaption(d.id, d.routename, d.initial, d.final, d.values[d.values.length - 1].pct, true); |
|
|
|
var route = svg.append("g") |
|
.attr("class", ".route") |
|
.selectAll("g") |
|
.data(busRoutes) |
|
.enter().append("g") |
|
.attr("class", d => "route" + d.id); |
|
|
|
route.append("path") |
|
.attr("id", d => "path" + d.id) |
|
.attr("class", d => d.id == "All Buses" ? "path" + d.id : "unselected") |
|
.attr("clip-path", "url(#clip)") |
|
.attr("d", d => line(d.values)) |
|
.attr("fill", "none") |
|
.attr("stroke", "grey") |
|
.attr("opacity", 0.3) |
|
.attr("stroke-width", 1) |
|
.on("mouseover", function(d) { |
|
d3.selectAll(".selected") |
|
.attr("class", "unselected"); |
|
|
|
if (d3.select(this).attr("class") != "pathAll Buses") { |
|
|
|
d3.select(this) |
|
.attr("class", "selected"); |
|
|
|
d3.select("#text" + d.id) |
|
.attr("class", "selected"); |
|
|
|
buildCaption(d.id, d.routename, d.initial, d.final, d.values[d.values.length - 1].pct, false); |
|
} else { |
|
buildCaption(d.id, d.routename, d.initial, d.final, d.values[d.values.length - 1].pct, true); |
|
}; |
|
}); |
|
|
|
route.append("text") |
|
.datum(d => { return {id: d.id, value: d.values[d.values.length - 1]}}) |
|
.attr("class", d => d.id == "All Buses" ? "textAll" : "unselected") |
|
.attr("id", d => "text" + d.id) |
|
.attr("transform", d => "translate(" + x(d.value.date) + "," + y(d.value.pct) + ")") |
|
.attr("x", 3) |
|
.attr("dy", "0.35em") |
|
.style("font", "10px sans-serif") |
|
.text(d => d.id); |
|
|
|
t = d3.select('.textAll').attr("transform"); |
|
d3.select(".textAll") |
|
.text("All Routes") |
|
.attr("transform", t + " rotate(-90)") |
|
.attr("dx", "-3em") |
|
.attr("dy", "3em") |
|
.style("fill", "darkred"); |
|
} |
|
|
|
function type(d, _, columns) { |
|
d.date = parseTime(d.date); |
|
for (var i = 1, n = columns.length, c; i < n; ++i) d[c = columns[i]] = +d[c]; |
|
return d; |
|
} |
|
|
|
</script> |
|
</body> |