Public
Edited
Jun 28, 2024
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart = () => {
// Scaffold
const svg = d3.create("svg").attr("class", "chart");
svg.append("style").html(css);
const g = svg.append("g");

// Axes
const xaxis = g.append("g")
.attr("class", "axis x-axis");

const yaxis = g.append("g")
.attr("class", "axis y-axis");

// Marks
const dot = g.append("g")
.attr("class", "dots")
.selectAll(".dot")
.data(data)
.join("circle")
.attr("class", "dot")
.attr("dx", 5)
.attr("fill", d => fill(d.season))
.attr("stroke", d => stroke(d.season));

const label = g.append("g")
.attr("class", "labels")
.selectAll(".label")
.data(data.filter(d => d.days === 7))
.join("text")
.attr("class", "label");
return Object.assign(svg.node(), {

update(ww, place, duration) {
// Resize: Dimensions
const r = ww <= 480 ? 6 : 7;
const margin = {
left: 45,
right: ww <= 480 ? 102 : ww <= 768 ? 114 : r + 1,
top: 32,
bottom: 8
};
const size = Math.min(420, ww);
const width = size - margin.left - margin.right;
const height = Math.min(300, size * 0.75) - margin.top - margin.bottom;

// Resize: Scales
x.range([0, width]);
y.range([height, 0]);

// Resize: Data
const labelData = data
.filter(d => d.days === 7)
.map(d => (d.y = y(d.mean_error), d));
const dodged = dodge(labelData.map(d => d.y), ww <= 480 ? 16 : 20);
labelData.forEach((d, i) => (d.y = dodged[i], d));
// Resize: Scaffold
svg
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);

g
.attr("transform", `translate(${[margin.left, margin.top]})`);

// Resize: Axes
xaxis
.call(g => xaxisGenerator(g, ww, x, height));

yaxis
.call(g => yaxisGenerator(g, y, width, height, margin));
// Resize: Marks
dot
.attr("cx", d => x(d.days))
.attr("cy", d => y(d.mean_error))
.attr("r", r);

label
.data(labelData)
.attr("dx", r + 5)
.attr("dy", r - 2)
.attr("fill", d => stroke(d.season))
.attr("x", width)
.attr("y", d => d.y)
.text(d => seasonText(d.season));
}
})
}
Insert cell
display.update(width, place)
Insert cell
Insert cell
css = `
.chart {
overflow: visible;
}

.axis .domain {
display: none;
}
.axis .tick text {
fill: #666666;
font-family: ${franklinLight};
font-size: 14px;
}
.axis .tick line {
stroke: #d5d5d5;
}
.axis.x-axis .tick line {
stroke-dasharray: 2,3;
}
.axis.y-axis .tick text {
paint-order: stroke fill;
stroke: white;
stroke-linejoin: round;
stroke-width: 6px;
text-anchor: start;
}

.label {
font-family: ${franklinLight};
font-size: ${width <= 480 ? "14" : "16"}px;
font-weight: bold;
}

.robotext {
font-family: ${franklinLight};
}
.robotext-pill {
font-weight: bold;
white-space: nowrap;
}
`
Insert cell
Insert cell
xaxisGenerator = (g, ww, x, height) => {
const generator = d3.axisTop(x)
.tickFormat((d, i) => `${d}${i === 0 ? ww <= 350 ? " day" : " day out" : ""}`)
.tickSize(height + 20);

const axis = g
.attr("transform", `translate(0, ${height})`)
.call(generator);

const tick = axis.selectAll(".tick")
tick.select("text")
.style("opacity", d => ww <= 500 ? d % 2 === 1 ? 1 : 0 : 1)

return g;
}
Insert cell
yaxisGenerator = (g, y, width, height, margin) => {
const generator = d3.axisLeft(y)
.tickFormat(d => `${d.toFixed(yInterval === 1 ? 0 : 1)}°F`)
.tickPadding(0)
.tickSize(width + margin.left)
.tickValues(d3.range(yDomain[0], yDomain[1] + yInterval, yInterval));

const axis = g
.attr("transform", `translate(${width})`)
.call(generator);

return g;
}
Insert cell
Insert cell
x = d3.scalePoint().domain(d3.range(1, 8).slice())
Insert cell
extent = d3.extent(data, d => d.mean_error);
Insert cell
range = extent[1] - extent[0];
Insert cell
yInterval = range < 1 ? 0.5 : 1;
Insert cell
yDomain = [floorTo(extent[0], yInterval), ceilTo(extent[1], yInterval)];
Insert cell
y = d3.scaleLinear().domain(yDomain);
Insert cell
fill = season => season === "year" ? "#fff" : season === "warm" ? "#ffc200" : "#0082e6"
Insert cell
stroke = season => season === "year" ? "#2a2a2a" : season === "warm" ? "#cc9c00" : "#005799"
Insert cell
seasonText = season => season === "year" ? "Full year" : season === "warm" ? "Warm months" : "Cool months"
Insert cell
Insert cell
places = (await FileAttachment("cities.json").json()).sort((a, b) => d3.ascending(a.city, b.city) || d3.ascending(a.state_name, b.state_name))
Insert cell
data = place.data.sort((a, b) => d3.ascending(a.season, b.season))
Insert cell
Insert cell
// Takes an array of numbers representing the y-coordinate
// See https://observablehq.com/@d3/slope-chart/3
function dodge(positions, separation = 10, maxiter = 10, maxerror = 1e-1) {
let n = positions.length;
if (!positions.every(isFinite)) throw new Error("invalid position");
if (!(n > 1)) return positions;
let index = d3.range(positions.length);
for (let iter = 0; iter < maxiter; ++iter) {
index.sort((i, j) => d3.ascending(positions[i], positions[j]));
let error = 0;
for (let i = 1; i < n; ++i) {
let delta = positions[index[i]] - positions[index[i - 1]];
if (delta < separation) {
delta = (separation - delta) / 2;
error = Math.max(error, delta);
positions[index[i - 1]] -= delta;
positions[index[i]] += delta;
}
}
if (error < maxerror) break;
}
return positions;
}
Insert cell
function ceilTo(number, interval) {
return Math.ceil(number / interval) * interval;
}
Insert cell
function floorTo(number, interval) {
return Math.floor(number / interval) * interval;
}
Insert cell
Insert cell
import { franklinLight } from "@climatelab/fonts@46"
Insert cell
import { toc } from "@climatelab/toc@45"
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