# =========================
# LIBRERÍAS
# =========================
library(shiny) # Crea apps web
library(bslib) # Temas modernos y layout responsive
library(bsicons) # Íconos Bootstrap
library(reactable) # Tablas interactivas
library(tidyverse) # dplyr, ggplot2, etc.
library(leaflet) # Mapas interactivos
library(leaflet.extras) # Heatmaps y capas extra
# =========================
# DATOS
# =========================
dfConsumos <- read.csv( # Lee el archivo CSV
file = "datos/consuEpeAnonGeo.csv", # Ruta del archivo
sep = ";", # Separador ;
fileEncoding = "ISO-8859-1" # Codificación de caracteres
)
Agencia <- dfConsumos %>% # Crea el vector de agencias
pull(Agencia) %>% # Extrae la columna Agencia
unique() %>% # Elimina repetidos
sort() # Orden alfabético
# =========================
# UI
# =========================
ui <- bslib::page_navbar( # Página con barra superior
theme = bslib::bs_theme(bootswatch = "flatly"), # Tema visual Flatly
title = "Análisis de Consumos Eléctricos", # Título en la barra
sidebar = bslib::sidebar( # Sidebar lateral
title = "Panel de Control", # Título sidebar
numericInput( # Input año
inputId = "Anio", # ID interno
label = "Año", # Texto visible
value = 2020, # Valor inicial
min = min(dfConsumos$Anio), # Mínimo permitido
max = max(dfConsumos$Anio) # Máximo permitido
),
radioButtons( # Input agencia
inputId = "Agencia", # ID interno
label = "Agencia", # Texto visible
choices = Agencia, # Lista de agencias
selected = "Oficina Comercial Rioja" # Valor inicial
)
),
# =========================
# PESTAÑA TABLERO
# =========================
bslib::nav_panel(
title = "Tablero", # Nombre pestaña
bslib::layout_columns( # 2 value boxes lado a lado
fill = FALSE, # No ocupar alto completo
bslib::value_box( # Primer valor
title = "Cantidad de Usuarios", # Nombre
value = textOutput("n_usu"), # Valor mostrado
showcase = bs_icon("person-fill") # Ícono
),
bslib::value_box( # Segundo valor
title = "Consumo Total (kWh)", # Nombre
value = textOutput("n_consumo"), # Valor mostrado
showcase = bs_icon("lightning-charge-fill")# Ícono
)
),
bslib::card( # Tarjeta con gráfico
full_screen = TRUE, # Puede pantalla completa
bslib::card_header("Top 5 Consumidores del Año Seleccionado"), # Título
plotOutput(outputId = "gg") # Aquí va el gráfico
)
),
# =========================
# PESTAÑA MAPA
# =========================
bslib::nav_panel(
title = "Mapa", # Título pestaña
bslib::card( # Tarjeta del mapa
full_screen = TRUE, # Modo full screen
bslib::card_header("Clientes georreferenciados"), # Encabezado
leafletOutput("mapa", height = "600px") # Mapa de leaflet
)
),
# =========================
# PESTAÑA DATOS
# =========================
bslib::nav_panel(
title = "Datos", # Nombre pestaña
bslib::card( # Tarjeta con tabla
full_screen = TRUE, # Ocupa pantalla
bslib::card_header("Tabla de Datos Filtrados"), # Título
reactableOutput("tabla") # Tabla reactable
)
),
# =========================
# ÍTEMS A LA DERECHA DEL NAVBAR
# =========================
bslib::nav_spacer(), # Empuja lo siguiente a la derecha
bslib::nav_item( # Menú Mensajes
div(
class = "dropdown", # Dropdown Bootstrap
a(
"Mensajes", # Texto del botón
class = "nav-link dropdown-toggle", # Estilo + comportamiento
href = "#", # No navega
`data-bs-toggle` = "dropdown" # Activa el menú
),
div(
class = "dropdown-menu dropdown-menu-end", # Menú alineado derecha
div(class = "dropdown-item", strong("Alumno: "), "Andrés Guiu"),
div(class = "dropdown-item", strong("Profesor: "), "Mag. Marcos Prunello"),
div(class = "dropdown-item", strong("Profesor: "), "Mag. Diego Marfetán Molina")
)
)
),
bslib::nav_item( # Link externo EPE
a(
"EPE", # Texto link
href = "https://epe.santafe.gov.ar/", # URL
target = "_blank" # Abrir nueva pestaña
)
)
)
# =========================
# SERVER
# =========================
server <- function(input, output) { # Lógica de la app
datos_filtrados <- reactive({ # Filtra datos seg inputs
dfConsumos %>%
filter(
Anio == input$Anio, # Año elegido
Agencia == input$Agencia # Agencia elegida
)
})
output$n_usu <- renderText({ # ValueBox 1
n <- nrow(datos_filtrados()) # Cuenta registros
scales::label_number(
big.mark=".", decimal.mark=",") (n) # Formato 12.345
})
output$n_consumo <- renderText({ # ValueBox 2
total <- datos_filtrados() %>% # Toma datos filtrados
pull(UltCons) %>% # Extrae consumos
sum(na.rm = TRUE) # Suma todo
scales::label_number(
big.mark=".", decimal.mark=",") (total) # Formato 999.999
})
output$gg <- renderPlot({ # Gráfico Top 5
datos_filtrados() %>%
slice_max(UltCons, n = 5) %>% # Los 5 mayores
arrange(UltCons) %>% # Orden ascendente
mutate(
Nombre = factor(Nombre, levels = Nombre), # Mantiene orden
etiqueta = paste0(Nombre, ": ", round(UltCons), " kWh")
) %>%
ggplot(aes(x = UltCons, y = Nombre, fill = UltCons)) +
geom_bar(stat = "identity") + # Barras
geom_text(aes(x = 0, label = etiqueta), hjust = 0) + # Texto izq
scale_fill_gradient(low="#f7fcba", high="#cd2710") + # Paleta
scale_x_continuous(name = "
Consumo (kWh)") +
theme_bw() +
theme(
legend.position = "none",
axis.text.y = element_blank(),
axis.title.y = element_blank(),
axis.ticks.y = element_blank()
)
})
output$tabla <- renderReactable({ # Tabla de datos
datos_filtrados() %>%
select(
`Nro. Cliente` = NroCli,
`Nombre` = Nombre,
`BT/RE` = Btre,
`Consumo Promedio` = ConProm,
`Último Consumo` = UltCons
) %>%
reactable()
})
output$mapa <- renderLeaflet({ # Construye mapa
df_map <- datos_filtrados() %>% # Filtrado
mutate(
lat_sf = as.numeric(lat_sf), # Asegura numeric
lon_sf = as.numeric(lon_sf)
) %>%
filter(!is.na(lat_sf), !is.na(lon_sf)) # Quita NA coordenadas
if (nrow(df_map) == 0) { # Si no hay puntos
return(leaflet() %>% addProviderTiles("OpenStreetMap"))
}
lon_min <- min(df_map$lon_sf) # Bounds min lon
lon_max <- max(df_map$lon_sf) # Bounds max lon
lat_min <- min(df_map$lat_sf) # Bounds min lat
lat_max <- max(df_map$lat_sf) # Bounds max lat
leaflet(df_map) %>% # Mapa base
addProviderTiles("OpenStreetMap") %>%
addCircleMarkers( # Marcadores
lng = ~lon_sf,
lat = ~lat_sf,
radius = 4,
stroke = FALSE,
fillOpacity = 0.8,
popup = ~paste0(
"<b>Cliente:</b> ", Nombre, "<br>",
"<b>Agencia:</b> ", Agencia, "<br>",
"<b>Último consumo:</b> ", round(UltCons), " kWh"
)
) %>%
fitBounds(lon_min, lat_min, lon_max, lat_max) %>% # Centrado mapa
addHeatmap( # Capa heatmap
lng = ~lon_sf,
lat = ~lat_sf,
intensity = ~1,
blur = 25,
radius = 20
)
})
}
# =========================
# Iniciar la app
# =========================
shinyApp(ui, server) # Ejecuta la appAnálisis de Consumos Eléctricos - App Shiny
Introducción
En este documento se presenta el código completo y comentado de una aplicación Shiny para analizar consumos eléctricos de clientes de la EPESF.
La app incluye:
- Un tablero con indicadores y un gráfico de los 5 mayores consumidores.
- Un mapa interactivo con georreferenciación y capa de heatmap.
- Una tabla interactiva con los datos filtrados.
- Un encabezado con un pequeño menú de mensajes y un enlace a la página de la EPE.
El objetivo es que el código sea fácil de leer y reutilizar como base para trabajos prácticos o futuros desarrollos.
Estructura general de la app
La app sigue la estructura clásica de Shiny:
- Carga de librerías y datos.
- Definición de la interfaz de usuario (UI).
- Definición de la lógica del servidor (server).
- Llamado a
shinyApp(ui, server)para ejecutar la aplicación.
A continuación se muestra el código completo, con comentarios en cada bloque.