Published
Edited
Mar 13, 2020
Importers
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
END_DATE = "2020-03-10"
Insert cell
Insert cell
committees2016 = FileAttachment("committees2016.json").json()
Insert cell
shortNames = ({
31640: "Foxx",
35549: "Conway",
21102: "Fioretti",
35595: "More",
20758: "Alvarez",
31632: "More"
})
Insert cell
nameScale = d3.scaleOrdinal(
["Foxx", "Conway", "Fioretti", "More", "Alvarez"],
d3.schemeDark2
)
Insert cell
md`## Production notes

Screenshots taken at 1024

`
Insert cell
Insert cell
html`
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,600,700,800&display=swap" rel="stylesheet">
<style>
.graphic {
font-family: Open Sans, sans-serif;
font-weight: 400;
}
.graphic .annotation-note-label {
font-size: 12px;
fill: #333;
}
.graphic .annotation-note-title {
font-size: 10px;
font-weight: 800;
fill: #333;
}
</style>`
Insert cell
// graphic = {
// return html`
// <link href="https://fonts.googleapis.com/css?family=Open+Sans:400,600,700,800&display=swap" rel="stylesheet">
// <style>
// #chart, .graphic {
// font-family: Open Sans, sans-serif;
// font-weight: 400;
// }
// .graphic {
// padding: 1rem 0;
// margin-bottom: 2rem;
// border-bottom: 1px solid #ccc;
// }
// .graphic p.chatter {
// max-width: 700px;
// font-size: .75rem;
// margin: 1.4rem 0 1rem;
// }
// .graphic p.source {
// font-weight: 400;
// font-size: 0.6rem;
// margin-top: .6rem;
// margin-bottom: 0;
// color: #999;
// }
// #chart .annotation-note-label {
// font-size: 12px;
// fill: #333;
// }
// #chart .annotation-note-title {
// font-size: 10px;
// font-weight: 800;
// fill: #333;
// }
// </style>
// <div class="graphic">
// <h1>The 2020 Cook County state’s attorney’s race is shaping up to be the most expensive ever. </h1>
// <p class="chatter">Lead challenger Bill Conway’s campaign committee has raised triple that of incumbent Kim Foxx thanks to millions in support from his father. But Foxx has benefited from external spending by super PACs (not pictured).</p>
// ${chart2020}
// <h2>2016 state's attorney primary fundraising</h2>
// ${chart2016}
// <p class="source">Source: Illinois State Board Of Elections. 2020 figures go back to 2019 Q2 quarterly filing and through March 10, 2020; 2016 figures go back to the 2015 Q2 quarterly filing.</p>
// <p class="source">David Eads, Josh McGhee, Asraa Mustufa</p>
// </div>
// `;
// }
Insert cell
{
applyAnnotations(labels, chart2020);
}
Insert cell
labelAnnotations = [
{
id: "note0",
note: {
label:
"Incumbent Kim Foxx enters the race with about $350,000 in available funds."
},
data: {
date: new Date("2019-08-01"),
y: 0
},
dx: 0,
dy: -150
},
{
id: "note1",
note: {
label:
"Bill Conway's campaign committee created. A few days later, his father donates $500,000.",
title: "Aug. 8, 2019"
},
data: {
date: new Date("2019-08-08"),
y: 0
},
dx: 0,
dy: -25
},
{
id: "note2",
note: {
label:
"Starting in 2020, Conway's father regularly donates $2.5-$3 million at regular intervals.",
title: "Jan. 1, 2020"
},
data: {
date: new Date("2020-01-01"),
y: 5.5e6
},
dx: -130,
dy: -1
},
{
id: "note3",
type: d3.annotationCalloutCircle,
note: {
label: "Foxx's 2020 fundraising resembles her 2016 run."
},
data: {
date: new Date("2020-02-25"),
y: 2.9e6
},
dx: -chartWidth * 0.124,
dy: -100
}
]
Insert cell
chartHeight = width > breakpoint ? chartWidth * 0.35 : chartWidth * 0.5
Insert cell
oldChartHeight = width > breakpoint ? chartWidth * 0.11 : chartWidth * .13
Insert cell
chartWidth = width
Insert cell
Insert cell
chart2020 = {
const svg = render_chart(
committees2020,
width > breakpoint ? '2019-07-30' : '2019-10-31',
'2020-03-10',
chartHeight
);
svg
.attr("class", "graphic")
.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 12)
.attr("text-anchor", "start")
.selectAll("text")
.data(committees2020)
.join("text")
.text(d => shortNames[d.id])
.attr("dy", d => {
switch (d.id) {
case 21102: // Fioretti
return 5;
default:
return -5;
}
})
.attr("x", d => chartWidth - margin.right + 3)
.attr("y", d =>
labelY(d.receiptsByDay[d.receiptsByDay.length - 1].cumulative_amount)
);

