ETC5523: Communicating with Data
Tutorial 10
Preparation
Below is a slightly modified version of the solar dashboard app we built in class. I have added a function to simulate solar production data. You’ll also want the solar data Download.
library(shiny)
library(DT)
library(readr)
library(dplyr)
library(ggplot2)
library(janitor)
library(tidyr)
library(lubridate)
add_solar_model <- function(solar_data, max_kw = 5) {
hours <- lubridate::hour(solar_data$datetime)
production <- ifelse(
hours >= 6 & hours <= 18,
max_kw * sin(pi * (hours - 6) / 12)^1.5 + rnorm(length(hours), mean = 0, sd = max_kw * 0.02),
0
)
# Ensure no negative values due to noise
production <- pmax(0, production)
solar_data |> mutate(production = production)
}
ui <- fluidPage(
headerPanel("Solar dashboard"),
sidebarPanel(
fileInput("solar_file", "Upload solar data CSV", accept = c(".csv")),
sliderInput("date_range", "Subset dates", min = as.Date('2024-07-15'), max = as.Date('2025-10-05'),
value = c(as.Date('2024-07-15'), as.Date('2025-10-05'))),
div(id = "price_inputs",
numericInput("electricity_price_1", "Price per kwh", value = 0.28, min = 0)
),
div(style = "text-align: right;",
actionButton("add_price", "Add another price"),
),
radioButtons("plot_geom", "Plot type",
choices = c("Line" = "line", "Histogram" = "hist"),
selected = "line"),
conditionalPanel(
condition = "input.plot_geom == 'hist'",
sliderInput("bins", "Number of bins", min = 5, max = 60, value = 30),
),
selectInput("aggregation_size", "Aggregation type", choices = c("None", "Daily", "Weekly", "Monthly")),
),
mainPanel(
tabsetPanel(
tabPanel("Data Plot", plotOutput("solar_plot"), downloadButton("download_plot", "Download Plot")),
tabPanel("Cost Plot", plotOutput("cost_plot"), downloadButton("download_cost_plot", "Download Cost Plot")),
tabPanel("Data", DTOutput("data_table"))
)
)
)
server <- function(input, output) {
n_prices <- reactiveVal(1)
observeEvent(input$add_price, {
n <- n_prices() + 1
n_prices(n)
insertUI(
selector = "#price_inputs",
where = "beforeEnd",
ui = numericInput(
paste0("electricity_price_", n),
paste("Price per kwh"), value = 0.28, min = 0)
)
})
solar_data <- reactive({
req(input$solar_file)
read.csv(input$solar_file$datapath) |>
pivot_longer(
cols = starts_with("X"),
names_to = "datetime",
values_to = "energy_kwh"
) |>
mutate(
datetime = gsub("X(\\d{2}\\.\\d{2})\\.\\.\\.(\\d{2}\\.\\d{2})", "\\1", datetime),
energy_kwh = as.numeric(energy_kwh),
datetime = paste0(DATE, " ", datetime),
datetime = lubridate::ymd_hm(datetime)
) |>
janitor::clean_names()
})
solar_data_filtered <- reactive({
req(solar_data())
solar_data() |>
filter(datetime >= input$date_range[1] & datetime <= input$date_range[2])
})
solar_data_aggregated <- reactive({
req(solar_data_filtered())
if (input$aggregation_size == "Daily") {
solar_data_filtered() |>
group_by(date) |>
summarise(energy_kwh = sum(energy_kwh, na.rm = TRUE)) |>
rename(datetime = date) |>
mutate(datetime = as.Date(datetime)) |>
ungroup()
} else if (input$aggregation_size == "Weekly") {
solar_data_filtered() |>
mutate(week = lubridate::floor_date(datetime, unit = "week")) |>
group_by(week) |>
summarise(energy_kwh = sum(energy_kwh, na.rm = TRUE)) |>
rename(datetime = week)
} else if (input$aggregation_size == "Monthly") {
solar_data_filtered() |>
mutate(month = lubridate::floor_date(datetime, unit = "month")) |>
group_by(month) |>
summarise(energy_kwh = sum(energy_kwh, na.rm = TRUE)) |>
rename(datetime = month)
} else {
solar_data_filtered()
}
})
solar_data_priced <- reactive({
req(prices())
solar_data_priced <- solar_data_filtered()
for (i in 1:n_prices()) {
solar_data_priced[, paste0("electricity_price_", i)] <- prices()[i] * solar_data_priced$energy_kwh
solar_data_priced[, paste0("total_cost_", i)] <- cumsum(solar_data_priced[, paste0("electricity_price_", i)])
}
solar_data_priced
})
solar_plot <- reactive({
plot <- ggplot(solar_data_aggregated()) +
labs(y = 'kWh', x = 'Date') +
theme_minimal()
if (input$plot_geom == "line") {
ggplot(solar_data_aggregated()) + geom_line(aes(x=datetime, y=energy_kwh)) +
labs(y='kWh', x='Date') +
theme_minimal()
} else if (input$plot_geom == "hist") {
ggplot(solar_data_aggregated()) + geom_histogram(bins = input$bins, aes(x = energy_kwh)) +
labs(y='Count', x='Energy (kWh)') +
theme_minimal()
}
})
output$solar_plot <- renderPlot({
solar_plot()
})
output$download_plot <- downloadHandler(
filename = function() { paste("solar_plot", Sys.Date(), ".png", sep = "") },
content = function(file) {
ggsave(file, plot = solar_plot(), device = "png", bg = "white", width = 8, height = 6)
}
)
output$download_cost_plot <- downloadHandler(
filename = function() { paste("cost_plot", Sys.Date(), ".png", sep = "") },
content = function(file) {
ggsave(file, plot = cost_plot(), device = "png", bg = "white", width = 8, height = 6)
}
)
output$data_table <- renderDT({
req(solar_data())
solar_data_filtered()
})
prices <- reactive({
ids <- paste0("electricity_price_", 1:n_prices())
values <- sapply(ids, function(id) input[[id]])
values[!is.na(values)]
})
cost_plot <- reactive({
req(solar_data_priced())
req(prices())
solar_data_priced() |>
select(datetime, starts_with("total_cost_")) |>
pivot_longer(!c(datetime, starts_with("electricity_price_")), values_to = "total_cost", names_to = "cost_per_kwh") |>
mutate(
cost_per_kwh = factor(cost_per_kwh, labels = scales::dollar(prices()))
) |>
ggplot(aes(x = datetime, y = total_cost, colour = cost_per_kwh)) +
geom_line() +
labs(y = 'Cost ($)', x = 'Date', colour = "Cost/kwh") +
theme_minimal()
})
output$cost_plot <- renderPlot({
cost_plot()
})
}
shinyApp(ui, server)You use the add_solar_model function to simulate solar production data. For example:
solar_data |> add_solar_model(max_kw = 5)and a new column called production will be added to the data frame.
Exercise 1
Modify the app so that the user can select the max_kw parameter via a numeric input. The slider should allow values between 1 and 20, with a default value of 5.
Add a plot to the tabset that shows the simulated production data over time.
Exercise 2
Design plots that show:
- How effective a solar system of a given size can be at reducing electricity costs (as a sales pitch)
- Our sales team wants to increase battery sales. To do this, they would like to show the customer the total amount of production in a day that they are not using. Create a text summary that displays this “wasted” energy.
Hints for part 1:
Instead of looking at raw production data, we should instead look at net consumption, that is usage minus production. This can be done by mutating a new column in solar_data_priced with mutate(net_use = energy_kwh - production)
Then, we need to change our cost calculation to use net_use instead of energy_kwh. But we also need to ensure we don’t get money back for excess energy.
pmax to ensure the value isn’t negative.
- What do you notice about the money saved by having solar panels, versus the size of the solar system? Why does it seem so low?