Skip to content

Commit efd8ba0

Browse files
pettirossFil
andauthored
New tip for demand graph in EIA (US Energy) example (#1041)
* 1. Remove unused columns from data loader 2. Move row filtering from page to data loader 3. Change tooltip in line chart to show all values for a given date so values are not obscured 4. Add comments and minor code decluttering * Use default colors for hover tip * Rolled up data in a separate table for tips * Rollup to tip (cont'd) * Hover feedback to dashed line * git hygiene — rename this file instead of delete+new (pt 1) * git hygiene pt 2 — Delete us-demand.zip.js * git hygiene pt 3 — rename us-demand.csv.js to us-demand.zip.js * git hygiene pt 4 — trying CLI mv instead of github UI * git hygiene pt 5 — CLI mv * Remove redundant data type labels * Remove chart junk * Remove second "date" from tip * Remove option that does nothing * Add semicolons * Add semicolons * Revert to single csv data loader instead of zip * Change date to more global friendly format * minimize diff & tighter loop * adopt d3.rollup --------- Co-authored-by: Philippe Rivière <[email protected]>
1 parent 429b1f3 commit efd8ba0

File tree

3 files changed

+84
-42
lines changed

3 files changed

+84
-42
lines changed

examples/eia/docs/components/charts.js

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
import * as Plot from "npm:@observablehq/plot";
2-
import {extent} from "npm:d3";
2+
import {extent, format, rollup, timeFormat} from "npm:d3";
3+
4+
function friendlyTypeName(d) {
5+
switch (d) {
6+
case "demandActual":
7+
return "Demand (actual)";
8+
case "demandForecast":
9+
return "Demand (forecast)";
10+
case "netGeneration":
11+
return "Net generation";
12+
}
13+
}
314

415
// Top 5 balancing authorities chart
516
export function top5BalancingAuthoritiesChart(width, height, top5Demand, maxDemand) {
@@ -17,29 +28,67 @@ export function top5BalancingAuthoritiesChart(width, height, top5Demand, maxDema
1728
fill: "#9498a0",
1829
sort: {y: "x", reverse: true, limit: 10},
1930
tip: true,
20-
title: ({ name, value }) => `name: ${name}\ndemand: ${value / 1000} GWh`
31+
title: ({name, value}) => `name: ${name}\ndemand: ${value / 1000} GWh`
2132
})
2233
]
2334
});
2435
}
2536

