Public
Edited
Sep 27, 2023
Importers
1 star
Insert cell
Insert cell
Insert cell
data = FileAttachment("bls-metro-unemployment.csv").csv({typed: true})
Insert cell
nameColumn = 'division'
Insert cell
dateColumn = "date"
Insert cell
valueColumn = "unemployment"
Insert cell
yAxisLabel = "Unemployment (%)"
Insert cell
Insert cell
width = 928
Insert cell
height = 600
Insert cell
import { us_recessions } from "@chiahsun-ws/recessions-calculation"
Insert cell
highlightedPeriods = us_recessions.map(d => [d.FROM, d.TO])
Insert cell
import { formatDate, d3_unique } from "@chiahsun-ws/d3-functions"
Insert cell
import { Swatches } from "@d3/color-legend"
Insert cell
import { interpolateCategoricalOptions } from "@chiahsun-ws/d3-functions"
Insert cell
viewof interpolationSelected = Inputs.select(interpolateCategoricalOptions, {label: "Select interpolation", value: "schemeSet2"})
Insert cell
chart.legend
Insert cell
chart = {
const margin = {top: 70, right: 40, bottom: 40, left: 40};

const svg_width = width + margin.left + margin.right;
const svg_height = height + margin.top + margin.bottom;

const svg = d3.create("svg").attr("width", svg_width).attr("height", svg_height);

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

const x = d3.scaleUtc()
.domain(d3.extent(data, d => d[dateColumn]))
.range([0, width]);

chart.append("g")
.attr("transform", `translate(0, ${height})`)
.call(d3.axisBottom(x));

const y = d3.scaleLinear()
.domain(d3.extent(data, d => d[valueColumn])).nice()
.range([height, 0]);

chart.append("g")
.call(d3.axisLeft(y))
.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", -30)
.attr("y", -25)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(`↑ ${yAxisLabel}`));

const points = data.map(d => [d[dateColumn], d[valueColumn], d[nameColumn]]);
const groups = d3.rollup(points, v => Object.assign(v, {z: v[0][2]}), d => d[2]);
const line = d3.line()
.x(d => x(d[0]))
.y(d => y(d[1]));

const color = d3.scaleOrdinal()
.domain(d3_unique(points, d => d[2]))
.range(d3[interpolationSelected]);

const legend = Swatches(color);

const path = chart.append("g")
.attr("fill", "none")
.attr("stroke-width", 1.5)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.selectAll("path")
.data(groups.values())
.join("path")
.attr("stroke", d => color(d.z))
// .attr("stroke", d => color.domain().includes(d[2]) ? color(d[2]) : "steelblue")
.attr("d", line);

const dot = chart.append("g")
.attr("display", "none");

dot.append("circle").attr("r", 2.5);

dot.append("text").attr("text-anchor", "middle");

chart.on("pointerenter", pointerenter)
.on("pointermove", pointermove)
.on("pointerleave", pointerleave);

function pointerenter() {
path.style("mix-blend-mode", null).style("stroke", "#ddd");
dot.attr("display", null);
}

function pointermove(event) {
const [xm, ym] = d3.pointer(event);
const i = d3.leastIndex(points, ([d, v]) => Math.hypot(x(d) - xm, y(v) - ym));
const [d, v, k] = points[i];
path.style("stroke", ({z}) => z === k ? null : "#ddd").filter(({z}) => z === k).raise();
dot.attr("transform", `translate(${x(d)}, ${y(v)})`)
.select("text")
.call(text => text
.selectAll("tspan")
.data([k, formatDate(d), v])
.join("tspan")
.attr("x", 0)
.attr("y", (_, i) => `${i * 1.1 - 4}em`)
.attr("font-size", "1em")
.attr("font-weight", (_, i) => i ? null : "bold")
.style("text-anchor", "middle")
.text(d => d));
}

function pointerleave() {
path.style("mix-blend-mode", "multiply").style("stroke", null);
dot.attr("display", "none");
}

// Add highlight region
chart.append("g")
.selectAll("rect")
.data(highlightedPeriods)
.join("rect")
.attr('x', d => x(d[0]))
.attr('y', 0)
.attr('width', d => x(d[1]) - x(d[0]))
.attr('height', height)
.attr('stroke', 'grey')
.attr('fill', 'grey')
.attr('opacity', 0.2);

return Object.assign(svg.node(), {legend: legend});
}
Insert cell
points = data.map(d => [d[dateColumn], d[valueColumn], d[nameColumn]])
Insert cell
divisions = d3_unique(points, d => d[2])
Insert cell
sampleColor = d3.scaleOrdinal()
.domain(d3_unique(points, d => d[2]))
.range(d3[interpolationSelected])
Insert cell
sampleColor.domain() // ("Bethesda-Rockville-Frederick, MD Met Div")
Insert cell
sampleColor.range()
Insert cell
d3.map(divisions, d => sampleColor(d))
Insert cell
sampleColor("Gary, IN Met Div")
Insert cell
groups = d3.rollup(points, v => Object.assign(v, {z: v[0][2]}), d => d[2])
Insert cell
groups.get("Bethesda-Rockville-Frederick, MD Met Div")[0]
Insert cell
groups.get("Bethesda-Rockville-Frederick, MD Met Div")['z']
Insert cell
Array.from(groups.values())[0].z
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