Public
Edited
Nov 5, 2021
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart2021 = drawLineChart(ledger, {
x: d => d.billing_cycle_date,
y: d => d.balance,
yLabel: "↑ Water balance ($)",
width,
height: 500,
color: wbezColors.get('royalpurple')
})
Insert cell
Insert cell
viewof maps = {
const outerContainer = html`<div>${style()}<div>`
const mapContainer = html`<div style="display:flex; flex-direction:row"></div>`

const hed = html`<h4 class='hed'>Sixty percent of the city’s water debt is concentrated in majority-Black ZIP codes</h4>`
const subhed = html`<p class='subhed'>The ZIP codes with the highest debt burden are majority-Black or Latino. Meanwhile, most of the areas with the least debt are predominantly white.</p>`
const debtMapSVG = drawMap(data, chicago, 'n_debt_per_property', false)
const debtMapContainer = html`<div style="flex-basis:50%"><h5 class='map-title'>Number of debts per property</h5><br>${debtMapSVG}</div>`
const racialMapSVG = drawMap(data, chicago, 'racial_majority', true)
const racialMapContainer = html`<div style="flex-basis:50%"><h5 class='map-title'>Majority-Black ZIP codes</h5></br>${racialMapSVG}</div>`
const sourceLang = `Public records request to the Chicago Department of Finance, and U.S. Census Bureau, American Community Survey 2015-2019 five-year data release.`
const notesLang = `A ZIP code is defined as majority-Black when the American Community Survey estimates more than half of the population identifies as Black.`
const creditLang = `Charmaine Runes/WBEZ.`
const sourceText = html`<div class='source-notes'><span style='font-weight:bold'>Source: </span>${sourceLang}</div>`
const notesText = html`<div class='source-notes'><span style='font-weight:bold'>Notes: </span>${notesLang}</div>`
const creditText = html`<div class='source-notes'><span style='font-weight:bold'>Visualization: </span>${creditLang}</div>`

mapContainer.appendChild(debtMapContainer)
mapContainer.appendChild(racialMapContainer)

outerContainer.appendChild(hed);
outerContainer.appendChild(subhed);
outerContainer.appendChild(mapContainer)
outerContainer.appendChild(sourceText);
outerContainer.appendChild(notesText);
outerContainer.appendChild(creditText);
return outerContainer;
}
Insert cell
debtMap = drawMap(data, chicago, 'n_debt_per_property', false);
Insert cell
racialMap = drawMap(data, chicago, 'racial_majority', true)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function drawStackedBarChart(data) {
let svg = d3.create("svg")
.attr("viewBox", [0, 0, width, heightBarChart])
.style("overflow", "visible");
let series = d3.stack()
.keys(data.columns.slice(1))
.offset(d3.stackOffsetExpand)
(data)
.map(d => (d.forEach(v => {
v.key = d.key
}), d));
let margin = ({top: 10, right: 10, bottom: 20, left: 60});
let x = d3.scaleLinear()
.range([margin.left, width - margin.right]);
let y = d3.scaleBand()
.domain(data.map(d => d.Period))
.range([margin.top, heightBarChart - margin.bottom])
.padding(0.08);
let xAxis = g => g
.attr("transform", `translate(0,${margin.top})`)
.call(d3.axisTop(x).tickSize(0).ticks(0))
.call(g => g.selectAll(".domain").remove());
let yAxis = g => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y).tickSize(0).ticks(0))
.call(g => g.selectAll(".domain").remove());

svg.append("g")
.selectAll("g")
.data(series)
.enter().append("g")
.attr("fill", d => color(d.key))
.selectAll("rect")
.data(d => d)
.join("rect")
.attr("x", d => x(d[0]))
.attr("y", (d, i) => y(d.data.Period))
.attr("width", d => x(d[1]) - x(d[0]))
.attr("height", y.bandwidth())

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

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