2637
// US electricity demand, generation and forecasting chart
27-
export function usGenDemandForecastChart(width, height, usDemandGenForecast, currentHour) {
38+
export function usGenDemandForecastChart(width, height, data, currentHour) {
39+
// Roll up each hour's values into a single row for a cohesive tip
40+
const compoundTips = rollup(
41+
data,
42+
(v) => ({...v[0], ...Object.fromEntries(v.map(({name, value}) => [name, value]))}),
43+
(d) => d.date
44+
).values();
45+
2846
return Plot.plot({
2947
width,
3048
marginTop: 0,
3149
height: height - 50,
32-
y: {label: null},
33-
x: {type: "time", tickSize: 0, tickPadding: 3},
50+
y: {tickSize: 0, label: null},
51+
x: {type: "time", tickSize: 0, tickPadding: 3, label: null},
3452
color: {
3553
legend: true,
36-
domain: ["Day-ahead demand forecast", "Demand", "Net generation"],
37-
range: ["#6cc5b0", "#ff8ab7", "#a463f2"]
54+
domain: ["demandActual", "demandForecast", "netGeneration"],
55+
tickFormat: friendlyTypeName,
56+
range: ["#ff8ab7", "#6cc5b0", "#a463f2"]
3857
},
3958
grid: true,
4059
marks: [
4160
Plot.ruleX([currentHour], {strokeOpacity: 0.5}),
42-
Plot.line(usDemandGenForecast, {x: "date", y: (d) => d.value / 1000, stroke: "name", strokeWidth: 1.2, tip: true})
61+
Plot.line(data, {
62+
x: "date",
63+
y: (d) => d.value / 1000,
64+
stroke: "name",
65+
strokeWidth: 1.2
66+
}),
67+
Plot.ruleX(
68+
compoundTips,
69+
Plot.pointerX({
70+
x: "date",
71+
strokeDasharray: [2, 2],
72+
channels: {
73+
date: {value: "date", label: "Time"},
74+
demandActual: {value: "demandActual", label: friendlyTypeName("demandActual")},
75+
demandForecast: {value: "demandForecast", label: friendlyTypeName("demandForecast")},
76+
netGeneration: {value: "netGeneration", label: friendlyTypeName("netGeneration")}
77+
},
78+
tip: {
79+
format: {
80+
date: (d) => timeFormat("%-d %b %-I %p")(d),
81+
demandActual: (d) => `${format(".1f")(d / 1000)} GWh`,
82+
demandForecast: (d) => `${format(".1f")(d / 1000)} GWh`,
83+
netGeneration: (d) => `${format(".1f")(d / 1000)} GWh`,
84+
x: false
85+
},
86+
fontSize: 12,
87+
anchor: "bottom",
88+
frameAnchor: "top"
89+
}
90+
})
91+
)
4392
]
4493
});
4594
}
Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,31 @@
11
import * as d3 from "d3";
22

3-
const end = d3.timeDay.offset(d3.timeHour(new Date()), 1)
3+
// Construct web API call for last 7 days of hourly demand data in MWh
4+
// Types: DF = forecasted demand, D = demand (actual), NG = net generation
5+
const end = d3.timeDay.offset(d3.timeHour(new Date()), 1);
6+
const start = d3.timeHour(d3.utcDay.offset(end, -7));
7+
const convertDate = d3.timeFormat("%m%d%Y %H:%M:%S");
8+
const usDemandUrl = `https://www.eia.gov/electricity/930-api/region_data/series_data?type[0]=DF&type[1]=D&type[2]=NG&start=${convertDate(start)}&end=${convertDate(end)}&frequency=hourly&timezone=Eastern&limit=10000&respondent[0]=US48`;
49

5-
const start = d3.timeHour(d3.utcDay.offset(end, -7))
10+
const datetimeFormat = d3.utcParse("%m/%d/%Y %H:%M:%S");
11+
const dateFormat = d3.utcParse("%m/%d/%Y");
12+
const typeNameRemap = {DF: "demandForecast", D: "demandActual", NG: "netGeneration"};
613

7-
const convertDate = d3.timeFormat("%m%d%Y %H:%M:%S")
8-
9-
const usDemandUrl = `https://www.eia.gov/electricity/930-api/region_data/series_data?type[0]=D&type[1]=DF&type[2]=NG&type[3]=TI&start=${convertDate(start)}&end=${convertDate(end)}&frequency=hourly&timezone=Eastern&limit=10000&respondent[0]=US48`
10-
11-
const tidySeries = (response, id, name) => {
12-
let series = response[0].data
13-
let datetimeFormat = d3.utcParse("%m/%d/%Y %H:%M:%S")
14-
let dateFormat = d3.utcParse("%m/%d/%Y")
15-
return series.flatMap(s => {
16-
return s.VALUES.DATES.map((d,i) => {
17-
return {
18-
id: s[id],
19-
name: s[name],
20-
date: datetimeFormat(d) ? datetimeFormat(d) : dateFormat(d),
21-
value: s.VALUES.DATA[i],
22-
reported: s.VALUES.DATA_REPORTED[i],
23-
imputed: s.VALUES.DATA_IMPUTED[i]
24-
}
25-
})
14+
// Flatten JSON from date / type / value hierarchy to a tidy array
15+
const jsonToTidy = (data, id) => {
16+
let series = data[0].data
17+
return series.flatMap(s => {
18+
return s.VALUES.DATES.map((d, i) => {
19+
return {
20+
name: typeNameRemap[s[id]],
21+
date: datetimeFormat(d) ?? dateFormat(d),
22+
value: s.VALUES.DATA[i]
23+
}
2624
})
27-
}
25+
})
26+
};
2827

29-
const usOverviewSeries = await d3.json(usDemandUrl).then(response => {
30-
return tidySeries(response, "TYPE_ID", "TYPE_NAME")
31-
});
28+
const jsonData = await d3.json(usDemandUrl);
29+
const tidySeries = jsonToTidy(jsonData, "TYPE_ID");
3230

33-
process.stdout.write(d3.csvFormat(usOverviewSeries));
31+
process.stdout.write(d3.csvFormat(tidySeries));

examples/eia/docs/index.md

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,6 @@ import {balancingAuthoritiesLegend, balancingAuthoritiesMap} from "./components/
1414
const countryInterchangeSeries = FileAttachment("data/country-interchange.csv").csv({typed: true});
1515
```
1616

17-
```js
18-
// US overall demand, generation, forecast
19-
const usOverview = FileAttachment("data/us-demand.csv").csv({typed: true});
20-
```
21-
2217
```js
2318
const baHourlyDemand = FileAttachment("data/eia-ba-hourly.csv").csv({typed: true});
2419
```
@@ -50,8 +45,8 @@ const eiaPoints = FileAttachment("data/eia-system-points.json").json().then(d =>
5045
```
5146

5247
```js
53-
// US total demand, generation and forecast excluding total (sum)
54-
const usDemandGenForecast = usOverview.filter(d => d.name != "Total interchange");
48+
// US total demand, generation and forecast
49+
const usDemandGenForecast = FileAttachment("data/us-demand.csv").csv({typed: true});
5550
```
5651

5752
```js
@@ -176,7 +171,7 @@ function centerResize(render) {
176171
${resize((width, height) => top5BalancingAuthoritiesChart(width, height, top5LatestDemand, maxDemand))}
177172
</div>
178173
<div class="card grid-colspan-2">
179-
<h2>US electricity generation, demand, and demand forecast (GWh)</h2>
174+
<h2>US electricity generation demand vs. day-ahead forecast (GWh)</h2>
180175
${resize((width, height) => usGenDemandForecastChart(width, height, usDemandGenForecast, currentHour))}
181176
</div>
182177
<div class="card grid-colspan-2">

0 commit comments

Comments
 (0)