svg
.append("g")
.attr("font-size", 14)
.attr("text-anchor", "end")
.attr("font-weight", "800")
.selectAll("text")
.data(
committees2020.filter(
d =>
d.receiptsByDay[d.receiptsByDay.length - 1].cumulative_amount > 2.5e5
)
)
.join("text")
.text(d =>
d3.format("$.2s")(
d.receiptsByDay[d.receiptsByDay.length - 1].cumulative_amount
)
)
.attr("dy", -5)
.attr("x", d => chartWidth - margin.right)
.attr("y", d =>
labelY(d.receiptsByDay[d.receiptsByDay.length - 1].cumulative_amount)
);

return svg.node();
}
Insert cell
chart2016 = {
const svg = render_chart(
committees2016,
width > breakpoint ? '2015-07-30' : '2015-10-31',
'2016-03-10',
oldChartHeight
);

svg
.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 11)
.attr("text-anchor", "start")
.selectAll("text")
.data(committees2016)
.join("text")
.text(d => shortNames[d.id])
.attr("visibility", (d, i) =>
width > breakpoint || i === 1 ? "visible" : "hidden"
)
.attr("dy", d => 3)
.attr("x", d => chartWidth - margin.right + 3)
.attr("y", d =>
oldY(d.receiptsByDay[d.receiptsByDay.length - 1].cumulative_amount)
);

return svg.node();
}
Insert cell
Insert cell
Insert cell
function render_chart(data, startDate, endDate, height) {
const x = d3
.scaleTime()
.domain([new Date(startDate), new Date(endDate)])
.range([margin.left, chartWidth - margin.right]);

const y = d3
.scaleLinear()
.domain([
0,
d3.max(
data.map(
d => d.receiptsByDay[d.receiptsByDay.length - 1].cumulative_amount
)
)
])
.nice()
.range([height - margin.bottom, margin.top]);

const line = d3
.line()
.curve(d3.curveStepAfter)
.x((d, i) => x(new Date(d.day)))
.y(d => y(d.cumulative_amount));

const xAxis = g =>
g.attr("transform", `translate(0,${height - margin.bottom})`).call(
d3
.axisBottom(x)
.ticks(width / 120)
.tickSizeOuter(0)
.tickFormat(d => d3.timeFormat("%b '%y")(d))
);

const yAxis = g =>
g
.attr("transform", `translate(${margin.left},0)`)
.call(
d3
.axisLeft(y)
.ticks(height / 50)
.tickFormat(d => d3.format("$~s")(d))
)
.call(g => g.select(".domain").remove());

const svg = d3
.create("svg")
.attr("viewBox", [0, 0, chartWidth, height])
.attr("id", "chart")
.style("overflow", "hidden");

const path = svg
.append("g")
.attr("fill", "none")
.attr("stroke-width", 2)
.selectAll("path")
.data(data)
.join("path")
.style("mix-blend-mode", "multiply")
.attr("stroke", (d, i) => {
return nameScale(shortNames[d.id]);
})
.attr("d", d => line(d.receiptsByDay));

svg.append("g").call(xAxis);
svg.append("g").call(yAxis);

return svg;
}
Insert cell
margin = ({ left: 30, right: 50, top: 10, bottom: 20 })
Insert cell
style = html``
Insert cell
d3.sum(
committees2020.map(
d => d.receiptsByDay[d.receiptsByDay.length - 1].cumulative_amount
)
)
Insert cell
breakpoint = 700
Insert cell
labelX = d3
.scaleTime()
.domain([new Date('2019-07-30'), new Date('2020-03-10')])
.range([margin.left, chartWidth - margin.right])
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
conwayDonors = donorTable(committees2020[0])
Insert cell
foxxDonors = donorTable(committees2020[2])
Insert cell
moreDonors = donorTable(committees2020[3])
Insert cell
function donorTable(d) {
const max = d3.sum(d.topDonors, d => d.amount);
const bigFormatter = d3.format(".1f");
const smallFormatter = d3.format(".3f");

return html`<h2>${d.name}</h2>${HyperGrid(d.topDonors, {
height: 315,
columns: [
{
name: "Donor",
formatter: d => d.name,
width: "1fr"
},
{
name: "Donated since July 1, 2019",
formatter: d => d3.format("$.3~s")(d.amount),
width: ".7fr"
},
{
name: "Percent of total",
formatter: d => {
const pct = (100 * d.amount) / max;
const numFormatter = pct > .1 ? bigFormatter : smallFormatter;
return html`<div style="padding-right: 40px; position: relative;"><div style="color:#fff; height: 19px; width: ${pct}%; background-color: steelblue;"></div>
<div style="position: absolute; top: 0; right: 10px; /
max}%;">${numFormatter(pct)}%</div>
</div>`;
},
width: ".7fr"
}
]
})}`;
}
Insert cell
// md`## Conway donations
// ${HyperGrid(committees2020[0].receipts, { height: 315 })}
// `
Insert cell
Insert cell
d3 = require("d3@5", "d3-array", "d3-svg-annotation")
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more