return svg.node();
}
Insert cell
Insert cell
Insert cell
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/line-chart
function drawLineChart(data, {
x = ([x]) => x, // given d in data, returns the (temporal) x-value
y = ([, y]) => y, // given d in data, returns the (quantitative) y-value
defined, // for gaps in data
curve = d3.curveLinear, // method of interpolation between points
marginTop = 20, // top margin, in pixels
marginRight = 30, // right margin, in pixels
marginBottom = 30, // bottom margin, in pixels
marginLeft = 40, // left margin, in pixels
width = 640, // outer width, in pixels
height = 400, // outer height, in pixels
xType = d3.scaleUtc, // the x-scale type
xDomain, // [xmin, xmax]
xRange = [marginLeft, width - marginRight], // [left, right]
yType = d3.scaleLinear, // the y-scale type
yDomain, // [ymin, ymax]
yRange = [height - marginBottom, marginTop], // [bottom, top]
yFormat, // a format specifier string for the y-axis
yLabel, // a label for the y-axis
color = "currentColor", // stroke color of line
strokeLinecap = "round", // stroke line cap of the line
strokeLinejoin = "round", // stroke line join of the line
strokeWidth = 1.5, // stroke width of line, in pixels
strokeOpacity = 1, // stroke opacity of line
} = {}) {
// Compute values.
const X = d3.map(data, x);
const Y = d3.map(data, y);
const I = d3.range(X.length);
if (defined === undefined) defined = (d, i) => !isNaN(X[i]) && !isNaN(Y[i]);
const D = d3.map(data, defined);

// Compute default domains.
if (xDomain === undefined) xDomain = d3.extent(X);
if (yDomain === undefined) yDomain = [0, d3.max(Y)];

// Construct scales and axes.
const xScale = xType(xDomain, xRange);
const yScale = yType(yDomain, yRange);
const xAxis = d3.axisBottom(xScale).ticks(width / 80).tickSizeOuter(0);
const yAxis = d3.axisLeft(yScale).tickFormat(yFormat).tickSize(-width);

// Construct a line generator.
const line = d3.line()
.defined(i => D[i])
.curve(curve)
.x(i => xScale(X[i]))
.y(i => yScale(Y[i]));

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");

svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.attr("class", "x-axis")
.call(xAxis);

svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.attr("class", "y-axis")
.call(yAxis)
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick:not(:first-of-type) line")
.attr("stroke-opacity", 0.2)
.attr("stroke-dasharray", "2,2"))
.call(g => g.append("text")
.attr("x", -marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(yLabel));

svg.append("path")
.attr("fill", "none")
.attr("stroke", color)
.attr("stroke-width", strokeWidth)
.attr("stroke-linecap", strokeLinecap)
.attr("stroke-linejoin", strokeLinejoin)
.attr("stroke-opacity", strokeOpacity)
.attr("d", line(I));

return svg.node();
}
Insert cell
function drawMap(data, geography, indicator, highlight) {
let mapData = d3.filter(data, d => d.indicator == indicator)
let topo = getTopoJSON(geography)
let zips = getZipsToFilter(topo)
let reference = buildReference(zips, mapData)
let format = context_vars.get(indicator).tickFormat
let svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("class", "topo")

if (highlight == true) {
// Highlight
svg.append("g")
.selectAll("path")
.attr("id", "map")
.data(topo.features)
.join("path")
.attr("d", createGeoPath(geography))
.attr("class", d => "zip-" + d.properties.zip)
.attr("fill", d => {
if (reference[d.properties.zip] == 'Black') {
return wbezColors.get('peacock');
} else {
return wbezColors.get('lightgray');
}
})
.style("stroke", wbezColors.get('white'))
.style("stroke-width", .5)
.append("title")
.text(d => {
if (reference[d.properties.zip] && format != '') {
return `${d.properties.zip}: ${d3.format(format)(reference[d.properties.zip])}`
} else {
return `${d.properties.zip}: N/A`
}})
} else {
let domainValues = Object.values(reference)
let color = buildColorRamp(domainValues)
// Choropleth
svg.append("g")
.selectAll("path")
.attr("id", "map")
.data(topo.features)
.join("path")
.attr("d", createGeoPath(geography))
.attr("class", d => "zip-" + d.properties.zip)
.attr("fill", d => color(reference[parseFloat(d.properties.zip)]))
.style("stroke", wbezColors.get('white'))
.style("stroke-width", .5)
.append("title")
.text(d => {
if (reference[d.properties.zip]) {
return `${d.properties.zip}: ${d3.format(format)(reference[d.properties.zip])}`
} else {
return `${d.properties.zip}: N/A`
}})

// Legend
svg.append("g")
.attr("transform", "translate(10, 700)")
.attr("class", "legend")
.append(() => legend({
color,
title: context_vars.get(indicator).name,
tickFormat: format,
width: 500,
height: 50}));
}
return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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