Public
Edited
Sep 26, 2023
Insert cell
Insert cell
Insert cell
gdp = FileAttachment("us-gdp.csv").csv({typed: true}).then(values => d3.map(values, d => ({...d, change: d.change * 100})))
Insert cell
d3.extent(gdp, d => d.change)
Insert cell
import { us_recessions } from "@chiahsun-ws/recessions-calculation"
Insert cell
Insert cell
{
const margin = {top: 30, right: 30, bottom: 70, left: 40};

const gdpChartWidth = 1080;
const gdpChartHeight = 500;
const changeChartHeight = 100;
const innerMargin = 45;

const svgWidth = gdpChartWidth + margin.left + margin.right;
const svgHeight = gdpChartHeight + changeChartHeight + margin.bottom + margin.top;

const svg = d3.create("svg")
.attr("viewBox", [0, 0, svgWidth, svgHeight])
.attr("width", svgWidth)
.attr("height", svgHeight)
.attr("style", "max-width: 100%; height: auto; height: intrinsic; font: 10px sans-serif;")
.style("overflow", "visible");

const gdpChart = svg.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);

const x = d3.scaleUtc(d3.extent(gdp, d => d.DATE), [0, gdpChartWidth]);
const y = d3.scaleLinear([0, d3.max(gdp, d => d.GDP)], [gdpChartHeight, 0]);

gdpChart.append("g").attr("transform", `translate(0, ${gdpChartHeight})`).call(d3.axisBottom(x).ticks(gdpChartWidth / 80).tickSizeOuter(0));
gdpChart.append("g")
.call(d3.axisLeft(y)
.ticks(gdpChartHeight / 40))
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone()
.attr("x2", width)
.attr("stroke-opacity", 0.1))
.call(g => g.append("text")
.attr("x", -margin.left)
.attr("y", 10 - margin.top)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text("↑ GDP (Billions of Chained 2012 Dollars)"));

const line = d3.line()
.x(d => x(d.DATE))
.y(d => y(d.GDP));

gdpChart.append("path")
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("d", line(gdp));

// Recessions
gdpChart.append("g")
.selectAll("rect")
.data(us_recessions)
.join("rect")
.attr('x', d => x(d.FROM))
.attr('y', 0)
.attr('width', d => x(d.TO) - x(d.FROM))
.attr('height', gdpChartHeight)
.attr('stroke', 'grey')
.attr('fill', 'grey')
.attr('opacity', 0.5);

const changeChartOffset = margin.top + gdpChartHeight + innerMargin;
const changeChart = svg.append("g")
.attr("transform", `translate(${margin.left}, ${changeChartOffset})`);
const y2 = d3.scaleLinear(d3.extent(gdp, d => d.change), [changeChartHeight, 0]);
// https://observablehq.com/@d3/d3-scaleband
const x2 = d3.scaleBand()
.domain(d3.map(gdp, d => d.DATE))
.range([0, gdpChartWidth])
.paddingInner(0.2);
changeChart.append("g")
.call(d3.axisLeft(y2)
.ticks(changeChartHeight / 40))
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone()
.attr("x2", width)
.attr("stroke-opacity", 0.1))
.call(g => g.append("text")
.attr("x", -margin.left)
.attr("y", 25 - margin.top)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text("↑ Change (%)"));
const chartChartDisplay = changeChart.append("g");
// We actually redraw all the change values
function appendChange(pos) {
chartChartDisplay.selectAll("rect")
.data(gdp)
.join("rect")
.attr("fill", (d) => d.change >= 0 ? "steelblue" : "red")
.attr("x", (d) => x(d.DATE))
.attr("y", d => d.change >= 0 ? y2(d.change) : y2(0))
.attr("height", (d) => d.change >= 0 ? (y2(0) - y2(d.change)) : y2(d.change) - y2(0))
.attr("width", x2.bandwidth())
.attr("opacity", (d, i) => (pos === i) ? 1 : 0.5);
}
appendChange(null);

const tooltip = gdpChart.append("g");
const focus = gdpChart.append('g')
.append('circle')
.style("fill", "none")
.attr("stroke", "black")
.attr('r', 2);

svg.on("pointerenter pointermove", pointermoved);
// .on("pointerleave", pointerleft);

const formatTime = d3.timeFormat("%Y-%m-%d");
const formatDecimal = d3.format(",.3f");

function pointermoved(event) {
const bisect = d3.bisector(d => d.DATE).center;
const mx = d3.pointer(event)[0];
const i = bisect(gdp, x.invert(mx - margin.left));
appendChange(i);
const date = gdp[i].DATE;
const value = gdp[i].GDP;
const change = gdp[i].change;

const focusX = x(date);
const focusY = y(value);

tooltip.attr("transform", `translate(${focusX}, ${focusY})`);
focus.attr("transform", `translate(${focusX}, ${focusY})`);
const text = tooltip.selectAll("text")
.data([,])
.join("text")
.call(text => text
.selectAll("tspan")
.data([formatTime(date), value, formatDecimal(change)])
.join("tspan")
.attr("x", 0)
.attr("y", (_, i) => `${i * 1.1 - 4}em`)
.attr("font-size", "1.3em")
.attr("font-weight", (_, i) => i ? null : "bold")
.style("text-anchor", "middle")
.text(d => d));
}

function pointerleft() {
tooltip.style("display", "none");
focus.style("display", "none");
}


return svg.node();
}
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