Public
Edited
Jun 6, 2024
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
bars = () => {
// Scales
const x = d3.scaleLinear().domain([0, d3.sum(barsData, d => d.days)]);
const color = threshold => palette[thresholds.indexOf(threshold)];
const colorText = threshold => paletteText[thresholds.indexOf(threshold)];

// Scaffold
const wrapper = d3.create("div").attr("class", "bars");
const svg = wrapper.append("svg").style("display", "table").style("margin", "0 auto")
const g = svg.append("g");

// Marks
const rect = g.selectAll("rect")
.data(barsData)
.join("rect")
.attr("fill", d => color(d.threshold))
.attr("stroke", d => colorText(d.threshold));

const label = g.selectAll(".label")
.data(barsData)
.join("text")
.attr("class", "label")
.attr("y", d => -6 - ((d.word_count - 1) * 16))
.html(d => d.html);

label.selectAll("tspan")
.attr("x", 0)
.attr("dy", (_, i) => i * 16);
const value = g.selectAll(".value")
.data(barsData)
.join("text")
.attr("class", "value")
.attr("fill", d => colorText(d.threshold))
.attr("x", 4);

return Object.assign(wrapper.node(), {
resize(width) {
const margin = { left: 1, right: 1, top: 40, bottom: 1 };
const chartwidth = Math.min(width, 400) - margin.left - margin.right;
const chartheight = 32;

// Resize: Scales
x.range([0, chartwidth]);
// Resize: Data
const first = barsData.findIndex(d => x(d.days) > 80);
barsData.forEach((d, i) => {
d.first = i === first;
return d;
});

wrapper.style("width", `${Math.min(640, width)}px`)

svg
.attr("width", chartwidth + margin.left + margin.right)
.attr("height", chartheight + margin.top + margin.bottom);
g
.attr("transform", `translate(${[margin.left, margin.top]})`);
// Resize: Marks
rect
.attr("height", chartheight)
.attr("width", d => x(d.days))
.attr("x", (_, i) => x(d3.sum(barsData.slice(0, i), d0 => d0.days)));

label
.attr("opacity", d => x(d.days) > maxWidth ? 1 : 0)
.attr("transform", (d, i) => {
if (x(d.days) > maxWidth) {
return `translate(${x(d3.sum(barsData.slice(0, i), d0 => d0.days))})`;
}
else {
return "translate(0)";
}
});
value
.attr("opacity", d => x(d.days) > maxWidth ? 1 : 0)
.attr("transform", (d, i) => {
if (x(d.days) > maxWidth) {
return `translate(${x(d3.sum(barsData.slice(0, i), d0 => d0.days))})`;
}
else {
return "translate(0)"
}
})
.attr("y", chartheight / 2 + 6)
.text(d => `${d.days}${d.first ? " days" : ""}`);
}
})
}
Insert cell
chart = () => {
// Scaffold
const svg = d3.create("svg");
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");

const yaxisLabel = g.selectAll(".y-axis-label")
.data(thresholdLabels)
.join("g")
.attr("class", "y-axis-label");

// Marks
const day = g.selectAll(".day")
.data(daily)
.join("circle")
.attr("class", "day")
.attr("fill", d => color(d.heat_index))
.attr("stroke", d => d3.color(color(d.heat_index)).darker(0.8));

const thresholdLabel = g.selectAll(".threshold-label")
.data(d3.zip(thresholdValues.slice(1, 5), ["Caution", "Extreme caution", "Danger", "Extreme danger"]))
.join("text")
.attr("class", "threshold-label")
.attr("dy", "0.32em")
.attr("fill", d => d[0] >= 103 ? color(d[0]) : color(d[0], true))
.attr("opacity", d => d[0] >= yDomain.yMin && d[0] <= yDomain.yMax ? 1 : 0)
.attr("x", 4)
.attr("y", -7)
.text(d => d[1]);
return Object.assign(svg.node(), {
resize(ww){
// Resize: Dimensions
const r = ww <= 400 ? 3 : 4;
const margin = { left: 40, right: r + 1, top: 14, bottom: 26};
const basewidth = Math.min(ww, 640);
const width = basewidth - margin.left - margin.right;
const height = basewidth * 9 / 16 - margin.top - margin.bottom;

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

// 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
.attr("transform", `translate(0, ${height})`)
.call(xaxisGenerator);

yaxisGenerator.tickSize(width + 10)
const ytickOffset = 13;
yaxis
.attr("transform", `translate(${width})`)
.call(yaxisGenerator);
const tick = yaxis.selectAll(".tick");
tick.select("line")
.style("stroke", d => {
return thresholdValues.includes(d) ? d < 103 ? color(d, d <= 103) : color(d) : "#e9e9e9"
})
.attr("x2", -width - margin.left)
tick.select("text")
.style("fill", d => {
return thresholdValues.includes(d) ? d < 103 ? color(d, d <= 103) : color(d) : "#666666"
})
.attr("x", -width - margin.left)
.attr("y", -8);

yaxisLabel
.attr("transform", d => `translate(${[-margin.left, d.y]})`)

thresholdLabel
.attr("transform", d => `translate(0, ${y(d[0])})`);
// Resize: Marks
day
.attr("r", r)
.attr("cx", d => x(d.datetime))
.attr("cy", d => y(d.heat_index));
}
})
}
Insert cell
display.resize(width)
Insert cell
displayBars.resize(width)
Insert cell
Insert cell
css = `
.axis .domain {
display: none;
}
.axis .tick text {
fill: #666666;
font-family: ${franklinLight};
font-size: 14px;
}
.axis .tick line {
shape-rendering: crispEdges;
}
.axis.x-axis .tick line {
stroke: #aaaaaa;
}
.axis.y-axis .tick line {
stroke: #d5d5d5;
}
.axis.y-axis .tick text {
text-anchor: start;
}
.y-axis-label {
font-family: ${franklinLight};
}
.y-axis-label .threshold {
font-size: 16px;
font-weight: bold;
}
.y-axis-label .days {
font-size: 14px;
}
.threshold-label {
font-family: ${franklinLight};
font-size: 14px;
font-weight: bold;
}
.bars text {
font-family: ${franklinLight};
}
.bars text.value {
font-weight: bold;
}
.bars rect {
shape-rendering: crispEdges;
}
`
Insert cell
Insert cell
maxWidth = 50
Insert cell
barsData = thresholds.map(threshold => {
let html;
const words = `${threshold[0].toUpperCase()}${threshold.slice(1)}`.split(" ");
if (words.length === 1) html = words.join(" ");
else {
html = `<tspan>${words.join("</tspan><tspan>")}</tspan>`
}
const days = data.thresholds[threshold];
return {threshold, html, word_count: words.length, days };
});
Insert cell
Insert cell
months = ["Jan.", "Feb.", "March", "April", "May", "June", "July", "Aug.", "Sept.", "Oct.", "Nov.", "Dec."]
Insert cell
xaxisGenerator = d3.axisBottom(x)
.tickFormat(d => months[d.getUTCMonth()])
.tickSize(10)
Insert cell
yaxisGenerator = d3.axisLeft(y)
.tickFormat((d, i, e) => `${d}°F`)
.tickValues(yDomain.tickValues)
Insert cell
Insert cell
palette = [ "#fff", "#e9e9e9", "#d5d5d5", "#cc3d00", "#6f203d" ]
Insert cell
paletteText = [ "#808080", "#666666", "#494949", "#330f00", "#280b16" ]
Insert cell
temps = [80, 90, 103, 125]
Insert cell
color = (heat_index, text = false) => {
// https://www.weather.gov/ama/heatindex
if (heat_index >= 80 && heat_index < 90) {
return text ? paletteText[1] : palette[1];
}
else if (heat_index >= 90 && heat_index < 103) {
return text ? paletteText[2] : palette[2];
}
else if (heat_index >= 103 && heat_index < 125) {
return text ? paletteText[3] : palette[3];
}
else if (heat_index >= 125) {
return text ? paletteText[4] : palette[4];
}
else {
return text ? paletteText[0] : palette[0];
}
}
Insert cell
Insert cell
x = d3.scaleTime().domain(d3.extent(daily, d => d.datetime))
Insert cell
yExtent = d3.extent(daily, d => d.heat_index)
Insert cell
yRange = yExtent[1] - yExtent[0]
Insert cell
yInterval = yRange > 60 ? 20 : 10
Insert cell
yMin = Math.floor(yExtent[0] / yInterval) * yInterval
Insert cell
yMax = Math.ceil(yExtent[1] / yInterval) * yInterval
Insert cell
y = d3.scaleLinear().domain([yDomain.yMin, yDomain.yMax])
Insert cell
yDomain = {
const yExtent = d3.extent(data.daily, d => d.heat_index);
const yRange = yExtent[1] - yExtent[0];
const yInterval = yRange < 20 ? 10 : 20;

let valueIndexLow = thresholdValues.findIndex(v => v >= yExtent[0])
let valueLow = thresholdValues[valueIndexLow - 1]
let valueIndexHigh = thresholdValues.findLastIndex(v => v <= yExtent[1])
let valueHigh = thresholdValues[valueIndexHigh + 1];

const valuesFiltered = thresholdValues.filter(n => n >= valueLow && n <= valueHigh);
const valuesFilteredMax = valuesFiltered[valuesFiltered.length - 1];
let tickValues = [];

if (!valuesFiltered.length) {
tickValues.push(...d3.range(
Math.floor(yExtent[0] / yInterval) * yInterval,
yExtent[1] + yInterval,
yInterval
))
}

else if (valuesFiltered[0] < thresholdValues[1]) {
const R = d3.range(
Math.floor(yExtent[0] / yInterval) * yInterval,
thresholdValues[1],
yInterval
);
tickValues.push(...R);
}

if (valuesFilteredMax > thresholdValues[3]) {
tickValues.push(...valuesFiltered.filter(v => v <= thresholdValues[3]));

if (yExtent[1] < 110) {
tickValues.push(110)
}
else if (yExtent[1] < 120) {
tickValues.push(120)
}
else {
tickValues.push(125)
}
}
else {
tickValues.push(...valuesFiltered)
}

tickValues = removeIfLessThanPrevious(tickValues);

return { yInterval, yMin: tickValues[0], yMax: tickValues[tickValues.length - 1], tickValues };
}
Insert cell
Insert cell
threshold = "danger"
Insert cell
days = {
let days = 0;
T.forEach((t) => {
days += data.thresholds[t];
});
return days;
}
Insert cell
T = thresholds.slice(thresholds.findIndex(d => d === threshold))
Insert cell
thresholdLabels = thresholds
.map((threshold, i, e) => {
const thresholdI = thresholds.indexOf(threshold);
const minTemp = thresholdValues[thresholdI];
const maxTemp = thresholdValues[thresholdI + 1]
return {
threshold,
color: palette[i],
minTemp,
maxTemp
}
})
.filter(d => d.minTemp >= yMin && d.minTemp <= yMax)
.map(d => {
const max = Math.min(d.maxTemp, yMax)
const min = Math.max(d.minTemp, yMin);
d.y = 0.5 * (y(min) + y(max));
d.days = data.thresholds[d.threshold];
return d;
})
Insert cell
thresholdValues = [0, 80, 90, 103, 125, Infinity]
Insert cell
thresholds = Object.keys(data.thresholds)
Insert cell
daily = data.daily.map(d => (d.datetime = new Date(d.date), d))
Insert cell
// data = FileAttachment("ghs855@1.json").json()
data = selected
Insert cell
cities = [
await FileAttachment("ghs250.json").json(),
await FileAttachment("ghs626.json").json(),
await FileAttachment("ghs1910.json").json(),
await FileAttachment("ghs2125.json").json(),
await FileAttachment("ghs6845.json").json(),
await FileAttachment("ghs9872.json").json(),
await FileAttachment("ghs10715.json").json(),
await FileAttachment("ghs11498.json").json(),
await FileAttachment("ghs11800.json").json()
]
.sort((a, b) => d3.ascending(a.label, b.label))
Insert cell
Insert cell
formatPop = pop => {
if (pop > 1e6) return `${+(pop / 1e6).toFixed(1)} million`;
else return d3.format(",")(pop);
}
Insert cell
function removeIfLessThanPrevious(arr) {
if (!Array.isArray(arr) || arr.length === 0) {
return [];
}

const result = [arr[0]]; // Initialize the result array with the first element of the input array

for (let i = 1; i < arr.length; i++) {
if (arr[i] >= arr[i - 1]) {
result.push(arr[i]);
}
}

return result;
}
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