ETC5523: Communicating with Data
Tutorial 10: One calculation, three stories
🎯 Objectives
By the end of this tutorial, you will be able to:
- build interval solar, grid-flow and battery-state calculations step by step;
- reuse one reactive calculation across several outputs;
- generate narrative text from reactive results;
- create persuasive and sceptical framings without changing the evidence; and
- audit an interactive communication for misleading differences.
What you will build
One dashboard presents the same household energy scenario in three ways:
- Why invest: a salesperson foregrounds bill savings and the value of storage.
- Energy balance: a neutral analyst explains consumption, production, imports and exports.
- Cautious case: a sceptic foregrounds up-front cost, payback and model limitations.
All three tabs must use the same scenario_data(), metrics() and daily_data() reactives.
Your framing may select and emphasise different consequences. It may not silently change the data, system size, tariffs, battery or calculation.
Preparation
If you completed the workshop, continue with your workshop_app.R: it already contains the tabset and calculation-trace tools used below.
Otherwise, create a folder for the tutorial and download:
- the starter app;
- the household energy data; and
- optionally, the complete dashboard for comparison after you finish.
Keep the app file and solar_data.csv in the same folder, then open the app and select Run App. If you are continuing from the workshop, retain the Evidence tab while completing the three audience tabs.
The supplied data are already in long form. Each row records half an hour of observed household consumption. The app adds deterministic modelled solar production and a simplified battery dispatch.
1. Build the solar calculation outside Shiny
Before working with reactives, reproduce the calculation for one day.
Run:
worked_day <- consumption_data |>
filter(date == as.Date("2024-12-06"))Add these columns one at a time:
- interval_hours: infer the interval from the difference between consecutive datetimes;
- hour_of_day: combine the hour and minute as a decimal;
- daylight: create a curve that is zero outside 6 am–6 pm and peaks at midday;
- seasonal: create the summer–winter scaling factor; and
- solar_kwh: multiply system capacity, daylight, season and interval length.
Use a 5 kW system.
Check your result:
- solar production should be zero at midnight;
- it should peak near midday;
- no value should be negative; and
- doubling the system size should double every solar value.
Plot observed consumption and modelled production for the day. Label them so a reader cannot confuse observed and modelled quantities.
2. Move from energy balance to grid flows
Add net demand:
worked_day <- worked_day |>
mutate(net_demand_kwh = energy_kwh - solar_kwh)For each row:
- positive net demand requires energy from the battery or grid;
- negative net demand is surplus available to charge the battery or export.
First calculate the no-battery case using pmax():
worked_day <- worked_day |>
mutate(
grid_import_without_battery_kwh = pmax(net_demand_kwh, 0),
grid_export_without_battery_kwh = pmax(-net_demand_kwh, 0)
)Why is the minus sign needed in the export calculation?
3. Trace the battery as a state through time
A battery cannot be calculated independently row by row. Its state at one interval depends on its state at the previous interval.
Start with this deliberately simple sequence:
| Interval | Consumption | Solar | Net demand |
|---|---|---|---|
| 1 | 1 | 0 | 1 |
| 2 | 1 | 3 | −2 |
| 3 | 1 | 2 | −1 |
| 4 | 1 | 0 | 1 |
| 5 | 1 | 0 | 1 |
| 6 | 1 | 0 | 1 |
Assume:
- a 2 kWh battery;
- an empty initial battery;
- no charging or discharging losses.
For each row, calculate battery state, grid import and grid export. Capacity cannot exceed 2 kWh and state cannot fall below zero.
Now inspect dispatch_battery() in the starter app. Match each part of the function to one row of your manual trace:
- read net demand;
- calculate energy stored or delivered;
- enforce capacity or available state with min();
- update state; and
- record the remaining import or export.
The app uses the square root of round-trip efficiency for both charging and discharging. Explain why applying 90% twice would not represent 90% round-trip efficiency.
4. Turn grid flows into costs
For every interval, the baseline buys all observed consumption from the grid:
baseline_cost <- energy_kwh * import_tariffThe scenario pays for imports and receives a credit for exports:
scenario_cost <-
grid_import_kwh * import_tariff -
grid_export_kwh * feed_in_tariff
interval_saving <- baseline_cost - scenario_costCheck the signs using one imported and one exported interval. Explain why exported electricity is not necessarily “wasted”.
Only after completing the interval calculation should the app sum energy and costs, calculate self-sufficiency, annualise savings and estimate simple payback.
5. Trace the common reactive calculation
Before changing the app, locate these three reactive objects:
- scenario_data() filters the observed records, adds solar production, dispatches the battery and calculates interval costs;
- metrics() reduces that scenario to values used in headlines; and
- daily_data() prepares daily totals for plotting.
For each input below, predict which reactive objects should invalidate:
- date range;
- solar system size;
- import tariff;
- battery capacity; and
- active tab.
6. Build the salesperson framing
The salesperson wants the user to ask:
What do I gain from installing this system?
In the Why invest tab, replace the placeholder with:
h3(textOutput("sales_headline", inline = TRUE)),
textOutput("sales_message"),
plotOutput("sales_plot", height = "430px")In the server:
- create a headline containing the selected period’s bill saving;
- mention self-sufficiency and, when included, energy redirected by the battery; and
- plot cumulative bill savings over time.
Do not calculate any new scenario inside an output. Use metrics() and daily_data().
7. Build the cautious framing
The sceptic wants the user to ask:
What could make this investment unattractive?
In the Cautious case tab, add:
h3(textOutput("sceptic_headline", inline = TRUE)),
textOutput("sceptic_message"),
plotOutput("sceptic_plot", height = "430px")Create:
- a headline reporting simple payback;
- text that names at least two limitations; and
- a plot of cumulative savings after subtracting the up-front cost.
Again, use the existing common reactives.
8. Audit the three views
Run the app and record the following for each tab:
| View | Main claim | Foregrounded measure | Important context left secondary |
|---|---|---|---|
| Why invest | |||
| Energy balance | |||
| Cautious case |
Then test at least three combinations of:
- solar system size;
- battery included or excluded;
- import and feed-in tariffs; and
- date range.
Check that:
- the text updates when the inputs change;
- the three views remain numerically compatible;
- no tab suggests that observed consumption is modelled;
- no tab presents modelled production as observed; and
- battery claims disappear when the battery is excluded.
9. Find the ethical boundary
Choose one change below and implement it temporarily:
- show only summer months in the salesperson view;
- remove the up-front-cost line from the cautious view;
- use different tariffs in different tabs; or
- rename “modelled solar production” as “solar production”.
Discuss with a partner:
- Does the change merely guide attention?
- What would a reasonable user infer?
- What label or disclosure would make the choice defensible?
- Should the change remain in the dashboard?
Restore a defensible version before continuing.
A transparent subset can be legitimate. A hidden change to the calculation is not.
10. Test the non-ideal states
Try to produce:
- a very short date range;
- a small solar system and expensive battery;
- a zero feed-in tariff; and
- a scenario with no battery.
For each state, ask:
- Does every output still make sense?
- Does the app explain an awkward result?
- Does the user know what to change next?
Add one validate(need()) message or one conditional sentence that improves a weak state.
11. Writer’s note
Complete both sentences:
My strongest legitimate framing choice is…
It would become misleading if…
Extension: let the evidence travel
Add a fourth tab containing:
downloadButton("download_data", "Download common scenario data")Connect it to scenario_data() with downloadHandler(). The downloaded data should include observed consumption, modelled production, battery state, grid imports, grid exports and interval savings.
The complete implementation is available in the finished dashboard.