Public
Edited
May 21, 2024
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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 xaxisLabel = g.append("text")
.attr("class", "axis-label x-axis-label");
const yaxisLabel = g.append("text")
.attr("class", "axis-label y-axis-label");

// Marks
const circle = g.selectAll("circle")
.data(data)
.join("circle")
.attr("fill", d => color(d.city));

const annotation = g.selectAll(".annotation")
.data(annotations)
.join("text")
.attr("class", "annotation")
.attr("fill", d => color(d.city))
.text(d => d.city);
return Object.assign(svg.node(), {
resize(size){
const r = 2;
const margin = { left: 56, right: 20, top: 32, bottom: 48 };
const width = size - margin.left - margin.right;
const height = Math.max(400, size) - margin.top - margin.bottom;
const offset = 33;

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

// Resize: Generators
xaxisGenerator.tickSize(height + 10);
yaxisGenerator.tickSize(width + 10);
// Resize: Scaffold
svg
.attr("width", size)
.attr("height", height + margin.top + margin.bottom);

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

// Resize: Scales
xaxis
.call(xaxisGenerator);

yaxis
.attr("transform", `translate(${width})`)
.call(yaxisGenerator);

const tick = yaxis.select(".tick:last-of-type");
tick.select("text").attr("x", -(yaxisGenerator.tickSize() + yaxisGenerator.tickPadding()) + offset)
tick.select("line").attr("x2", -yaxisGenerator.tickSize() + offset);
xaxisLabel
.attr("y", height + 40)
.html(`<tspan dx=-14 dy=4>Daily maximum temperature</tspan><tspan class="arrow" dx=4 dy=-1>→</tspan>`);

yaxisLabel
.attr("x", -margin.left)
.attr("y", -margin.top + 8)
.html(`<tspan dx=10 dy=4>Daily electricity demand</tspan><tspan class="arrow" x=${-margin.left} dy=-3>↑</tspan>`);

// Resize: Marks
circle
.attr("cx", d => x(d.tmax))
.attr("cy", d => y(d.demand))
.attr("r", r);

annotation
.attr("text-anchor", d => d.textAnchor(size))
.attr("x", d => x(d.x(size)))
.attr("y", d => y(d.y(size)));
}
});
}
Insert cell
display.resize(size)
Insert cell
Insert cell
css = `
.axis-label {
font-family: ${franklinLight};
font-size: 16px;
}
.axis-label .arrow {
font-size: 13px;
}
.axis-label.y-axis-label {
paint-order: stroke fill;
stroke: white;
stroke-linejoin: round;
stroke-width: 12px;
}
.axis-label.y-axis-label .arrow {
font-size: 13px;
paint-order: fill stroke;
stroke: none;
}

.axis .tick line {
stroke: #e9e9e9;
}
.axis .tick text {
font-family: ${franklinLight};
font-size: 14px;
}
.axis .domain {
display: none;
}

.annotation {
font-family: ${franklinLight};
font-weight: bold;
paint-order: stroke fill;
stroke: white;
stroke-linejoin: round;
stroke-width: 8px;
}
`
Insert cell
color = city => {
if (city === "New York") {
return "rgb(237, 125, 47)";
}
if (city === "Phoenix") {
return "rgb(145, 66, 35)";
}
return "#c0c0c0"
}
Insert cell
Insert cell
xaxisGenerator = d3.axisBottom(x)
.tickFormat(d => `${d}°F`)
.tickValues(d3.range(20, 140, 20));
Insert cell
yaxisGenerator = d3.axisLeft(y)
.tickFormat((d, i, e) => `${d / 1e3}K${i === e.length - 1 ? " MWh" : ""}`)
.tickValues(d3.range(50e3, 250e3, 25e3));
Insert cell
Insert cell
x = d3.scaleLinear()
.domain([20, 120])
Insert cell
y = d3.scaleLinear()
.domain([50e3, 225e3])
Insert cell
Insert cell
size = Math.min(width, 640)
Insert cell
Insert cell
annotations = [
{
city: "New York",
x: ww => 70,
y: ww => 160e3,
textAnchor: ww => "end"
}, {
city: "Phoenix",
x: ww => ww <= 380 ? 47 : ww <= 480 ? 118 : 110,
y: ww => ww <= 380 ? 80e3 : ww <= 480 ? 160e3 : 155e3,
textAnchor: ww => "end"
}
]
Insert cell
data = [
...phoenix,
...newyork
]
Insert cell
Insert cell
phoenix = phoenixDemand.map(d => {
return {
city: "Phoenix",
...d,
tmax: phoenixTmax.find(d0 => d0.date === d.date).tmax
}
})
Insert cell
// via https://www.eia.gov/electricity/gridmonitor/expanded-view/electric_overview/balancing_authority/AZPS/ElectricityOverview-13/edit
// you can only download one year at a time
phoenixDemand = [
...await FileAttachment("azps_20200101-20201231.csv").csv(),
...await FileAttachment("azps_20210101-20211231.csv").csv(),
...await FileAttachment("azps_20220101-20221231.csv").csv(),
...await FileAttachment("azps_20230101-20231231.csv").csv()
]
.filter(d => d["Demand (MWh)"]) // remove empty row at end of each file
.map(d => {
return {
date: formatDate(d["Timestamp (Hour Ending)"]),
demand: +d["Demand (MWh)"]
}
})
Insert cell
// via https://open-meteo.com/en/docs/historical-weather-api#latitude=33.4484&longitude=-112.074&start_date=2020-01-01&end_date=2023-12-31&hourly=&daily=temperature_2m_max&temperature_unit=fahrenheit&timezone=America%2FDenver
phoenixTmax = formatOpenMeteo(await FileAttachment("open-meteo-33.43N112.03W333m.csv").text())
Insert cell
Insert cell
newyork = newyorkDemand.map(d => {
return {
city: "New York",
...d,
tmax: newyorkTmax.find(d0 => d0.date === d.date).tmax
}
})
Insert cell
// via https://www.eia.gov/electricity/gridmonitor/expanded-view/electric_overview/balancing_authority/NYIS/ElectricityRegionDemand-20/edit
// you can only download one year at a time
newyorkDemand = [
...await FileAttachment("nyiso_20200101-20201231.csv").csv(),
...await FileAttachment("nyiso_20210101-20211231.csv").csv(),
...await FileAttachment("nyiso_20220101-20221231.csv").csv(),
...await FileAttachment("nyiso_20230101-20231231.csv").csv()
]
.filter(d => d["New York City Demand (MWh)"]) // remove empty row at end of each file
.map(d => {
return {
date: formatDate(d["Timestamp (Hour Ending)"]),
demand: +d["New York City Demand (MWh)"]
}
})
Insert cell
// via https://open-meteo.com/en/docs/historical-weather-api#latitude=40.7143&longitude=-74.006&start_date=2020-01-01&end_date=2023-12-31&hourly=&daily=temperature_2m_max&temperature_unit=fahrenheit&timezone=America%2FNew_York
newyorkTmax = formatOpenMeteo(await FileAttachment("open-meteo-40.74N74.04W51m.csv").text())
Insert cell
Insert cell
formatDate = timestamp => {
const date = timestamp.split(",")[0];
const [m, d, y] = date.split("/");
return `${y}-${m.padStart(2, 0)}-${d.padStart(2, 0)}`
}
Insert cell
formatOpenMeteo = text => {
const lines = text.split("\n")
const cols = ["date", "tmax"]
return lines
.slice(4)
.map(line => {
const [ date, tmaxString ] = line.split(",");
return {
date,
tmax: +tmaxString
}
})
}
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