1 Welcome

It uses synthetic data (generated on the fly) to tell a story about a fictional city’s mobility, energy demand, and air quality across a year.

2 Libraries

library(dplyr)
library(tidyr)
library(lubridate)
library(plotly)

3 Create synthetic data

We simulate daily measurements for a fictional city, Nuevo Arcade, with:

  • Seasonality (winter/summer effects)
  • Weekly rhythms (weekdays vs weekends)
  • A “festival month” spike in mobility
  • Random noise to keep things interesting
set.seed(42)

dates <- seq.Date(from = as.Date("2025-01-01"), to = as.Date("2025-12-31"), by = "day")
n <- length(dates)

df <- tibble(
  date = dates,
  doy = yday(date),
  dow = wday(date, label = TRUE, week_start = 1),
  month = month(date, label = TRUE)
) %>%
  mutate(
    # Smooth yearly seasonality: winter peaks in heating demand; summer peaks in cooling
    season = sin(2*pi*(doy/365)),
    weekday = if_else(dow %in% c("Sat","Sun"), 0, 1),

    # Mobility index: higher on weekdays, plus a festival boost in May
    mobility = 55 +
      10*weekday +
      8*season +
      12*if_else(month == "May", 1, 0) +
      rnorm(n, sd = 4),

    # Energy demand: U-shaped with both heating and cooling, plus a bit of mobility coupling
    energy_mwh = 380 +
      55*abs(season) +
      0.8*mobility +
      rnorm(n, sd = 18),

    # PM2.5: worsens in winter (inversions), improves in summer, and rises slightly with mobility
    pm25 = 22 +
      10*(season < -0.2) +   # winter-ish boost
      (-6)*season +          # summer improves
      0.12*mobility +
      rnorm(n, sd = 3)
  ) %>%
  mutate(
    mobility = pmax(mobility, 0),
    pm25 = pmax(pm25, 1)
  )

head(df)

4 Interactive story, in Plotly

4.1 1) A year at a glance

Use the legend to toggle series, hover to see exact values, and zoom into a time window.

p_year <- plot_ly(df, x = ~date) %>%
  add_lines(y = ~mobility, name = "Mobility index", hovertemplate = "Date: %{x}<br>Mobility: %{y:.1f}<extra></extra>") %>%
  add_lines(y = ~energy_mwh, name = "Energy (MWh)", yaxis = "y2",
            hovertemplate = "Date: %{x}<br>Energy: %{y:.0f} MWh<extra></extra>") %>%
  add_lines(y = ~pm25, name = "PM2.5 (µg/m³)", yaxis = "y3",
            hovertemplate = "Date: %{x}<br>PM2.5: %{y:.1f} µg/m³<extra></extra>") %>%
  layout(
    title = "Nuevo Arcadia: Mobility, Energy & Air Quality (Synthetic, Daily)",
    xaxis = list(title = "", rangeselector = list(
      buttons = list(
        list(count = 1, label = "1m", step = "month", stepmode = "backward"),
        list(count = 3, label = "3m", step = "month", stepmode = "backward"),
        list(count = 6, label = "6m", step = "month", stepmode = "backward"),
        list(step = "all", label = "All")
      )
    ), rangeslider = list(visible = TRUE)),
    yaxis = list(title = "Mobility index"),
    yaxis2 = list(title = "Energy (MWh)", overlaying = "y", side = "right", showgrid = FALSE),
    yaxis3 = list(title = "PM2.5 (µg/m³)", overlaying = "y", side = "right", position = 0.95, showgrid = FALSE),
    legend = list(orientation = "h", x = 0, y = -0.15)
  )

p_year

4.2 2) The tradeoff: activity vs air quality

Here we color points by season (month). A quick regression line hints at the relationship.

p_scatter <- plot_ly(
  df,
  x = ~mobility,
  y = ~pm25,
  color = ~month,
  colors = "Set2",
  type = "scatter",
  mode = "markers",
  marker = list(size = 7, opacity = 0.7),
  hovertemplate = paste0(
    "Date: %{customdata}<br>",
    "Mobility: %{x:.1f}<br>",
    "PM2.5: %{y:.1f} µg/m³<extra></extra>"
  ),
  customdata = ~as.character(date)
) %>%
  add_lines(
    x = ~mobility,
    y = ~fitted(lm(pm25 ~ mobility, data = df)),
    name = "Linear trend",
    inherit = FALSE
  ) %>%
  layout(
    title = "When the city moves, does the air suffer?",
    xaxis = list(title = "Mobility index"),
    yaxis = list(title = "PM2.5 (µg/m³)"),
    legend = list(orientation = "h", x = 0, y = -0.2)
  )

p_scatter

5 Reproducibility

sessionInfo()
## R version 4.4.3 (2025-02-28 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
## 
## Matrix products: default
## 
## 
## locale:
## [1] LC_COLLATE=Spanish_Mexico.utf8  LC_CTYPE=Spanish_Mexico.utf8   
## [3] LC_MONETARY=Spanish_Mexico.utf8 LC_NUMERIC=C                   
## [5] LC_TIME=Spanish_Mexico.utf8    
## 
## time zone: America/Mexico_City
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] plotly_4.10.4   ggplot2_3.5.2   lubridate_1.9.4 tidyr_1.3.1    
## [5] dplyr_1.1.4    
## 
## loaded via a namespace (and not attached):
##  [1] gtable_0.3.6       jsonlite_2.0.0     compiler_4.4.3     tidyselect_1.2.1  
##  [5] dichromat_2.0-0.1  jquerylib_0.1.4    scales_1.4.0       yaml_2.3.10       
##  [9] fastmap_1.2.0      R6_2.6.1           generics_0.1.3     knitr_1.50        
## [13] htmlwidgets_1.6.4  tibble_3.2.1       bslib_0.9.0        pillar_1.10.2     
## [17] RColorBrewer_1.1-3 rlang_1.1.6        cachem_1.1.0       xfun_0.52         
## [21] sass_0.4.10        lazyeval_0.2.2     viridisLite_0.4.2  timechange_0.3.0  
## [25] cli_3.6.5          withr_3.0.2        magrittr_2.0.3     crosstalk_1.2.1   
## [29] digest_0.6.37      grid_4.4.3         rstudioapi_0.17.1  lifecycle_1.0.4   
## [33] vctrs_0.6.5        data.table_1.17.0  evaluate_1.0.3     glue_1.8.0        
## [37] farver_2.1.2       httr_1.4.7         rmarkdown_2.29     purrr_1.0.4       
## [41] tools_4.4.3        pkgconfig_2.0.3    htmltools_0.5.8.1