Published
Edited
Apr 30, 2021
3 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
d3 = require('d3@6','d3-simple-slider')
Insert cell
covid19 = d3.csvParse(await FileAttachment("COVID-City-Daily@1.csv").text())
Insert cell
Insert cell
bostonData = covid19
.filter(d => d.cityid === "25")
.map(d => (
{
"date":d3.timeParse("%Y/%m/%d")(`${d.year}/${d.month}/${d.day}`),
"new_case_rate":d.new_case_rate
}
))
Insert cell
Insert cell
employment = d3.csvParse(await FileAttachment("Employment-City-Daily.csv").text())
Insert cell
bostonDataEmployment = employment
.filter(d => d.countyfips === "25025"&& d != undefined)
.map(d => (
{
"date":d3.timeParse("%Y/%m/%d")(`${d.year}/${d.month}/${d.day}`),
"emp_combined":d.emp_combined*100,
"emp_low":d.emp_combined_inclow*100,
"emp_middle":d.emp_combined_incmiddle*100,
"emp_high":d.emp_combined_inchigh*100
}
))
Insert cell
Insert cell
ridership = d3.csvParse(await FileAttachment("mbda-gated-station-validations-by-line-seasonally-adjusted@2.csv").text())
Insert cell
ridershipFormatted = ridership
.map(d => (
{
"date":d3.timeParse("%Y/%m/%d")(`${d.year}/${d.month}/${d.day}`),
"line":d.line,
"ridership":d.ridership
}
)
)
Insert cell
ridershipData= ({
blue:ridershipFormatted.filter(d=>d.line==='Blue' && !(d.date < allDates[0])),
red:ridershipFormatted.filter(d=>d.line==='Red' && !(d.date < allDates[0])),
orange:ridershipFormatted.filter(d=>d.line==='Orange' && !(d.date < allDates[0])),
green:ridershipFormatted.filter(d=>d.line==='Green' && !(d.date < allDates[0]))})
Insert cell
Insert cell
validationsAndIncomes = d3.csvParse(await FileAttachment("tabular-station-validations-plus-income@1.csv").text())
Insert cell
validationAndIncomeFormatted=validationsAndIncomes
.map(d => (
{
"date":d3.timeParse("%Y/%m/%d")(`${d.year}/${d.month}/${d.day}`),
"line":d.line,
"station":d.station,
"validation_change":d.validation_change,
"projected_x":d.projected_x,
"median_income":d.median_income
}
)
)
Insert cell
validationAndIncomeData= ({
blue:validationAndIncomeFormatted.filter(d=>d.line==='blue' && !(d.date < allDates[0])),
red_a:validationAndIncomeFormatted.filter(d=>d.line==='red-a' && !(d.date < allDates[0])),
red_b:validationAndIncomeFormatted.filter(d=>d.line==='red-b' && !(d.date < allDates[0])),
green_b:validationAndIncomeFormatted.filter(d=>d.line==='green-b' && !(d.date < allDates[0])),
green_c:validationAndIncomeFormatted.filter(d=>d.line==='green-c' && !(d.date < allDates[0])),
green_d:validationAndIncomeFormatted.filter(d=>d.line==='green-d' && !(d.date < allDates[0])),
green_e:validationAndIncomeFormatted.filter(d=>d.line==='green-e' && !(d.date < allDates[0])),
orange:validationAndIncomeFormatted.filter(d=>d.line==='orange' && !(d.date < allDates[0]))
})
Insert cell
tokenStationTimeseries = validationAndIncomeData.red_a
.filter(d =>d.station === "alewife")
Insert cell
maxStationDate = tokenStationTimeseries[tokenStationTimeseries.length-1].date
Insert cell
incomeDataStatic = ({
blue:validationAndIncomeData.blue.filter(d=>d.date < allDates[1]),
red_a:validationAndIncomeData.red_a.filter(d=>d.date < allDates[1]),
red_b:validationAndIncomeData.red_b.filter(d=>d.date < allDates[1]),
green_b:validationAndIncomeData.green_b.filter(d=>d.date < allDates[1]),
green_c:validationAndIncomeData.green_c.filter(d=>d.date < allDates[1]),
green_d:validationAndIncomeData.green_d.filter(d=>d.date < allDates[1]),
green_e:validationAndIncomeData.green_e.filter(d=>d.date < allDates[1]),
orange:validationAndIncomeData.orange.filter(d=>d.date < allDates[1])
})
Insert cell
Insert cell
height = Math.ceil(width * screen.height / screen.width)
Insert cell
padding_between_charts = Math.floor(height/100)*10
Insert cell
margin = ({top: 20, right: 20, bottom: 60, left: 40})
Insert cell
colors = (
{
covid_off: "gainsboro",
covid_on:"dimgray",
emp_middle:"#8dd3c7",
emp_low: "#bebada",
emp_high:"#fccde5",
emp_combined:"#d9d9d9",
blue:"#244995",
green:"#118342",
green_e:"#bae4b3",
green_d:"#74c476",
green_c:"#31a354",
green_b:"#006d2c",
orange:"#ef7d15",
red:"#e22224",
red_b:"#fc9272",
red_a:"#de2d26"
}
)
Insert cell
allDates = bostonDataEmployment.map(d => d.date)
.filter(d=>d<bostonData[0].date)
.concat(bostonData.map(d=>d.date))
Insert cell
Insert cell
bisectDate = d3.bisector(function(d) { return d.date; }).left;
Insert cell
xScale = d3.scaleBand()
.domain(allDates)
.range([1/3*(margin.left - 2*margin.right + 2*width), width - margin.right])
.paddingInner(0.5)
.paddingOuter(0)
Insert cell
yScale = d3.scaleLinear()
.domain([0, d3.max(bostonData.map(d => d.new_case_rate))])
.range([1/3*(-2*padding_between_charts + height - margin.bottom + 2*margin.top),
margin.top])
Insert cell
function scaleBandInvert(scale) {
var domain = scale.domain();
var paddingOuter = scale(domain[0]);
var eachBand = scale.step();
return function (value) {
var index = Math.floor(((value - paddingOuter) / eachBand));
return domain[Math.max(0,Math.min(index, domain.length-1))];
}
}
Insert cell
xAxis = g => g
.attr("transform", `translate(0,${
1/3*(height - margin.bottom + 2*margin.top - 2*padding_between_charts)
})`)
.call(
d3.axisBottom(xScale)
.tickFormat(d3.timeFormat("%b %y"))
.tickValues(xScale.domain().filter(d => d.getDate() == 1 && (d.getMonth() % 2)==0))
.tickSizeOuter(0))
Insert cell
yAxis = g => g
.attr("transform", `translate(${1/3*(margin.left - 2*margin.right + 2*width)},0)`)
.call(d3.axisLeft(yScale).ticks(null, bostonData.format))
.call(g => g.append("text")
.attr("x", margin.left/2)
.attr("y", margin.top/2)
.attr('font-size', '16px')
.attr('font-weight', 'bold')
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.attr("alignment-baseline","text-before-edge")
.text("New covid cases in Boston per 100,000 people")
)
Insert cell
Insert cell
employment_medata = (
{
emp_high: ({label:'High income'}),
emp_combined: ({label:'Combined'}),
emp_middle: ({label:'Middle income'}),
emp_low: ({label:'Low income'})
}
)
Insert cell
xAxisEmp = g => g
.attr("transform", `translate(0,${
1/3*(-padding_between_charts + 2*height - 2*margin.bottom +margin.top)})`)
.call(
d3.axisBottom(xScale)
.tickFormat(d3.timeFormat("%b %y"))
.tickValues(xScale.domain().filter(d => d.getDate() == 1 && (d.getMonth() % 2)==0))
.tickSizeOuter(0))
Insert cell
yScaleEmp = d3.scaleLinear()
.domain([1.1*d3.min(bostonDataEmployment.map(d=>+d.emp_low)),
1.1*d3.max(bostonDataEmployment.map(d=>+d.emp_high))])
.range([
1/3*(-padding_between_charts + 2*height - 2*margin.bottom + margin.top),
1/3*(padding_between_charts + height - margin.bottom + 2*margin.top)])
Insert cell
yAxisEmp = g => g
.attr("transform", `translate(${1/3*(margin.left - 2*margin.right + 2*width)},0)`)
.call(d3.axisLeft(yScaleEmp).ticks(null, bostonDataEmployment.format))
.call(g => g.append("text")
.attr("x", margin.left/2)
.attr("y", 1/3*(padding_between_charts + height - margin.bottom + 2*margin.top)-margin.top/2)
.attr('font-size', '16px')
.attr('font-weight', 'bold')
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text("Seasonally adjusted change in employment (%)")
)
Insert cell
function employmentLine(field){
return d3.line()
.x(d => xScale(d.date) + xScale.bandwidth() / 2)
.y(d => yScaleEmp(d[field]))
}
Insert cell
Insert cell
function ridershipLine(mbta_line_scale){
return d3.line()
.x(d => xScale(d.date) + xScale.bandwidth() / 2)
.y(d => mbta_line_scale(d.ridership))
}
Insert cell
function yScaleLine(offsets){
return d3.scaleLinear()
.domain([1.05*d3.min(ridership.map(d=>+d.ridership)),
1.05*d3.max(ridership.map(d=>+d.ridership))])
.range(offsets)
}
Insert cell
yScalesMBTA = (
{
blue:yScaleLine([
1/4*(3*height - 3*margin.bottom + margin.top + 2*padding_between_charts),
1/3*(2*height - 2*margin.bottom + margin.top + 2*padding_between_charts)
]),
green:yScaleLine([
1/6*(5*height - 5*margin.bottom + margin.top + 2*padding_between_charts),
1/4*(3*height - 3*margin.bottom + margin.top + 2*padding_between_charts)
]),
orange:yScaleLine([
1/12*(11*height - 11*margin.bottom + margin.top +2*padding_between_charts),
1/6*(5*height - 5*margin.bottom + margin.top + 2*padding_between_charts)
]),
red:yScaleLine([
height-margin.bottom,
1/12*(11*height - 11*margin.bottom + margin.top +2*padding_between_charts)
]),
}
)
Insert cell
function yAxisLine(lineScale){
return g => g
.attr("transform", `translate(${1/3*(margin.left - 2*margin.right + 2*width)},0)`)
.call(d3.axisLeft(lineScale).tickValues([-75,-50,-25,0]))
}
Insert cell
yAxesMBTA = (
{
blue:yAxisLine(yScalesMBTA.blue),
green:yAxisLine(yScalesMBTA.green),
orange:yAxisLine(yScalesMBTA.orange),
red: g => g
.attr("transform", `translate(${1/3*(margin.left - 2*margin.right + 2*width)},0)`)
.call(d3.axisLeft(yScalesMBTA.red).tickValues([-75,-50,-25,0]))
.call(g => g.append("text")
.attr("x", margin.left/2)
.attr("y", 1/3*(2*height - 2*margin.bottom + margin.top + 2*padding_between_charts)-margin.top/2)
.attr('font-size', '18px')
.attr('font-weight', 'bold')
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text("Seasonally adjusted change in MBTA ridership (%)")
)
}
)
Insert cell
function xAxisLine(offset){
return g => g
.attr("transform", `translate(0,${offset})`)
.call(
d3.axisBottom(xScale)
.tickFormat("")
.tickValues(xScale.domain().filter(d => d.getDate() == 1 && (d.getMonth() % 2)==0))
.tickSizeOuter(0))
}
Insert cell
xAxesMBTA = (
{
blue:xAxisLine(1/4*(3*height - 3*margin.bottom + margin.top + 2*padding_between_charts)),
green:xAxisLine(1/6*(5*height - 5*margin.bottom + margin.top + 2*padding_between_charts)),
orange:xAxisLine(1/12*(11*height - 11*margin.bottom + margin.top +2*padding_between_charts)),
red: g => g
.attr("transform", `translate(0,${height-margin.bottom})`)
.call(
d3.axisBottom(xScale)
.tickFormat(d3.timeFormat("%b %y"))
.tickValues(xScale.domain().filter(d => d.getDate() == 1 && (d.getMonth() % 2)==0))
.tickSizeOuter(0))
}
)
Insert cell
Insert cell
function yScaleSparkline(offsets){
return d3.scaleLinear()
.domain([0,200000])
.range(offsets)
}
Insert cell
function yAxisSparkLine(line){
return g => g
.call(d3.axisLeft(sparkline_yscales[line])
.tickValues([0,50000,100000,150000])
.tickFormat(d3.format("~s")))
}
Insert cell
function sparklineMaker(line) {
return d3.line()
.x(d => xScale_sparkline(d.projected_x))
.y(d => sparkline_yscales[line](d.median_income))
.curve(d3.curveStep)
}
Insert cell
function sparklineAreaMaker(line) {
return d3.area()
.x(d => xScale_sparkline(d.projected_x))
.y1(d => sparkline_yscales[line](d.median_income))
.y0(sparkline_yscales[line](0.))
.curve(d3.curveStep)
}
Insert cell
sparkline_yscales=(
{
blue:yScaleSparkline([height-margin.bottom,
1/7*(6*height - 6*margin.bottom + margin.top)]),
orange:yScaleSparkline([1/4*(3*height - 3*margin.bottom + margin.top),
1/28*(17*height - 17*margin.bottom + 11*margin.top)]),
red_a:yScaleSparkline([1/2*(height - margin.bottom + margin.top),
1/14*(5*height - 5*margin.bottom + 9*margin.top)]),
red_b:yScaleSparkline([1/14*(5*height - 5*margin.bottom + 9*margin.top),
1/14*(3*height - 3*margin.bottom + 11*margin.top)]),
green_b:yScaleSparkline([height - margin.bottom,
1/7*(6*height - 6*margin.bottom + margin.top)]),
green_c:yScaleSparkline([1/7*(6*height - 6*margin.bottom + margin.top),
1/7*(5*height - 5*margin.bottom + 2*margin.top)]),
green_d:yScaleSparkline([1/7*(5*height - 5*margin.bottom + 2*margin.top),
1/7*(4*height - 4*margin.bottom + 3*margin.top)]),
green_e:yScaleSparkline([1/7*(4*height - 4*margin.bottom + 3*margin.top),
1/7*(3*height - 3*margin.bottom + 4*margin.top)]),
}
)
Insert cell
xScale_sparkline = d3.scaleLinear()
.domain([0,
(Math.ceil(1.1*(
( d3.max(incomeDataStatic.red_b.map(d=>+d.projected_x)))+d3.max(incomeDataStatic.green_d.map(d=>+d.projected_x))
)*10)/10)])
.range([margin.left, 1/3*(margin.left - 2*margin.right + 2*width)-margin.left*2])
Insert cell
sparkline_scale_annotation = d3.scaleLinear()
.domain([0, 3])
.range([xScale_sparkline(0), xScale_sparkline(3*0.185)])
Insert cell
sparklinesXAxis = g => g
.attr("transform", `translate(${axis_flush},${height-margin.bottom})`)
.call(
d3.axisBottom(sparkline_scale_annotation)
.tickValues([0,1,2,3])
.tickSizeOuter(0))
.call(g => g.append("text")
.attr("x", sparkline_scale_annotation(1.5))
.attr("y", -margin.top/2)
.attr('font-size', '16px')
.attr('font-weight', 'bold')
.attr("fill", "currentColor")
.attr("text-anchor", "middle")
.text("Distance (mi)")
)
Insert cell
axis_flush = xScale_sparkline(d3.max(incomeDataStatic.red_b.map(d=>+d.projected_x)))-sparkline_scale_annotation(3)
Insert cell
green_line_flush=(
Math.ceil(
1.1*(d3.max(incomeDataStatic.red_b.map(d=>+d.projected_x)) +d3.max(incomeDataStatic.green_d.map(d=>+d.projected_x)))*10)/10)-d3.max(incomeDataStatic.green_d.map(d=>+d.projected_x))
Insert cell
sparkline_metadata= (
{
green_b: ({offset:green_line_flush, inflection:-1}),
green_c: ({offset:green_line_flush, inflection:0.625}),
green_d: ({offset:green_line_flush, inflection:0.625}),
green_e: ({offset:green_line_flush, inflection:0.52}),
blue: ({offset:0, inflection:-1}),
orange: ({offset:0, inflection:-1}),
red_b: ({offset:0, inflection:1.65}),
red_a: ({offset:0, inflection:-1}),
}
)
Insert cell
Insert cell
function centroids(line_low,line_high) {
return function(d) {
if (d.projected_x < sparkline_metadata[line_high].inflection)
{
return ridership_yscales[line_low](-50)
}
else {
return ridership_yscales[line_high](-50)
}
}
}
Insert cell
trackLines=(
{
red_a:d3.line()
.x(d => xScale_sparkline(d.projected_x))
.y(d => centroids('red_a','red_a')(d))
.curve(d3.curveMonotoneX),

red_b:d3.line()
.x(d => xScale_sparkline(d.projected_x))
.y(d => centroids('red_a','red_b')(d))
.curve(d3.curveMonotoneX),

green_b:d3.line()
.x(d => xScale_sparkline(d.projected_x))
.y(d => centroids('green_b','green_b')(d))
.curve(d3.curveMonotoneX),

green_c:d3.line()
.x(d => xScale_sparkline(d.projected_x))
.y(d => centroids('green_b','green_c')(d))
.curve(d3.curveMonotoneX),

green_d:d3.line()
.x(d => xScale_sparkline(d.projected_x))
.y(d => centroids('green_b','green_d')(d))
.curve(d3.curveMonotoneX),

green_e:d3.line()
.x(d => xScale_sparkline(d.projected_x))
.y(d => centroids('green_b','green_e')(d))
.curve(d3.curveMonotoneX),

orange:d3.line()
.x(d => xScale_sparkline(d.projected_x))
.y(d => centroids('orange','orange')(d))
.curve(d3.curveMonotoneX),

blue:d3.line()
.x(d => xScale_sparkline(d.projected_x))
.y(d => centroids('blue','blue')(d))
.curve(d3.curveMonotoneX)
}
)
Insert cell
Insert cell
rectsHeightScale = d3.scaleLinear()
.domain([-100,0])
.range([0,3/40*(height - margin.bottom - margin.top)])
Insert cell
function yScaleRidership(offsets){
return d3.scaleLinear()
.domain([-100,0])
.range(offsets)
}
Insert cell
ridership_yscales=(
{
blue:yScaleRidership([1/7*(6*height - 6*margin.bottom + margin.top),
1/4*(3*height - 3*margin.bottom + margin.top)]),
orange:yScaleRidership([1/28*(17*height - 17*margin.bottom + 11*margin.top),
1/2*(height - margin.bottom + margin.top)]),
red_a:yScaleRidership([1/14*(3*height - 3*margin.bottom + 11*margin.top),
1/28*(3*height - 3*margin.bottom + 25*margin.top)]),
red_b:yScaleRidership([1/28*(3*height - 3*margin.bottom + 25*margin.top),
margin.top]),
green_b:yScaleRidership([1/7*(3*height - 3*margin.bottom + 4*margin.top),
1/28*(9*height - 9*margin.bottom + 19*margin.top)]),
green_c:yScaleRidership([1/28*(9*height - 9*margin.bottom + 19*margin.top),
1/14*(3*height - 3*margin.bottom + 11*margin.top)]),
green_d:yScaleRidership([1/14*(3*height - 3*margin.bottom + 11*margin.top),
1/28*(3*height - 3*margin.bottom + 25*margin.top)]),
green_e:yScaleRidership([1/28*(3*height - 3*margin.bottom + 25*margin.top),
margin.top]),
}
)
Insert cell
Insert cell
intro_width = 750
Insert cell
intro_height = 500
Insert cell
intro_margin = ({left:25,right:25,top:25,bottom:25})
Insert cell
xScaleIntro = d3.scaleBand()
.domain(allDates)
.range([intro_margin.left, intro_width - intro_margin.right])
.paddingInner(0.5)
.paddingOuter(0)
Insert cell
yScaleIntro = d3.scaleLinear()
.domain([0, d3.max(bostonData.map(d => d.new_case_rate))])
.range([1/3*(-2*intro_padding_between_charts + intro_height - intro_margin.bottom + 2*intro_margin.top),
intro_margin.top])
Insert cell
intro_padding_between_charts = Math.floor(intro_height/100)*10
Insert cell
xAxisIntro = g => g
.attr("transform", `translate(0,${
1/3*(intro_height - intro_margin.bottom + 2*intro_margin.top - 2*intro_padding_between_charts)
})`)
.call(
d3.axisBottom(xScaleIntro)
.tickFormat(d3.timeFormat("%b %y"))
.tickValues(xScaleIntro.domain().filter(d => d.getDate() == 1 && (d.getMonth() % 2)==0))
.tickSizeOuter(0))
Insert cell
yAxisIntro = g => g
.attr("transform", `translate(${intro_margin.left},0)`)
.call(d3.axisLeft(yScaleIntro).ticks(null, bostonData.format))
.call(g => g.append("text")
.attr("x", intro_margin.left/2)
.attr("y", intro_margin.top/2)
.attr('font-size', '14px')
.attr('font-weight', 'bold')
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.attr("alignment-baseline","text-before-edge")
.text("New covid cases in Boston per 100,000 people")
)
Insert cell
xAxisEmpIntro = g => g
.attr("transform", `translate(0,${
1/3*(-intro_padding_between_charts + 2*intro_height - 2*intro_margin.bottom +intro_margin.top)})`)
.call(
d3.axisBottom(xScaleIntro)
.tickFormat(d3.timeFormat("%b %y"))
.tickValues(xScaleIntro.domain().filter(d => d.getDate() == 1 && (d.getMonth() % 2)==0))
.tickSizeOuter(0))
Insert cell
yScaleEmpIntro = d3.scaleLinear()
.domain([1.1*d3.min(bostonDataEmployment.map(d=>+d.emp_low)),
1.1*d3.max(bostonDataEmployment.map(d=>+d.emp_high))])
.range([
1/3*(-intro_padding_between_charts + 2*intro_height - 2*intro_margin.bottom + intro_margin.top),
1/3*(intro_padding_between_charts + intro_height - intro_margin.bottom + 2*intro_margin.top)])
Insert cell
yAxisEmpIntro = g => g
.attr("transform", `translate(${intro_margin.left},0)`)
.call(d3.axisLeft(yScaleEmpIntro).ticks(null, bostonDataEmployment.format))
.call(g => g.append("text")
.attr("x", intro_margin.left/2)
.attr("y", 1/3*(intro_padding_between_charts + intro_height - intro_margin.bottom + 2*intro_margin.top)-intro_margin.top/2)
.attr('font-size', '14px')
.attr('font-weight', 'bold')
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text("Seasonally adjusted change in employment (%)")
)
Insert cell
function employmentLineIntro(field){
return d3.line()
.x(d => xScaleIntro(d.date) + xScaleIntro.bandwidth() / 2)
.y(d => yScaleEmpIntro(d[field]))
}
Insert cell
function ridershipLineIntro(mbta_line_scale){
return d3.line()
.x(d => xScaleIntro(d.date) + xScaleIntro.bandwidth() / 2)
.y(d => mbta_line_scale(d.ridership))
}
Insert cell
yScalesMBTAIntro = (
{
blue:yScaleLine([
1/4*(3*intro_height - 3*intro_margin.bottom + intro_margin.top + 2*intro_padding_between_charts),
1/3*(2*intro_height - 2*intro_margin.bottom + intro_margin.top + 2*intro_padding_between_charts)
]),
green:yScaleLine([
1/6*(5*intro_height - 5*intro_margin.bottom + intro_margin.top + 2*intro_padding_between_charts),
1/4*(3*intro_height - 3*intro_margin.bottom + intro_margin.top + 2*intro_padding_between_charts)
]),
orange:yScaleLine([
1/12*(11*intro_height - 11*intro_margin.bottom + intro_margin.top +2*intro_padding_between_charts),
1/6*(5*intro_height - 5*intro_margin.bottom + intro_margin.top + 2*intro_padding_between_charts)
]),
red:yScaleLine([
intro_height-intro_margin.bottom,
1/12*(11*intro_height - 11*intro_margin.bottom + intro_margin.top +2*intro_padding_between_charts)
]),
}
)
Insert cell
function yAxisLineIntro(lineScale){
return g => g
.attr("transform", `translate(${intro_margin.left},0)`)
.call(d3.axisLeft(lineScale).tickValues([-50,0]))
}
Insert cell
yAxesMBTAIntro = (
{
blue:yAxisLineIntro(yScalesMBTAIntro.blue),
green:yAxisLineIntro(yScalesMBTAIntro.green),
orange:yAxisLineIntro(yScalesMBTAIntro.orange),
red: g => g
.attr("transform", `translate(${intro_margin.left},0)`)
.call(d3.axisLeft(yScalesMBTAIntro.red).tickValues([-50,0]))
.call(g => g.append("text")
.attr("x", intro_margin.left/2)
.attr("y", 1/3*(2*intro_height - 2*intro_margin.bottom + intro_margin.top + 2*intro_padding_between_charts)-margin.top/2)
.attr('font-size', '14px')
.attr('font-weight', 'bold')
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text("Seasonally adjusted change in MBTA ridership (%)")
)
}
)
Insert cell
function xAxisLineIntro(offset){
return g => g
.attr("transform", `translate(0,${offset})`)
.call(
d3.axisBottom(xScaleIntro)
.tickFormat("")
.tickValues(xScaleIntro.domain().filter(d => d.getDate() == 1 && (d.getMonth() % 2)==0))
.tickSizeOuter(0))
}
Insert cell
xAxesMBTAIntro = (
{
blue:xAxisLineIntro(1/4*(3*intro_height - 3*intro_margin.bottom + intro_margin.top + 2*intro_padding_between_charts)),
green:xAxisLineIntro(1/6*(5*intro_height - 5*intro_margin.bottom + intro_margin.top + 2*intro_padding_between_charts)),
orange:xAxisLineIntro(1/12*(11*intro_height - 11*intro_margin.bottom + intro_margin.top +2*intro_padding_between_charts)),
red: g => g
.attr("transform", `translate(0,${intro_height-intro_margin.bottom})`)
.call(
d3.axisBottom(xScaleIntro)
.tickFormat(d3.timeFormat("%b %y"))
.tickValues(xScaleIntro.domain().filter(d => d.getDate() == 1 && (d.getMonth() % 2)==0))
.tickSizeOuter(0))
}
)
Insert cell
Insert cell
slider = d3.sliderBottom()
.min(d3.min(allDates))
.max(d3.max(allDates))
.step(1000 * 60 * 60 * 24)
.width(250)
.tickFormat(d3.timeFormat("%b %y"))
.tickValues(xScale.domain().filter(d => d.getDate() == 1 && (d.getMonth() % 2)==0))
.default(allDates[65]);
Insert cell
callout = (g, value) => {
if (!value) return g.style("display", "none");

g
.style("display", null)
.style("pointer-events", "none")
.style("font", "14px sans-serif");

const path = g.selectAll("path")
.data([null])
.join("path")
.attr("fill", "white")
.attr("stroke", "black");

const text = g.selectAll("text")
.data([null])
.join("text")
.call(text => text
.selectAll("tspan")
.data((value + "").split(/\n/))
.join("tspan")
.attr("x", 0)
.attr("y", (d, i) => `${i * 1.1}em`)
.style("font-weight", (_, i) => i ? null : "bold")
.text(d => d));

const {x, y, width: w, height: h} = text.node().getBBox();

text.attr("transform", `translate(${-w / 2},${15 - y})`);
path.attr("d", `M${-w / 2 - 10},5H-5l5,-5l5,5H${w / 2 + 10}v${h + 20}h-${w + 20}z`);
}
Insert cell
function pathTween(d1, precision) {
return function() {
var path0 = this,
path1 = path0.cloneNode(),
n0 = path0.getTotalLength(),
n1 = (path1.setAttribute("d", d1), path1).getTotalLength();

// Uniform sampling of distance based on specified precision.
var distances = [0], i = 0, dt = precision / Math.max(n0, n1);
while ((i += dt) < 1) distances.push(i);
distances.push(1);

// Compute point-interpolators at each distance.
var points = distances.map(function(t) {
var p0 = path0.getPointAtLength(t * n0),
p1 = path1.getPointAtLength(t * n1);
return d3.interpolate([p0.x, p0.y], [p1.x, p1.y]);
});

return function(t) {
return t < 1 ? "M" + points.map(function(p) { return p(t); }).join("L") : d1;
};
};
}
Insert cell
highlight_width = 750
Insert cell
highlight_height = 500
Insert cell
svg_width = 400
Insert cell
highlight_margin = ({left:25,right:25,top:25,bottom:25})
Insert cell
xScale_highlight = d3.scaleLinear()
.domain([0,d3.max(incomeDataStatic.red_b.map(d=>+d.projected_x))])
.range([svg_width+highlight_margin.left*2, highlight_width-highlight_margin.right])
Insert cell
highlight_xAxis = g => g
.attr("transform", `translate(0,${1/5*(4*highlight_height - 4*highlight_margin.bottom +
highlight_margin.top)})`)
.call(
d3.axisBottom(xScale_highlight)
.tickValues(d3.range(0,16.05,3).map(d=>d*0.185))
.tickFormat(d=>d3.format(".2")(d/0.185))
.tickSizeOuter(0))
.call(g => g.append("text")
.attr("x", 1/2*(2*highlight_margin.left-highlight_margin.right + highlight_width + svg_width))
.attr("y", highlight_margin.bottom*3/2)
.attr('font-size', '16px')
.attr('font-weight', 'bold')
.attr("fill", "currentColor")
.attr("text-anchor", "middle")
.text("Distance (mi)")
)
Insert cell
yScale_highlight = d3.scaleLinear()
.domain([0,200000])
.range([1/5*(4*highlight_height - 4*highlight_margin.bottom +
highlight_margin.top),1/5*(2*highlight_height - 2*highlight_margin.bottom +
3*highlight_margin.top)])
Insert cell
highlight_yAxis = g => g
.attr("transform", `translate(${svg_width+highlight_margin.left*2},0)`)
.call(d3.axisLeft(yScale_highlight)
.tickValues([0,50000,100000,150000])
.tickFormat(d3.format("~s")))
Insert cell
sparklineMakerHighlight = d3.line()
.x(d => xScale_highlight(d.projected_x))
.y(d => yScale_highlight(d.median_income))
.curve(d3.curveStep)
Insert cell
sparklineAreaMakerHighlight = d3.area()
.x(d => xScale_highlight(d.projected_x))
.y1(d => yScale_highlight(d.median_income))
.y0(d => yScale_highlight(0.))
.curve(d3.curveStep)
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more