app3 is a Shiny application for monitoring the corrected temperature of refrigeration or incubation equipment (estufas) using probe data exported from a data logger. The user uploads an Excel or CSV file at runtime and the app processes it interactively.
The application allows the user to:
Upload a data file (.xls, .xlsx, or .csv) containing temperature records,
Filter records by equipment (EquipoD) and by one or more dates,
Apply an equipment-specific correction factor to the raw temperature readings,
Visualise the corrected temperature over time as an interactive step-line chart with tolerance-limit reference lines,
Inspect deviations above and below the tolerance range in tabular form,
Identify the maximum deviation in each direction (and the date on which it occurred) through value boxes.
Unlike applications that load data at startup from a fixed file, app3 requires the user to upload a file each session via the fileInput widget (inputId = "subir"). The accepted formats are .xls, .xlsx, and .csv.
Raw columns expected in the file
Column
Type
Description
Eq_Date_Id
Date / datetime
Timestamp of each temperature reading — renamed to Fecha
EquipoD
Character
Equipment identifier (e.g. "Estufa 1652")
IdEquipo
Character
Probe / sensor identifier displayed in the report header
datos1() is the cleaned data frame used everywhere else in the server. All numeric columns are rounded to two decimal places and EquipoD is converted to a factor to make it sortable and usable as a grouping variable.
Equipment parameters (hard-coded)
Each equipment name maps to a set of calibration and tolerance parameters stored inside a switch() statement in datos_equipos():
Equipment
Correction factor
Lower limit (°C)
Upper limit (°C)
Estufa 1652
+0.24
29.29
29.82
Estufa 654
+0.41
41.20
41.90
These values are used to compute Temperatura_corregida and to classify each reading as within or outside the tolerance range.
User Interface (UI)
The UI is built with bslib::page_fluid(), which stacks all components vertically on the page.
Two UI elements are rendered after the data file is uploaded, because their choices depend on the file contents:
Output
Widget produced
Depends on
equipos
selectizeInput (updated via observe)
equipos() — factor levels of EquipoD
selec_fecha
pickerInput (multi-select, with “Select all”)
fechas() — unique dates in the uploaded file
Analysis trigger
All outputs that constitute the actual analysis (chart, header, tables, value boxes) are gated behind req(input$analisis). Nothing renders until the user presses ANALIZAR. This prevents partial or misleading output from appearing while the user is still adjusting the equipment or date filters.
This reactive is a lookup table: it maps the selected equipment name to a one-row tibble containing the correction factor and tolerance limits. It is consumed by subsetted(), the chart, and the summary header.
subsetted() — filtered and corrected data
The central reactive. Filters datos1() to the selected equipment and dates, removes zero-temperature records (sensor artefacts), and adds the corrected temperature column:
Show code
subsetted <-reactive(datos1() %>%filter( EquipoD ==equip() & Temperatura >0&as.Date(Fecha) %in% input$fechas ) %>%mutate(Temperatura_corregida = Temperatura +datos_equipos()$factor_correc))
Temperatura > 0 guards against logger artefacts (readings of 0 °C that indicate a sensor fault, not a real measurement).
Deviation datasets
Four reactives derive deviation sub-tables from subsetted():
geom_step() is used instead of geom_line() because temperature logger data is recorded at fixed intervals and the value is constant between readings — a step function accurately represents this physical reality. The two geom_hline() calls draw the lower and upper tolerance limits so deviations are immediately visible.
Summary header (output$fact_coerr)
Rendered as raw HTML, this block acts as a structured report header that summarises the key facts about the current analysis:
Show code
output$fact_coerr <-renderUI({req(input$analisis)HTML(paste0("<b>EQUIPO: </b>", unique(input$equipos), "</br>","<b>SONDA: </b>", unique(subsetted()$IdEquipo), "</br>","<b>FACTOR DE CORRECCIÓN: </b>",unique(datos_equipos()$factor_correc), "</br>","<b>TEMPERATURA DE TRABAJO: </b>",unique(datos_equipos()$lim_inf), " °C - ",unique(datos_equipos()$lim_sup), " °C", "</br>","<b>INICIO DE REGISTRO: </b>", unique(inicio()), "</br>","<b>FIN DE REGISTRO: </b>", unique(fin()), "</br>","<b>TEMPERATURA MÁXIMA: </b>", unique(max()), " °C", "</br>","<b>TEMPERATURA MÍNIMA: </b>", unique(min()), " °C", "</br>","<b>TEMPERATURA PROMEDIO: </b>",unique(promedio())," °C" ))})
Deviation tables (output$tabla – output$tabla4)
All four deviation tables follow the same pattern: pass the relevant reactive data frame to flextable(), apply autofit(), and convert to HTML with htmltools_value():
flextable is chosen over DT or knitr::kable() because it produces ready-to-print tables that can be directly pasted into Word or PDF reports, which is consistent with the lab-documentation context of this application.
Value boxes (output$max_desv_inf, output$max_desv_sup)
The two value boxes display the single worst deviation in each direction together with the date on which it occurred:
Show code
output$max_desv_inf <-renderUI({req(input$analisis)HTML(paste0(max_desv_inf(), " °C - ", fecha_max_desv_inf()))})output$max_desv_sup <-renderUI({req(input$analisis)HTML(paste0(max_desv_sup(), " °C - ", fecha_max_desv_sup()))})
Because these outputs sit inside a layout that is always visible (not inside a conditionalPanel), they are kept alive explicitly so that Shiny does not suspend them when the section scrolls out of view:
App launches
│
▼
User uploads file (input$subir)
│
└──► datos_xls() / datos1() built
│
├── equipos() ──► "equipos" selector populated
└── fechas() ──► "selec_fecha" picker rendered (all dates pre-selected)
User selects equipment (input$equipos)
│
└──► datos_equipos() recomputes (correction factor + limits for selected unit)
User adjusts date selection (input$fechas) — optional, all pre-selected
│
▼
User presses ANALIZAR (input$analisis)
│
▼
subsetted() filters and corrects temperatures
│
├── Scalar summaries: max(), min(), promedio(), inicio(), fin()
│
├── Deviation sub-tables:
│ desv_inf() ──► desv_inf1()
│ desv_sup() ──► desv_sup1()
│
├──► output$grafico step-line chart with tolerance lines
├──► output$fact_coerr HTML summary header
│
├──► output$tabla below-range deviations
├──► output$tabla2 above-range deviations
├──► output$tabla3 below-range deviations > 1 °C
├──► output$tabla4 above-range deviations > 1 °C
│
├──► output$max_desv_inf worst negative deviation + date
└──► output$max_desv_sup worst positive deviation + date
Design observations
Strengths
eventReactive for file upload. Using eventReactive(input$subir, ...) ensures the data is read exactly once per upload event and cached reactively, avoiding redundant file I/O on every downstream recalculation.
req(input$analisis) gate. Requiring the button press before rendering any output prevents the user from seeing incomplete results while still adjusting filters, and makes the analysis feel intentional.
Temperatura > 0 guard. Filtering out zero-temperature records defensively handles a common data-logger artefact without requiring any user action.
geom_step() for logger data. Accurately represents the piecewise-constant nature of fixed-interval temperature records.
base::max() / base::min() qualification. Explicitly namespacing these calls avoids silent errors caused by dplyr masking the base R generics.
suspendWhenHidden = FALSE on value boxes. Ensures the deviation outputs remain computed even when out of view, which is also necessary if their values were ever used to drive a conditionalPanel.
Opportunities for refactoring (for future reference)
Hard-coded equipment parameters. The switch() block in datos_equipos() must be manually edited each time a new piece of equipment is added. Storing these parameters in a separate lookup table (a small Excel sheet or a named list in a config.R file) and joining them at runtime would make the application data-driven and easier to maintain.
Repeated renderUI / flextable pattern. The four deviation-table outputs are structurally identical. A helper function parametrised by the reactive data frame would eliminate the repetition.
Single-equipment selector.selectizeInput allows only one equipment to be selected at a time (no multiple = TRUE). If comparisons across units are ever needed, switching to a pickerInput with multi-select would require minimal changes to the downstream reactives.
No input validation for missing columns. If the uploaded file does not contain the expected columns (Eq_Date_Id, EquipoD, Temperatura, IdEquipo), the app will crash with an uninformative error. Adding a validate() / need() check in datos1() would produce a user-friendly message instead.