Sismos por cada 100.000 km² dentro del área estudiada.
Sismos por cada 100.000 km² dentro del área estudiada.
El contraste principal se observa en la estructura vertical de la sismicidad.
---
title: "InferaData | Dashboard de Sismicidad Chile vs California"
subtitle: "USGS Earthquake Catalog · Mw ≥ 5,0 · 2000–2025"
author: "InferaData"
output:
flexdashboard::flex_dashboard:
orientation: rows
vertical_layout: scroll
theme: cosmo
source_code: embed
---
```{r setup, include=FALSE}
# ============================================================
# DASHBOARD INFERADATA - Chile vs California
# Diseñado para publicar en RPubs como HTML estático.
# No usa Shiny. La interactividad se logra con htmlwidgets,
# leaflet, crosstalk, plotly y DT.
# ============================================================
knitr::opts_chunk$set(
echo = FALSE,
warning = FALSE,
message = FALSE,
fig.align = "center"
)
paquetes <- c(
"dplyr", "tidyr", "readr", "lubridate", "stringr",
"ggplot2", "plotly", "leaflet", "leaflet.extras",
"crosstalk", "DT", "htmltools", "scales", "viridisLite",
"flexdashboard", "tibble"
)
faltan <- paquetes[!vapply(paquetes, requireNamespace, logical(1), quietly = TRUE)]
if (length(faltan) > 0) {
stop(
"Antes de tejer el dashboard instala estos paquetes: ",
paste(faltan, collapse = ", "),
"\nEjemplo: install.packages(c(",
paste(sprintf('"%s"', faltan), collapse = ", "),
"))"
)
}
invisible(lapply(paquetes, library, character.only = TRUE))
# ------------------------------
# Formatos generales
# ------------------------------
fmt_num <- function(x, digits = 0) {
format(round(x, digits), big.mark = ".", decimal.mark = ",", nsmall = digits)
}
fmt_pct <- function(x, digits = 1) {
paste0(fmt_num(x, digits), "%")
}
col_zona <- c("Chile" = "#0B6FA4", "California" = "#F28E2B")
col_depth <- c(
"0 - 20 km" = "#2DC7FF",
"20 - 70 km" = "#2CA25F",
"70 - 300 km" = "#FDB863",
"300 km o más" = "#B2182B"
)
col_mag <- c(
"5.0 - 5.9" = "#BFD7EA",
"6.0 - 6.9" = "#F6C85F",
"7.0 - 7.9" = "#F18F01",
"8.0 o más" = "#C73E1D"
)
anios <- 2000:2025
meses_es <- c("Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Sep", "Oct", "Nov", "Dic")
# ------------------------------
# Descarga reproducible USGS
# ------------------------------
descargar_usgs_anual <- function(minlat, maxlat, minlon, maxlon,
anio_inicio = 2000, anio_fin = 2025,
minmag = 5) {
base_url <- "https://earthquake.usgs.gov/fdsnws/event/1/query"
lista_datos <- list()
for (anio in anio_inicio:anio_fin) {
url <- paste0(
base_url,
"?format=csv",
"&starttime=", anio, "-01-01",
"&endtime=", anio + 1, "-01-01",
"&minlatitude=", minlat,
"&maxlatitude=", maxlat,
"&minlongitude=", minlon,
"&maxlongitude=", maxlon,
"&minmagnitude=", minmag,
"&eventtype=earthquake",
"&orderby=time-asc"
)
datos_anio <- tryCatch(
readr::read_csv(url, show_col_types = FALSE, progress = FALSE),
error = function(e) {
message("No se pudo descargar el año ", anio, ": ", e$message)
NULL
}
)
if (!is.null(datos_anio) && nrow(datos_anio) > 0) {
lista_datos[[as.character(anio)]] <- datos_anio
}
}
dplyr::bind_rows(lista_datos)
}
leer_o_descargar <- function(nombre_csv, zona) {
if (file.exists(nombre_csv)) {
return(readr::read_csv(nombre_csv, show_col_types = FALSE, progress = FALSE))
}
if (zona == "Chile") {
descargar_usgs_anual(
minlat = -46, maxlat = -17,
minlon = -76, maxlon = -66,
anio_inicio = 2000, anio_fin = 2025,
minmag = 5
)
} else {
descargar_usgs_anual(
minlat = 32, maxlat = 42,
minlon = -125, maxlon = -114,
anio_inicio = 2000, anio_fin = 2025,
minmag = 5
)
}
}
# Si existen los CSV generados por el script original, se leen.
# Si no existen, se descargan desde USGS al momento de tejer el Rmd.
chile_raw <- leer_o_descargar("chile_limpia.csv", "Chile")
california_raw <- leer_o_descargar("california_limpia.csv", "California")
# ------------------------------
# Preprocesamiento
# ------------------------------
variables_clave <- c("id", "time", "latitude", "longitude", "place", "depth", "mag", "magType", "type")
chile <- chile_raw %>%
dplyr::filter(if ("type" %in% names(.)) type == "earthquake" else TRUE) %>%
dplyr::filter(stats::complete.cases(dplyr::across(dplyr::any_of(variables_clave)))) %>%
dplyr::distinct() %>%
dplyr::mutate(
time = lubridate::ymd_hms(time, tz = "UTC", quiet = TRUE),
year = lubridate::year(time),
month = lubridate::month(time),
zona = "Chile",
Mw_hom = dplyr::case_when(
tolower(magType) %in% c("mw", "mwb", "mwc", "mwr", "mww") ~ mag,
tolower(magType) == "ml" & depth <= 50 ~ 0.80 * mag + 1.15,
tolower(magType) == "ml" & depth > 50 ~ 0.94 * mag + 0.30,
tolower(magType) == "ms" ~ 0.74 * mag + 1.60,
tolower(magType) == "mb" ~ 1.04 * mag - 0.02,
tolower(magType) %in% c("m", "md", "mc") ~ mag,
TRUE ~ NA_real_
),
magnitud = Mw_hom
) %>%
dplyr::filter(!is.na(magnitud), year >= 2000, year <= 2025)
california <- california_raw %>%
dplyr::filter(if ("type" %in% names(.)) type == "earthquake" else TRUE) %>%
dplyr::filter(stats::complete.cases(dplyr::across(dplyr::any_of(variables_clave)))) %>%
dplyr::distinct() %>%
dplyr::filter(tolower(magType) %in% c("mw", "mwr")) %>%
dplyr::mutate(
time = lubridate::ymd_hms(time, tz = "UTC", quiet = TRUE),
year = lubridate::year(time),
month = lubridate::month(time),
zona = "California",
magnitud = mag
) %>%
dplyr::filter(!is.na(magnitud), year >= 2000, year <= 2025)
clasificar_magnitud <- function(x) {
dplyr::case_when(
x >= 5.0 & x < 6.0 ~ "5.0 - 5.9",
x >= 6.0 & x < 7.0 ~ "6.0 - 6.9",
x >= 7.0 & x < 8.0 ~ "7.0 - 7.9",
x >= 8.0 ~ "8.0 o más",
TRUE ~ NA_character_
)
}
clasificar_profundidad <- function(x) {
# Se respeta la regla usada en el informe: los valores negativos de profundidad
# se conservan en la base, pero no se asignan a las categorías 0-20, 20-70, 70-300.
dplyr::case_when(
x >= 0 & x < 20 ~ "0 - 20 km",
x >= 20 & x < 70 ~ "20 - 70 km",
x >= 70 & x < 300 ~ "70 - 300 km",
x >= 300 ~ "300 km o más",
TRUE ~ NA_character_
)
}
sismos <- dplyr::bind_rows(chile, california) %>%
dplyr::mutate(
depth_plot = pmax(depth, 0),
rango_magnitud = factor(clasificar_magnitud(magnitud), levels = names(col_mag)),
categoria_magnitud = factor(
dplyr::case_when(
magnitud >= 5 & magnitud < 6 ~ "Moderada",
magnitud >= 6 & magnitud < 7 ~ "Fuerte",
magnitud >= 7 & magnitud < 8 ~ "Mayor",
magnitud >= 8 ~ "Gran Terremoto",
TRUE ~ NA_character_
),
levels = c("Moderada", "Fuerte", "Mayor", "Gran Terremoto")
),
rango_profundidad = factor(clasificar_profundidad(depth), levels = names(col_depth)),
evento_fuerte = ifelse(magnitud >= 6, "Mw ≥ 6,0", "Mw 5,0–5,9"),
etiqueta_evento = ifelse(magnitud >= 7, "Evento mayor", ifelse(magnitud >= 6, "Evento fuerte", "Evento moderado")),
mes_nombre = factor(meses_es[month], levels = meses_es),
id_sismo = paste0(zona, "_", dplyr::row_number()),
fecha = as.Date(time),
popup = paste0(
"<b>", zona, "</b><br>",
"<b>Lugar:</b> ", place, "<br>",
"<b>Fecha UTC:</b> ", format(time, "%Y-%m-%d %H:%M"), "<br>",
"<b>Magnitud Mw:</b> ", round(magnitud, 2), "<br>",
"<b>Profundidad:</b> ", round(depth, 2), " km<br>",
"<b>Tipo magnitud:</b> ", magType
)
)
# Áreas usadas en el informe. Se dejan fijas para mantener consistencia entre el informe y el dashboard.
areas_zonas <- tibble::tibble(
zona = c("Chile", "California"),
area_km2 = c(3022191, 1085219)
)
kpi_zona <- sismos %>%
dplyr::group_by(zona) %>%
dplyr::summarise(
total = dplyr::n(),
mag_media = mean(magnitud, na.rm = TRUE),
mag_mediana = median(magnitud, na.rm = TRUE),
mag_max = max(magnitud, na.rm = TRUE),
prof_media = mean(depth, na.rm = TRUE),
prof_mediana = median(depth, na.rm = TRUE),
prof_max = max(depth, na.rm = TRUE),
eventos_mw6 = sum(magnitud >= 6, na.rm = TRUE),
eventos_mw7 = sum(magnitud >= 7, na.rm = TRUE),
.groups = "drop"
) %>%
dplyr::left_join(areas_zonas, by = "zona") %>%
dplyr::mutate(
sismos_100k = total / area_km2 * 100000,
pct_mw6 = eventos_mw6 / total * 100
)
get_kpi <- function(zona_nombre, var_name) {
# Función deliberadamente simple para evitar problemas de tidy-eval al publicar en RPubs.
# Devuelve siempre un único valor, porque valueBox/htmltools falla si recibe vectores.
salida <- kpi_zona[kpi_zona$zona == zona_nombre, var_name, drop = TRUE]
if (length(salida) == 0) return(NA_real_)
salida[[1]]
}
valor_kpi <- function(zona_nombre, var_name, digits = 0, prefijo = "", sufijo = "") {
paste0(prefijo, fmt_num(get_kpi(zona_nombre, var_name), digits), sufijo)
}
# Tabla 5 del informe: resumen estadístico comparativo de magnitud y profundidad
resumen_estadistico <- sismos %>%
dplyr::select(zona, Magnitud = magnitud, Profundidad = depth) %>%
tidyr::pivot_longer(cols = c(Magnitud, Profundidad), names_to = "Variable", values_to = "valor") %>%
dplyr::group_by(zona, Variable) %>%
dplyr::summarise(
Media = mean(valor, na.rm = TRUE),
`Cuartil 1` = stats::quantile(valor, 0.25, na.rm = TRUE, type = 7),
Mediana = stats::median(valor, na.rm = TRUE),
`Cuartil 3` = stats::quantile(valor, 0.75, na.rm = TRUE, type = 7),
`Percentil 90` = stats::quantile(valor, 0.90, na.rm = TRUE, type = 7),
`Percentil 95` = stats::quantile(valor, 0.95, na.rm = TRUE, type = 7),
`Desv. Est.` = stats::sd(valor, na.rm = TRUE),
`Mín.` = min(valor, na.rm = TRUE),
`Máx.` = max(valor, na.rm = TRUE),
.groups = "drop"
) %>%
dplyr::arrange(factor(zona, levels = c("Chile", "California")), Variable)
# Figura 8 del informe: sismos por mes, según categoría de magnitud
conteo_mensual_mag <- sismos %>%
dplyr::count(zona, year, month, categoria_magnitud, name = "n") %>%
tidyr::complete(
zona,
year = anios,
month = 1:12,
categoria_magnitud = factor(c("Moderada", "Fuerte", "Mayor", "Gran Terremoto"),
levels = c("Moderada", "Fuerte", "Mayor", "Gran Terremoto")),
fill = list(n = 0)
) %>%
dplyr::mutate(
fecha_mes = as.Date(sprintf("%04d-%02d-01", year, month)),
mes_nombre = factor(meses_es[month], levels = meses_es)
)
# Tablas normalizadas
niveles_magnitud <- names(col_mag)
niveles_profundidad <- names(col_depth)
combinaciones_magnitud <- tidyr::expand_grid(
zona = c("Chile", "California"),
rango_magnitud = factor(niveles_magnitud, levels = niveles_magnitud)
)
tabla_magnitud <- sismos %>%
dplyr::count(zona, rango_magnitud, name = "n_sismos") %>%
dplyr::right_join(combinaciones_magnitud, by = c("zona", "rango_magnitud")) %>%
dplyr::mutate(n_sismos = tidyr::replace_na(n_sismos, 0L)) %>%
dplyr::group_by(zona) %>%
dplyr::mutate(
total_zona = sum(n_sismos),
porcentaje = 100 * n_sismos / total_zona
) %>%
dplyr::ungroup() %>%
dplyr::left_join(areas_zonas, by = "zona") %>%
dplyr::mutate(sismos_100k = n_sismos / area_km2 * 100000)
cortes <- tibble::tibble(corte = c("Mw ≥ 5,0", "Mw ≥ 6,0"), min_mag = c(5, 6))
base_cortes <- sismos %>%
tidyr::crossing(cortes) %>%
dplyr::filter(magnitud >= min_mag)
combinaciones_prof <- tidyr::expand_grid(
zona = c("Chile", "California"),
corte = c("Mw ≥ 5,0", "Mw ≥ 6,0"),
rango_profundidad = factor(niveles_profundidad, levels = niveles_profundidad)
)
tabla_profundidad <- base_cortes %>%
dplyr::count(zona, corte, rango_profundidad, name = "n_sismos") %>%
dplyr::right_join(combinaciones_prof, by = c("zona", "corte", "rango_profundidad")) %>%
dplyr::mutate(n_sismos = tidyr::replace_na(n_sismos, 0L)) %>%
dplyr::group_by(zona, corte) %>%
dplyr::mutate(
total_zona_corte = sum(n_sismos),
porcentaje = ifelse(total_zona_corte > 0, 100 * n_sismos / total_zona_corte, 0)
) %>%
dplyr::ungroup() %>%
dplyr::left_join(areas_zonas, by = "zona") %>%
dplyr::mutate(sismos_100k = n_sismos / area_km2 * 100000)
conteo_anual <- sismos %>%
tidyr::crossing(cortes) %>%
dplyr::filter(magnitud >= min_mag) %>%
dplyr::count(zona, corte, year, name = "n") %>%
tidyr::complete(zona, corte, year = anios, fill = list(n = 0))
conteo_mensual <- sismos %>%
dplyr::count(zona, year, month, mes_nombre, name = "n") %>%
tidyr::complete(
zona,
year = anios,
month = 1:12,
fill = list(n = 0)
) %>%
dplyr::mutate(mes_nombre = factor(meses_es[month], levels = meses_es))
# Objetos Crosstalk para mapas
sismos_mapa <- sismos %>%
dplyr::filter(!is.na(latitude), !is.na(longitude), !is.na(depth), !is.na(magnitud)) %>%
dplyr::mutate(
radio = pmax((magnitud - 4.6) * 4, 3),
profundidad_mapa = depth_plot
)
sismos_sd <- crosstalk::SharedData$new(sismos_mapa, key = ~id_sismo, group = "sismos_mapa")
chile_sd <- crosstalk::SharedData$new(dplyr::filter(sismos_mapa, zona == "Chile"), key = ~id_sismo, group = "sismos_chile")
california_sd <- crosstalk::SharedData$new(dplyr::filter(sismos_mapa, zona == "California"), key = ~id_sismo, group = "sismos_california")
pal_depth <- leaflet::colorNumeric(viridisLite::viridis(256), domain = sismos_mapa$depth_plot)
pal_zona <- leaflet::colorFactor(col_zona, domain = sismos_mapa$zona)
# Leyendas manuales
leyenda_heatmap <- htmltools::HTML("<div style='background:white; padding:8px; border-radius:6px; box-shadow:0 0 8px rgba(0,0,0,.25); font-size:12px;'><b>Mapa de calor</b><br><div style='width:170px;height:14px;margin:6px 0 4px 0;background:linear-gradient(to right, blue, cyan, green, yellow, red);border:1px solid #999;'></div><div style='display:flex;justify-content:space-between;width:170px;'><span>Baja</span><span>Alta</span></div><span>Concentración / intensidad</span></div>")
leyenda_punto <- htmltools::HTML("<div style='background:white; padding:8px; border-radius:6px; box-shadow:0 0 8px rgba(0,0,0,.25); font-size:12px;'><b>Tamaño del punto</b><br><span style='display:inline-block;width:8px;height:8px;border-radius:50%;background:#777;'></span> Menor magnitud<br><span style='display:inline-block;width:15px;height:15px;border-radius:50%;background:#777;'></span> Magnitud media<br><span style='display:inline-block;width:23px;height:23px;border-radius:50%;background:#777;'></span> Mayor magnitud</div>")
# Referencias tectónicas aproximadas para el mapa.
# Coordenadas en orden longitud-latitud. Se dibujan como referencia visual, no como cartografía oficial.
linea_chile <- tibble::tibble(
lon = c(-72.2, -72.6, -73.0, -73.5, -74.0, -74.6, -75.2, -75.7, -76.0),
lat = c(-18.0, -22.0, -26.0, -30.0, -34.0, -38.0, -42.0, -44.5, -46.0),
nombre = "Fosa Perú-Chile / margen de subducción",
tipo = "Referencia tectónica"
)
linea_san_andres <- tibble::tibble(
lon = c(-115.5, -116.0, -116.5, -117.2, -118.0, -118.8, -119.6, -120.4, -121.1, -121.8, -122.4, -123.1, -123.7, -124.0),
lat = c( 32.6, 33.0, 33.5, 34.1, 34.6, 35.2, 35.7, 36.3, 36.9, 37.4, 38.0, 38.8, 39.6, 40.2),
nombre = "Falla de San Andrés / límite transformante",
tipo = "Referencia tectónica"
)
agregar_lineas_tectonicas <- function(mapa) {
mapa %>%
leaflet::addPolylines(
data = linea_chile, lng = ~lon, lat = ~lat,
color = "#D7191C", weight = 4, opacity = 0.9, dashArray = "8,6",
label = ~nombre, group = "Referencias tectónicas"
) %>%
leaflet::addPolylines(
data = linea_san_andres, lng = ~lon, lat = ~lat,
color = "#6A00A8", weight = 4, opacity = 0.9, dashArray = "8,6",
label = ~nombre, group = "Referencias tectónicas"
)
}
leyenda_tectonica <- htmltools::HTML("<div style='background:white; padding:8px; border-radius:6px; box-shadow:0 0 8px rgba(0,0,0,.25); font-size:12px;'><b>Referencias tectónicas</b><br><span style='display:inline-block;width:24px;border-top:4px dashed #D7191C;margin-right:6px;'></span> Fosa Perú-Chile<br><span style='display:inline-block;width:24px;border-top:4px dashed #6A00A8;margin-right:6px;'></span> Falla de San Andrés<br><span style='color:#607D8B;'>Trazos aproximados para orientación visual.</span></div>")
# ------------------------------
# Funciones de gráficos
# ------------------------------
tema_dash <- function(base_size = 12) {
ggplot2::theme_minimal(base_size = base_size) +
ggplot2::theme(
plot.title = ggplot2::element_text(face = "bold", hjust = 0, color = "#263238"),
plot.subtitle = ggplot2::element_text(color = "#546E7A"),
legend.position = "bottom",
panel.grid.minor = ggplot2::element_blank()
)
}
plotly_clean <- function(p, tooltip = "text", height = 640) {
# En flexdashboard + RPubs, los gráficos se ven mejor con altura fija.
# responsive=TRUE mantiene el ajuste horizontal, pero evita que queden achatados.
plotly::ggplotly(p, tooltip = tooltip, height = height) %>%
plotly::layout(
height = height,
autosize = TRUE,
hoverlabel = list(bgcolor = "white"),
margin = list(l = 55, r = 30, b = 90, t = 90)
) %>%
plotly::config(responsive = TRUE, displaylogo = FALSE)
}
crear_mapa <- function(shared, data_heat, bounds, titulo_filtros = "Filtros", prefix = "mapa", map_height = 900) {
# Estructura robusta para RPubs: filtros arriba y mapa en tarjeta grande.
filtros <- htmltools::tags$div(
class = "map-filter-grid",
htmltools::tags$div(class = "map-filter-title", htmltools::tags$h4(titulo_filtros)),
crosstalk::filter_select(paste0(prefix, "_zona"), "Zona", shared, ~zona, multiple = TRUE, allLevels = TRUE),
crosstalk::filter_select(paste0(prefix, "_rprof"), "Profundidad", shared, ~rango_profundidad, multiple = TRUE, allLevels = TRUE),
crosstalk::filter_select(paste0(prefix, "_rmag"), "Rango de magnitud", shared, ~rango_magnitud, multiple = TRUE, allLevels = TRUE),
crosstalk::filter_slider(paste0(prefix, "_anio"), "Año", shared, ~year, step = 1),
crosstalk::filter_slider(paste0(prefix, "_mag"), "Magnitud Mw", shared, ~magnitud, step = 0.1),
crosstalk::filter_slider(paste0(prefix, "_dep"), "Profundidad (km)", shared, ~depth_plot, step = 1),
htmltools::tags$div(
class = "map-filter-note",
"Los puntos responden a los filtros. El mapa de calor y las líneas tectónicas son capas de referencia para explorar los mapas de epicentros del informe."
)
)
mapa <- leaflet::leaflet(shared, width = "100%", height = map_height) %>%
leaflet::addTiles(group = "OpenStreetMap") %>%
leaflet::addProviderTiles(leaflet::providers$CartoDB.Positron, group = "Mapa claro") %>%
leaflet::addProviderTiles(leaflet::providers$Esri.WorldTopoMap, group = "Topográfico") %>%
leaflet.extras::addHeatmap(
data = data_heat,
lng = ~longitude, lat = ~latitude,
intensity = ~magnitud,
radius = 20, blur = 15,
max = max(data_heat$magnitud, na.rm = TRUE),
minOpacity = 0.45,
gradient = c("0.10" = "blue", "0.30" = "cyan", "0.55" = "green", "0.75" = "yellow", "1.00" = "red"),
group = "Mapa de calor"
) %>%
agregar_lineas_tectonicas() %>%
leaflet::addCircleMarkers(
lng = ~longitude, lat = ~latitude,
radius = ~radio,
fillColor = ~pal_depth(depth_plot),
fillOpacity = .82,
color = "#111111",
weight = .7,
opacity = .98,
popup = ~popup,
label = ~paste0(zona, " · Mw ", round(magnitud, 2), " · ", round(depth, 1), " km"),
group = "Epicentros"
) %>%
leaflet::addLegend("bottomright", pal = pal_depth, values = data_heat$depth_plot,
title = "Profundidad (km)", opacity = 1) %>%
leaflet::addControl(leyenda_punto, position = "bottomleft") %>%
leaflet::addControl(leyenda_heatmap, position = "bottomleft") %>%
leaflet::addControl(leyenda_tectonica, position = "topright") %>%
leaflet::addLayersControl(
position = "topright",
baseGroups = c("OpenStreetMap", "Mapa claro", "Topográfico"),
overlayGroups = c("Epicentros", "Mapa de calor", "Referencias tectónicas"),
options = leaflet::layersControlOptions(collapsed = TRUE)
) %>%
leaflet::fitBounds(lng1 = bounds[1], lat1 = bounds[2], lng2 = bounds[3], lat2 = bounds[4])
htmltools::tagList(
filtros,
htmltools::tags$div(class = "map-card", mapa)
)
}
```
<style>
.navbar { background: linear-gradient(90deg, #0B1F33, #114B5F) !important; }
.navbar-brand { font-weight: 800 !important; letter-spacing: .2px; }
.value-box { border-radius: 16px !important; box-shadow: 0 8px 22px rgba(0,0,0,.10); }
.chart-title { font-weight: 800; }
.panel-filtros { background: #F7FAFC; border: 1px solid #DDE7EE; border-radius: 16px; padding: 16px; box-shadow: 0 8px 18px rgba(0,0,0,.06); }
.panel-filtros h4 { margin-top: 0; color: #0B1F33; font-weight: 800; }
.insight { background: linear-gradient(135deg, #F8FBFF, #EEF7FA); border-left: 6px solid #0B6FA4; border-radius: 14px; padding: 16px 18px; margin: 8px 0 14px 0; font-size: 15px; box-shadow: 0 6px 16px rgba(0,0,0,.06); }
.insight strong { color: #0B1F33; }
.small-note { color:#607D8B; font-size:12px; }
.exec-grid { display: grid; grid-template-columns: repeat(3, minmax(230px, 1fr)); gap: 16px; margin-bottom: 18px; }
.exec-card { background: white; border: 1px solid #DDE7EE; border-radius: 18px; padding: 18px; box-shadow: 0 8px 20px rgba(0,0,0,.07); }
.exec-card h3 { margin: 0 0 8px 0; color: #0B1F33; font-size: 18px; font-weight: 800; }
.exec-card p { margin: 0; color: #455A64; font-size: 14px; line-height: 1.45; }
.exec-card .big { font-size: 28px; font-weight: 900; color: #0B6FA4; margin-bottom: 6px; }
@media (max-width: 900px) { .exec-grid { grid-template-columns: 1fr; } }
.dt-center { text-align: center; }
.map-filter-grid {
display: grid;
grid-template-columns: repeat(3, minmax(230px, 1fr));
gap: 12px 16px;
background: #F7FAFC;
border: 1px solid #DDE7EE;
border-radius: 18px;
padding: 16px;
margin-bottom: 16px;
box-shadow: 0 8px 18px rgba(0,0,0,.06);
}
.map-filter-title { grid-column: 1 / -1; }
.map-filter-title h4 { margin: 0; color: #0B1F33; font-weight: 800; }
.map-filter-note {
grid-column: 1 / -1;
color: #546E7A;
font-size: 13px;
border-top: 1px solid #DDE7EE;
padding-top: 10px;
}
.map-card {
min-height: 920px;
background: white;
border: 1px solid #DDE7EE;
border-radius: 18px;
padding: 10px;
box-shadow: 0 10px 24px rgba(0,0,0,.08);
overflow: hidden;
}
.map-card .leaflet, .map-card .leaflet-container, .map-card .html-widget {
min-height: 900px !important;
height: 900px !important;
width: 100% !important;
}
.plotly, .js-plotly-plot, .plot-container, .svg-container {
min-height: 600px !important;
}
.chart-wrapper, .chart-stage {
overflow: visible !important;
}
@media (max-width: 900px) {
.map-filter-grid { grid-template-columns: 1fr; }
.map-card { min-height: 760px; }
.map-card .leaflet, .map-card .leaflet-container, .map-card .html-widget { min-height: 740px !important; height: 740px !important; }
}
</style>
<script>
// Leaflet dentro de flexdashboard/tabsets a veces queda en blanco hasta que recibe resize.
// Este pequeño ajuste fuerza el redibujado al cargar y al cambiar de pestaña.
document.addEventListener("DOMContentLoaded", function() {
function resizeWidgets() {
window.dispatchEvent(new Event("resize"));
}
setTimeout(resizeWidgets, 400);
setTimeout(resizeWidgets, 1200);
if (window.jQuery) {
jQuery(document).on("shown.bs.tab shown.bs.collapse", function() {
setTimeout(resizeWidgets, 250);
setTimeout(resizeWidgets, 800);
});
}
});
</script>
Resumen ejecutivo
=======================================================================
Row {data-height=170}
-----------------------------------------------------------------------
### Total Chile
```{r}
flexdashboard::valueBox(
value = valor_kpi("Chile", "total"),
caption = "eventos Mw ≥ 5,0 en Chile",
icon = "fa-globe",
color = "primary"
)
```
### Total California
```{r}
flexdashboard::valueBox(
value = valor_kpi("California", "total"),
caption = "eventos Mw ≥ 5,0 en California",
icon = "fa-map-marker",
color = "warning"
)
```
### Máxima magnitud Chile
```{r}
flexdashboard::valueBox(
value = paste0("Mw ", valor_kpi("Chile", "mag_max", digits = 1)),
caption = "máximo observado en Chile",
icon = "fa-bolt",
color = "danger"
)
```
### Máxima magnitud California
```{r}
flexdashboard::valueBox(
value = paste0("Mw ", valor_kpi("California", "mag_max", digits = 1)),
caption = "máximo observado en California",
icon = "fa-bolt",
color = "warning"
)
```
Row {data-height=520}
-----------------------------------------------------------------------
### Lectura ejecutiva
```{r}
htmltools::HTML(paste0(
"<div class='exec-grid'>",
"<div class='exec-card'><div class='big'>", fmt_num(get_kpi("Chile", "sismos_100k"), 2), "</div><h3>Densidad Chile</h3><p>Sismos por cada 100.000 km² dentro del área estudiada.</p></div>",
"<div class='exec-card'><div class='big'>", fmt_num(get_kpi("California", "sismos_100k"), 2), "</div><h3>Densidad California</h3><p>Sismos por cada 100.000 km² dentro del área estudiada.</p></div>",
"<div class='exec-card'><div class='big'>", fmt_num(get_kpi("Chile", "prof_max"), 1), " km</div><h3>Profundidad máxima Chile</h3><p>El contraste principal se observa en la estructura vertical de la sismicidad.</p></div>",
"</div>",
"<div class='insight'><strong>Hallazgo principal:</strong> las magnitudes centrales son parecidas entre zonas, pero Chile presenta mayor frecuencia, mayor densidad espacial, eventos extremos más grandes y una distribución de profundidades mucho más amplia. California concentra su sismicidad en rangos superficiales.</div>",
"<div class='insight'><strong>Alcance:</strong> este dashboard reproduce y organiza visualmente los resultados del informe inicial: mapas de epicentros, distribuciones de magnitud y profundidad, frecuencia temporal mensual, conteos normalizados, sensibilidad por umbral y matrices de correlación.</div>"
))
```
### Tabla 5 — Resumen estadístico comparativo
```{r}
resumen_tabla5_dt <- resumen_estadistico %>%
dplyr::mutate(dplyr::across(where(is.numeric), ~round(.x, 3))) %>%
dplyr::rename(Zona = zona)
DT::datatable(
resumen_tabla5_dt,
rownames = FALSE,
options = list(dom = "t", pageLength = 10, scrollX = TRUE),
class = "stripe hover compact"
) %>%
DT::formatStyle("Zona", fontWeight = "bold")
```
Mapa sísmico
=======================================================================
Column {.tabset}
-----------------------------------------------------------------------
### Vista combinada
```{r}
crear_mapa(
shared = sismos_sd,
data_heat = sismos_mapa,
bounds = c(-126, -47, -65, 43),
titulo_filtros = "Filtros generales",
prefix = "general"
)
```
### Chile
```{r}
crear_mapa(
shared = chile_sd,
data_heat = dplyr::filter(sismos_mapa, zona == "Chile"),
bounds = c(-76, -46, -66, -17),
titulo_filtros = "Filtros Chile",
prefix = "chile"
)
```
### California
```{r}
crear_mapa(
shared = california_sd,
data_heat = dplyr::filter(sismos_mapa, zona == "California"),
bounds = c(-125, 32, -114, 42),
titulo_filtros = "Filtros California",
prefix = "california"
)
```
Magnitud
=======================================================================
Row {data-height=700}
-----------------------------------------------------------------------
### Figura 10 — Distribución porcentual normalizada
```{r}
p_mag_pct <- tabla_magnitud %>%
ggplot2::ggplot(ggplot2::aes(
x = rango_magnitud, y = porcentaje, fill = zona,
text = paste0(
"Zona: ", zona,
"<br>Rango: ", rango_magnitud,
"<br>N° sismos: ", fmt_num(n_sismos),
"<br>Porcentaje: ", fmt_pct(porcentaje, 2)
)
)) +
ggplot2::geom_col(position = ggplot2::position_dodge(width = .8), width = .72) +
ggplot2::scale_fill_manual(values = col_zona) +
ggplot2::labs(
title = "Distribución porcentual normalizada por tamaño muestral",
subtitle = "Figura 10 del informe",
x = "Rango de magnitud",
y = "Porcentaje dentro de cada zona"
) +
tema_dash()
plotly_clean(p_mag_pct)
```
### Figura 11 — Sismos por 100.000 km² según magnitud
```{r}
p_mag_den <- tabla_magnitud %>%
ggplot2::ggplot(ggplot2::aes(
x = rango_magnitud, y = sismos_100k, fill = zona,
text = paste0(
"Zona: ", zona,
"<br>Rango: ", rango_magnitud,
"<br>N° sismos: ", fmt_num(n_sismos),
"<br>Sismos/100.000 km²: ", fmt_num(sismos_100k, 3)
)
)) +
ggplot2::geom_col(position = ggplot2::position_dodge(width = .8), width = .72) +
ggplot2::scale_fill_manual(values = col_zona) +
ggplot2::labs(
title = "Sismos por cada 100.000 km² según categoría de magnitud",
subtitle = "Figura 11 del informe",
x = "Rango de magnitud",
y = "Sismos / 100.000 km²"
) +
tema_dash()
plotly_clean(p_mag_den)
```
Row {data-height=600}
-----------------------------------------------------------------------
### Tabla 8 — Resumen de conteos normalizados por magnitud
```{r}
tabla_magnitud_dt <- tabla_magnitud %>%
dplyr::transmute(
Zona = zona,
Rango = as.character(rango_magnitud),
`N° sismos` = n_sismos,
`Total zona` = total_zona,
Proporción = round(n_sismos / total_zona, 5),
Porcentaje = paste0(round(porcentaje, 3), "%"),
`Área km²` = round(area_km2, 0),
`Sismos/100.000 km²` = round(sismos_100k, 4)
)
DT::datatable(
tabla_magnitud_dt,
rownames = FALSE,
options = list(dom = "t", pageLength = 10, scrollX = TRUE),
class = "stripe hover compact"
)
```
Profundidad y sensibilidad
=======================================================================
Row {data-height=700}
-----------------------------------------------------------------------
### Figura 12 — Sensibilidad porcentual por profundidad
```{r}
p_prof_pct <- tabla_profundidad %>%
dplyr::filter(rango_profundidad != "300 km o más") %>%
ggplot2::ggplot(ggplot2::aes(
x = rango_profundidad, y = porcentaje, fill = corte,
text = paste0(
"Zona: ", zona,
"<br>Corte: ", corte,
"<br>Profundidad: ", rango_profundidad,
"<br>N° sismos: ", fmt_num(n_sismos),
"<br>Porcentaje: ", fmt_pct(porcentaje, 2)
)
)) +
ggplot2::geom_col(position = ggplot2::position_dodge(width = .8), width = .72) +
ggplot2::facet_wrap(~zona) +
ggplot2::scale_fill_manual(values = c("Mw ≥ 5,0" = "#8ECAE6", "Mw ≥ 6,0" = "#FFB703")) +
ggplot2::labs(
title = "Distribución porcentual de sismos por profundidad según umbral",
subtitle = "Figura 12 del informe",
x = "Categoría de profundidad",
y = "Porcentaje"
) +
tema_dash()
plotly_clean(p_prof_pct)
```
### Figura 13 — Densidad espacial por profundidad
```{r}
p_prof_den <- tabla_profundidad %>%
dplyr::filter(rango_profundidad != "300 km o más") %>%
ggplot2::ggplot(ggplot2::aes(
x = rango_profundidad, y = sismos_100k, fill = corte,
text = paste0(
"Zona: ", zona,
"<br>Corte: ", corte,
"<br>Profundidad: ", rango_profundidad,
"<br>N° sismos: ", fmt_num(n_sismos),
"<br>Sismos/100.000 km²: ", fmt_num(sismos_100k, 3)
)
)) +
ggplot2::geom_col(position = ggplot2::position_dodge(width = .8), width = .72) +
ggplot2::facet_wrap(~zona, scales = "free_y") +
ggplot2::scale_fill_manual(values = c("Mw ≥ 5,0" = "#8ECAE6", "Mw ≥ 6,0" = "#FFB703")) +
ggplot2::labs(
title = "Densidad espacial de sismos por profundidad según corte de magnitud",
subtitle = "Figura 13 del informe",
x = "Categoría de profundidad",
y = "Sismos / 100.000 km²"
) +
tema_dash()
plotly_clean(p_prof_den)
```
Row {data-height=600}
-----------------------------------------------------------------------
### Tabla 10 — Profundidad, umbral y superficie
```{r}
tabla_profundidad_dt <- tabla_profundidad %>%
dplyr::transmute(
Zona = zona,
Corte = corte,
Profundidad = as.character(rango_profundidad),
`N°` = n_sismos,
Total = total_zona_corte,
`%` = round(porcentaje, 2),
`Sismos/100.000 km²` = round(sismos_100k, 4)
)
DT::datatable(
tabla_profundidad_dt,
rownames = FALSE,
options = list(dom = "t", pageLength = 20, scrollX = TRUE),
class = "stripe hover compact"
)
```
Frecuencia temporal
=======================================================================
Row {data-height=820}
-----------------------------------------------------------------------
### Figura 8 — Sismos por mes, según categoría de magnitud
```{r}
# Versión robusta para RPubs: se muestran solo meses con ocurrencia observada.
# Así se evitan paneles visualmente vacíos por la gran cantidad de meses con cero eventos,
# especialmente en California y en categorías de alta magnitud.
conteo_mensual_mag_plot <- conteo_mensual_mag %>%
dplyr::filter(n > 0) %>%
dplyr::mutate(
categoria_magnitud = factor(
as.character(categoria_magnitud),
levels = c("Moderada", "Fuerte", "Mayor", "Gran Terremoto")
)
)
p_mensual_mag <- conteo_mensual_mag_plot %>%
ggplot2::ggplot(ggplot2::aes(
x = fecha_mes, y = n, fill = zona,
text = paste0(
"Zona: ", zona,
"<br>Mes: ", format(fecha_mes, "%Y-%m"),
"<br>Categoría: ", categoria_magnitud,
"<br>N° sismos: ", fmt_num(n)
)
)) +
ggplot2::geom_col(width = 25, alpha = .85) +
ggplot2::facet_grid(zona ~ categoria_magnitud, scales = "free_y") +
ggplot2::scale_fill_manual(values = col_zona) +
ggplot2::scale_x_date(date_breaks = "5 years", date_labels = "%Y") +
ggplot2::labs(
title = "Sismos por mes, según categoría de magnitud",
subtitle = "Figura 8 del informe · se muestran los meses con al menos un evento registrado",
x = "Año",
y = "N° de sismos mensuales"
) +
tema_dash(base_size = 11) +
ggplot2::theme(
legend.position = "none",
axis.text.x = ggplot2::element_text(angle = 35, hjust = 1),
strip.text = ggplot2::element_text(face = "bold")
)
plotly_clean(p_mensual_mag, height = 820)
```
Asociación
=======================================================================
Column {.tabset}
-----------------------------------------------------------------------
### Figura 14 — Correlaciones California
```{r}
cor_long <- function(df, zona_nombre) {
vars <- df %>%
dplyr::filter(.data$zona == .env$zona_nombre) %>%
dplyr::select(Magnitud = magnitud, Profundidad = depth_plot, Latitud = latitude, Longitud = longitude)
metodos <- c("Pearson" = "pearson", "Spearman" = "spearman", "Kendall" = "kendall")
dplyr::bind_rows(lapply(names(metodos), function(nm) {
mat <- stats::cor(vars, use = "complete.obs", method = metodos[[nm]])
as.data.frame(as.table(mat)) %>%
dplyr::rename(`Variable 1` = Var1, `Variable 2` = Var2, r = Freq) %>%
dplyr::mutate(Método = nm)
}))
}
plot_cor_zona <- function(zona_nombre) {
df_cor <- cor_long(sismos, zona_nombre)
p <- df_cor %>%
ggplot2::ggplot(ggplot2::aes(
x = `Variable 1`, y = `Variable 2`, fill = r,
text = paste0("Método: ", Método, "<br>r: ", round(r, 3))
)) +
ggplot2::geom_tile(color = "white", linewidth = 1) +
ggplot2::geom_text(ggplot2::aes(label = round(r, 2)), fontface = "bold", size = 3.2) +
ggplot2::facet_wrap(~Método, nrow = 1) +
ggplot2::scale_fill_gradient2(low = "#B2182B", mid = "white", high = "#2166AC", midpoint = 0, limits = c(-1, 1)) +
ggplot2::labs(
title = paste0("Matrices de correlación — ", zona_nombre),
subtitle = "Pearson, Spearman y Kendall, como en el informe",
x = NULL,
y = NULL,
fill = "r"
) +
tema_dash(base_size = 11) +
ggplot2::theme(axis.text.x = ggplot2::element_text(angle = 35, hjust = 1))
plotly_clean(p, height = 680)
}
plot_cor_zona("California")
```
### Figura 15 — Correlaciones Chile
```{r}
plot_cor_zona("Chile")
```
Tablas del informe
=======================================================================
Column {.tabset}
-----------------------------------------------------------------------
### Tabla 5 — Resumen estadístico
```{r}
DT::datatable(
resumen_tabla5_dt,
rownames = FALSE,
extensions = c("Buttons"),
options = list(dom = "Bfrtip", buttons = c("copy", "csv", "excel"), pageLength = 10, scrollX = TRUE),
class = "stripe hover compact"
)
```
### Tabla 8 — Magnitud normalizada
```{r}
DT::datatable(
tabla_magnitud_dt,
rownames = FALSE,
extensions = c("Buttons"),
options = list(dom = "Bfrtip", buttons = c("copy", "csv", "excel"), pageLength = 10, scrollX = TRUE),
class = "stripe hover compact"
)
```
### Tabla 10 — Profundidad y sensibilidad
```{r}
DT::datatable(
tabla_profundidad_dt,
rownames = FALSE,
extensions = c("Buttons"),
options = list(dom = "Bfrtip", buttons = c("copy", "csv", "excel"), pageLength = 20, scrollX = TRUE),
class = "stripe hover compact"
)
```
Notas metodológicas
=======================================================================
```{r}
htmltools::HTML(paste0(
"<div class='insight'><strong>Notas para RPubs:</strong> este dashboard es HTML estático. Funciona sin Shiny Server. Los filtros del mapa usan <em>crosstalk</em>, los gráficos usan <em>plotly</em> y las tablas usan <em>DT</em>.</div>",
"<div class='insight'><strong>Consistencia con el informe:</strong> se retiraron gráficos auxiliares que no estaban en el informe inicial y se conservaron únicamente visualizaciones equivalentes a las figuras y tablas principales del documento que funcionan bien en formato dashboard. Las líneas tectónicas del mapa son referencias aproximadas, dibujadas en coordenadas longitud-latitud para orientar la lectura espacial.</div>",
"<div class='small-note'>Parámetros centrales: Chile [-46,-17] latitud y [-76,-66] longitud; California [32,42] latitud y [-125,-114] longitud; periodo 2000–2025; magnitud mínima Mw ≥ 5,0.</div>"
))
```