Upload 2 files
Browse files- depth_interactive_map_alpha.R +312 -144
depth_interactive_map_alpha.R
CHANGED
|
@@ -91,14 +91,17 @@ PAM_PAL <- c("#ffffd9", "#edf8b1", "#c7e9b4", "#7fcdbb",
|
|
| 91 |
ENVIPE_PAL <- c("#fef0d9", "#fdcc8a", "#fc8d59", "#e34a33",
|
| 92 |
"#b30000", "#7f0000") # OrRd 6
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
GIIM_COLORS <- c(
|
| 95 |
-
"
|
| 96 |
-
"
|
| 97 |
-
"
|
| 98 |
-
"
|
| 99 |
-
"
|
| 100 |
-
"Muy alto" = "#1e3a8a",
|
| 101 |
-
"No aplica" = "#94a3b8"
|
| 102 |
)
|
| 103 |
|
| 104 |
MIGRANTS_PAL <- c("#ffffb2", "#fecc5c", "#fd8d3c",
|
|
@@ -224,13 +227,13 @@ POP_COLS <- list(
|
|
| 224 |
|
| 225 |
# PATP sub-types (kind controls paint family: "num" = step, "cat" = match).
|
| 226 |
PATP_COLS <- list(
|
| 227 |
-
"t_part" = list(col = "t_part", kind = "num", label = "
|
| 228 |
breaks = c(35, 45, 50, 55, 60, 70)),
|
| 229 |
"t_ocup" = list(col = "t_ocup", kind = "num", label = "Occupation rate",
|
| 230 |
breaks = c(92, 95, 97, 98, 99, 99.5)),
|
| 231 |
-
"pr" = list(col = "pr", kind = "num", label = "
|
| 232 |
breaks = c(0.5, 2, 5, 10, 25, 50)),
|
| 233 |
-
"ind_conec" = list(col = "ind_conec", kind = "num", label = "
|
| 234 |
breaks = c(0.15, 0.25, 0.35, 0.45, 0.6, 0.75)),
|
| 235 |
"pr_pred_loc_rur" = list(col = "pr_pred_loc_rur", kind = "cat",
|
| 236 |
label = "Predominant rural problem",
|
|
@@ -261,13 +264,16 @@ POBREZA_COLS <- list(
|
|
| 261 |
# step through one month at a time. Decode: `year = ym %/% 12`, `month = ym %% 12 + 1`.
|
| 262 |
# Example: Jan 2010 = 24120, Dec 2026 = 24323.
|
| 263 |
TEMPORAL_LAYERS <- list(
|
| 264 |
-
"migrants-pts" = list(label = "
|
| 265 |
table = "migrants",
|
| 266 |
# ym_expr drives both the chart cache and the point
|
| 267 |
# loader; same DuckDB expression in both places so the
|
| 268 |
-
# chart axis and the map filter agree.
|
|
|
|
|
|
|
| 269 |
ym_expr = "incident_year * 12 + CAST(SUBSTR(month, 6, 2) AS INTEGER) - 1",
|
| 270 |
point_ym_expr = "incident_year * 12 + CAST(SUBSTR(month, 6, 2) AS INTEGER) - 1",
|
|
|
|
| 271 |
kind = "point"),
|
| 272 |
"ocved-pts" = list(label = "OCVED violence", color = COL_AMBER,
|
| 273 |
table = "ocved_events",
|
|
@@ -286,6 +292,15 @@ TEMPORAL_LAYERS <- list(
|
|
| 286 |
table = "dim_pam_mun",
|
| 287 |
ym_expr = "CAST(date_part('year', date) AS INTEGER) * 12 + CAST(date_part('month', date) AS INTEGER) - 1",
|
| 288 |
agg = "SUM(pam_n)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
kind = "polygon")
|
| 290 |
)
|
| 291 |
# Outer bounds for the slider in ym encoding: Jan 2010 .. Dec 2026.
|
|
@@ -619,6 +634,26 @@ WEB_SQL_TEMPLATES <- list(
|
|
| 619 |
GROUP BY CVEGEO
|
| 620 |
) p ON c.CVEGEO = p.CVEGEO",
|
| 621 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 622 |
temporal_yr_all =
|
| 623 |
"SELECT {yr_expr} AS yr, COUNT(*) AS n FROM {tbl}
|
| 624 |
WHERE {yr_col} IS NOT NULL
|
|
@@ -831,6 +866,13 @@ ddb_minmax <- function(con, tbl, col, filter_zero = TRUE) {
|
|
| 831 |
as.numeric(DBI::dbGetQuery(con, web_sql_qbreaks(tbl, col, n))[1L, ]),
|
| 832 |
error = function(e) seq_len(n - 1L)
|
| 833 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 834 |
list(
|
| 835 |
crime_sf = tryCatch(
|
| 836 |
DBI::dbGetQuery(con, web_sql_simplified("src_crime", tol = 0.005)) |>
|
|
@@ -841,7 +883,18 @@ ddb_minmax <- function(con, tbl, col, filter_zero = TRUE) {
|
|
| 841 |
qb = list(
|
| 842 |
crime = lapply(CRIME_COLS, function(info)
|
| 843 |
qbq("crime_mun", info$col, length(CRIME_STEP_PAL))),
|
| 844 |
-
desap
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 845 |
pam = qbq("dim_pam_mun", "pam_n", length(PAM_PAL)),
|
| 846 |
pop = qbq("crime_mun", "poblacion", length(POP_PAL))
|
| 847 |
),
|
|
@@ -1048,51 +1101,55 @@ WEB_SPEC <- list(
|
|
| 1048 |
list(id = "hazards", label = "Hazards", open = TRUE, items = list(
|
| 1049 |
list(kind = "layer", value = "crime-choro",
|
| 1050 |
label = "Criminal incidence by municipality",
|
| 1051 |
-
tooltip = "Rate of crimes per 100,000 people in each municipality. Select crime type using the buttons below.",
|
| 1052 |
-
legend = list(type = "ramp", pal = "crime", unit = " /100k"
|
|
|
|
| 1053 |
checked = TRUE),
|
| 1054 |
list(kind = "radios", group = "crime"),
|
| 1055 |
list(kind = "layer", value = "migrants-pts",
|
| 1056 |
-
label = "
|
| 1057 |
-
tooltip = "Each dot is a location where migrants went
|
| 1058 |
-
legend = list(type = "dot", color = COL_YEL, label = "
|
| 1059 |
list(kind = "layer", value = "desap-choro",
|
| 1060 |
-
label = "Missing and no located persons per 100k
|
| 1061 |
-
tooltip = "People officially reported as disappeared and no located in each municipality, shown per 100,000 residents (2010 to 2024)
|
|
|
|
| 1062 |
legend = list(type = "ramp", pal = "desap", unit = " /100k")),
|
| 1063 |
list(kind = "layer", value = "fosas-choro",
|
| 1064 |
label = "Clandestine graves by municipality",
|
| 1065 |
tooltip = "Number of clandestine graves and bodies found per municipality. Source: Plataforma Ciudadana de Fosas (2006-2024)",
|
| 1066 |
-
legend = list(type = "ramp", pal = "fosas", unit = " pits"
|
|
|
|
| 1067 |
list(kind = "radios", group = "fosas"),
|
| 1068 |
list(kind = "layer", value = "graves-pts",
|
| 1069 |
label = "Clandestine burial sites",
|
| 1070 |
tooltip = "Locations where clandestine grave sites have been discovered across Mexico.",
|
| 1071 |
-
legend = list(type = "dot", color = COL_RED, label = "
|
| 1072 |
list(kind = "layer", value = "ocved-pts",
|
| 1073 |
-
label = "Organised criminal violence events
|
| 1074 |
-
tooltip = "Incidents of organised crime violence such as confrontations, attacks and criminal activities.",
|
| 1075 |
legend = list(type = "dot", color = COL_AMBER, label = "Violence event")),
|
| 1076 |
list(kind = "layer", value = "ged-pts",
|
| 1077 |
-
label = "Armed conflict events
|
| 1078 |
-
tooltip = "Armed conflict events recorded by Uppsala University's global conflict research dataset.",
|
| 1079 |
legend = list(type = "dot", color = COL_LIGHT_BLUE, label = "Conflict event")),
|
| 1080 |
list(kind = "layer", value = "envipe-state",
|
| 1081 |
-
label = "Crime victimisation survey by state
|
| 1082 |
-
tooltip = "State-level survey on how safe people feel and how many crimes go unreported. Select metric below.",
|
| 1083 |
-
legend = list(type = "ramp", pal = "envipe", unit = " %"
|
|
|
|
| 1084 |
list(kind = "radios", group = "envipe"),
|
| 1085 |
list(kind = "soon", label = "Heat stress index")
|
| 1086 |
)),
|
| 1087 |
list(id = "exposure", label = "Exposure", open = FALSE, items = list(
|
| 1088 |
list(kind = "layer", value = "pam-choro",
|
| 1089 |
-
label = "Irregular migration events by municipality
|
| 1090 |
-
tooltip = "
|
| 1091 |
legend = list(type = "ramp", pal = "pam", unit = " events")),
|
| 1092 |
list(kind = "layer", value = "remesas-choro",
|
| 1093 |
-
label = "
|
| 1094 |
-
tooltip = "Index measuring the intensity of migration from Mexico to the United States, from none to very high.",
|
| 1095 |
-
legend = list(type = "cats", pal = "giim")),
|
| 1096 |
list(kind = "layer", value = "routes-lines",
|
| 1097 |
label = "Main migration routes through Mexico",
|
| 1098 |
tooltip = "Key routes migrants use to travel through Mexico towards the northern border.",
|
|
@@ -1102,9 +1159,10 @@ WEB_SPEC <- list(
|
|
| 1102 |
tooltip = "Railway lines used by migrants, including freight routes known as 'La Bestia'.",
|
| 1103 |
legend = list(type = "line", color = "#9E2A3A", label = "Railway", dashed = TRUE)),
|
| 1104 |
list(kind = "layer", value = "pop-choro",
|
| 1105 |
-
label = "Population (
|
| 1106 |
tooltip = "Census 2020 population counts. With the grid view off, each municipality shows the sum of its CPV cells (3 sub-types: total, born abroad, resident abroad in 2015). With the grid view on, each rHEALPix cell shows its own value at the active nivel (n4-n7).",
|
| 1107 |
-
legend = list(type = "ramp", pal = "pop", unit = " people"
|
|
|
|
| 1108 |
list(kind = "radios", group = "pop")
|
| 1109 |
)),
|
| 1110 |
list(id = "vulnerability", label = "Vulnerability", open = FALSE, items = list(
|
|
@@ -1113,9 +1171,10 @@ WEB_SPEC <- list(
|
|
| 1113 |
tooltip = "Shelters, food, health, government, legal and religious assistance points along migrant routes. Click a swatch in the legend to filter.",
|
| 1114 |
legend = list(type = "cats", pal = "infra_cats", filterable = "infra")),
|
| 1115 |
list(kind = "layer", value = "patp-choro",
|
| 1116 |
-
label = "
|
| 1117 |
-
tooltip = "Multi-dimensional
|
| 1118 |
-
legend = list(type = "ramp", pal = "patp", unit = ""
|
|
|
|
| 1119 |
# Variant legends shown when the matching categorical chip is active.
|
| 1120 |
# data-variant-key on each wrap is matched by setPATPType in JS.
|
| 1121 |
legend_extras = list(
|
|
@@ -1128,7 +1187,8 @@ WEB_SPEC <- list(
|
|
| 1128 |
list(kind = "layer", value = "pobreza-choro",
|
| 1129 |
label = "Poverty (CONEVAL 2020)",
|
| 1130 |
tooltip = "CONEVAL 2020 poverty / extreme poverty share by municipality. Enable the grid view to see this layer.",
|
| 1131 |
-
legend = list(type = "ramp", pal = "pobreza", unit = " %"
|
|
|
|
| 1132 |
list(kind = "radios", group = "pobreza")
|
| 1133 |
))
|
| 1134 |
),
|
|
@@ -1151,14 +1211,14 @@ WEB_SPEC <- list(
|
|
| 1151 |
),
|
| 1152 |
pop = list(
|
| 1153 |
list(label = "Total population", value = "POBTOT"),
|
| 1154 |
-
list(label = "
|
| 1155 |
-
list(label = "
|
| 1156 |
),
|
| 1157 |
patp = list(
|
| 1158 |
-
list(label = "Participation rate", value = "t_part"),
|
| 1159 |
list(label = "Occupation rate", value = "t_ocup"),
|
| 1160 |
list(label = "PR indicator", value = "pr"),
|
| 1161 |
-
list(label = "
|
| 1162 |
list(label = "Predominant rural problem", value = "pr_pred_loc_rur"),
|
| 1163 |
list(label = "Common-jurisdiction crime", value = "delitos_fc_b_j_a")
|
| 1164 |
),
|
|
@@ -1174,10 +1234,16 @@ WEB_SPEC <- list(
|
|
| 1174 |
subtitle = "Summary of main indicators for migrants across Mexico.",
|
| 1175 |
groups = list(
|
| 1176 |
list(id = "hazards", label = "Hazards", open = TRUE, cards = list(
|
| 1177 |
-
list(title = "Missing Migrants Incidents (IOM)", kind = "stat",
|
| 1178 |
stats = list(
|
| 1179 |
list(id = "n_incidents", label = "Recorded incidents", color = COL_YEL),
|
| 1180 |
list(id = "n_fatalities", label = "Confirmed dead", color = COL_RED)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1181 |
)),
|
| 1182 |
list(title = "Missing persons and not located (RNPDNO)", kind = "stat",
|
| 1183 |
stats = list(list(id = "n_desap", label = "Disappearances & no located", color = COL_RISK))),
|
|
@@ -1523,7 +1589,8 @@ LUCIDE_JS <- tags$head(
|
|
| 1523 |
// first). The chart follows only the most-recent one. ymLo/ymHi drive the
|
| 1524 |
// map filter (client-side for points, server for PAM). ym encoding:
|
| 1525 |
// ym = year * 12 + month - 1 (Jan 2010 = 24120, Dec 2026 = 24323).
|
| 1526 |
-
var TEMPORAL_LAYER_IDS = ['migrants-pts','ocved-pts','ged-pts',
|
|
|
|
| 1527 |
// Point layers carry a unified 'ym' feature property via load_pts_with_ym
|
| 1528 |
// so the client filter doesn't need to know each table's date format.
|
| 1529 |
var TEMPORAL_POINT_COL = {
|
|
@@ -2416,17 +2483,32 @@ dp_stat <- function(val_id, label, color = "#1e293b") {
|
|
| 2416 |
}
|
| 2417 |
|
| 2418 |
ind_card <- function(title, body, chart_id = NULL, note = NULL, soon = FALSE,
|
| 2419 |
-
half = FALSE, chart_height = "55px"
|
|
|
|
| 2420 |
cls <- paste("ind-card", if (isTRUE(half)) "ind-card--half" else "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2421 |
div(class = cls,
|
| 2422 |
div(class = "ind-card-hdr",
|
| 2423 |
span(class = "ind-card-title", title),
|
| 2424 |
if (soon) tags$span(class = "acc-coming-tag", "Soon")
|
| 2425 |
),
|
| 2426 |
if (!is.null(body)) div(class = "ind-card-body", body),
|
| 2427 |
-
|
| 2428 |
-
div(class = "ind-card-chart",
|
| 2429 |
-
plotOutput(chart_id, width = "100%", height = chart_height)),
|
| 2430 |
if (!is.null(note))
|
| 2431 |
div(class = "ind-card-note", note)
|
| 2432 |
)
|
|
@@ -2448,12 +2530,22 @@ acc_legend_from_spec <- function(lg) {
|
|
| 2448 |
pal <- if (!is.null(lg$pal)) WEB_SPEC$palettes[[lg$pal]] else NULL
|
| 2449 |
switch(lg$type,
|
| 2450 |
ramp = {
|
| 2451 |
-
#
|
| 2452 |
-
#
|
| 2453 |
-
|
| 2454 |
-
|
| 2455 |
-
|
| 2456 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2457 |
},
|
| 2458 |
cats = acc_legend_cats(pal, filterable_key = lg$filterable),
|
| 2459 |
dot = acc_legend_dot(lg$color, lg$label %||% "Event / point"),
|
|
@@ -2522,6 +2614,7 @@ render_panel_card <- function(card) {
|
|
| 2522 |
)
|
| 2523 |
ind_card(card$title, body,
|
| 2524 |
chart_id = card$chart,
|
|
|
|
| 2525 |
note = card$note,
|
| 2526 |
half = isTRUE(card$half),
|
| 2527 |
chart_height = "55px")
|
|
@@ -4250,6 +4343,28 @@ server <- function(input, output, session) {
|
|
| 4250 |
as.character(pl$cats %||% character(0)))
|
| 4251 |
}, ignoreInit = TRUE, ignoreNULL = TRUE)
|
| 4252 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4253 |
# --- Nav-bar search: fly to picked result -----------------------------------
|
| 4254 |
observeEvent(input$search_flyto, {
|
| 4255 |
pt <- input$search_flyto
|
|
@@ -4391,62 +4506,114 @@ server <- function(input, output, session) {
|
|
| 4391 |
)
|
| 4392 |
})
|
| 4393 |
|
| 4394 |
-
|
| 4395 |
-
|
| 4396 |
-
|
| 4397 |
-
|
| 4398 |
-
|
| 4399 |
-
|
| 4400 |
-
|
| 4401 |
-
|
| 4402 |
-
|
| 4403 |
-
|
| 4404 |
-
|
| 4405 |
-
|
| 4406 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4407 |
|
| 4408 |
-
|
| 4409 |
-
|
| 4410 |
-
|
| 4411 |
-
|
| 4412 |
-
|
| 4413 |
-
|
| 4414 |
-
|
| 4415 |
-
|
| 4416 |
-
|
| 4417 |
-
|
| 4418 |
-
|
| 4419 |
-
|
| 4420 |
-
|
| 4421 |
-
|
| 4422 |
-
|
| 4423 |
-
|
| 4424 |
-
|
| 4425 |
-
|
| 4426 |
-
|
| 4427 |
-
|
| 4428 |
-
|
| 4429 |
-
|
| 4430 |
-
|
| 4431 |
-
|
| 4432 |
-
|
| 4433 |
-
|
| 4434 |
-
|
| 4435 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4436 |
}
|
| 4437 |
-
|
| 4438 |
-
|
| 4439 |
-
|
| 4440 |
-
|
| 4441 |
-
|
| 4442 |
-
|
| 4443 |
-
|
| 4444 |
-
|
| 4445 |
-
|
| 4446 |
-
|
| 4447 |
-
|
| 4448 |
-
|
| 4449 |
-
}, bg = "transparent", res =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4450 |
|
| 4451 |
# Shared helper: temporal bar chart by year for point event tables
|
| 4452 |
make_temporal_yr <- function(tbl, yr_col, line_color) {
|
|
@@ -4561,45 +4728,45 @@ server <- function(input, output, session) {
|
|
| 4561 |
))
|
| 4562 |
}, ignoreInit = TRUE)
|
| 4563 |
|
| 4564 |
-
#
|
| 4565 |
-
#
|
| 4566 |
-
# layer
|
| 4567 |
-
#
|
| 4568 |
-
#
|
| 4569 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4570 |
observeEvent(input$temporal_filter, {
|
| 4571 |
pl <- input$temporal_filter
|
| 4572 |
if (is.null(pl)) return()
|
| 4573 |
active <- pl$active %||% character(0)
|
| 4574 |
-
if (!("pam-choro" %in% active) || !isTRUE(lazy_added$pam)) return()
|
| 4575 |
ym_lo <- pl$start %||% TEMPORAL_RANGE[1]
|
| 4576 |
ym_hi <- pl$end %||% TEMPORAL_RANGE[2]
|
| 4577 |
-
|
| 4578 |
-
|
| 4579 |
-
|
| 4580 |
-
|
| 4581 |
-
|
| 4582 |
-
|
| 4583 |
-
|
| 4584 |
-
|
| 4585 |
-
|
| 4586 |
-
|
| 4587 |
-
|
| 4588 |
-
|
| 4589 |
-
|
| 4590 |
-
|
| 4591 |
-
|
| 4592 |
-
|
| 4593 |
-
|
| 4594 |
-
|
| 4595 |
-
|
| 4596 |
-
|
| 4597 |
-
|
| 4598 |
-
|
| 4599 |
-
|
| 4600 |
-
# app appear to freeze whenever PAM was re-enabled.
|
| 4601 |
-
maplibre_proxy("map") |>
|
| 4602 |
-
set_source(layer_id = "pam-choro", source = sf_data)
|
| 4603 |
}, ignoreNULL = TRUE, ignoreInit = TRUE)
|
| 4604 |
|
| 4605 |
# --- PAM temporal plot (irregular migration events over time) ---------------
|
|
@@ -4711,7 +4878,8 @@ server <- function(input, output, session) {
|
|
| 4711 |
# (otherwise hidden outputs stay suspended and read "---"). Cost: one extra
|
| 4712 |
# render per output per session -- negligible (cached strings + small plots).
|
| 4713 |
for (.oid in c(
|
| 4714 |
-
"temporal_pam", "
|
|
|
|
| 4715 |
"breakdown_giim", "breakdown_infra",
|
| 4716 |
"n_incidents", "n_fatalities", "n_graves", "n_facilities", "n_events",
|
| 4717 |
"ocved_n", "n_ged", "n_mun", "n_desap", "n_pam", "n_fosas", "n_cuerpos",
|
|
|
|
| 91 |
ENVIPE_PAL <- c("#fef0d9", "#fdcc8a", "#fc8d59", "#e34a33",
|
| 92 |
"#b30000", "#7f0000") # OrRd 6
|
| 93 |
|
| 94 |
+
# GIIM keys MUST match the Spanish category strings stored in
|
| 95 |
+
# dim_remesas_mun.giim_mun (Muy bajo / Bajo / Medio / Alto / Muy alto) so the
|
| 96 |
+
# MapLibre `match` paint expression actually pairs each muni with a colour.
|
| 97 |
+
# "No aplica" and "Nulo" are intentionally absent here -- they paint via the
|
| 98 |
+
# match-fallback to fully-transparent and are filtered out of the legend.
|
| 99 |
GIIM_COLORS <- c(
|
| 100 |
+
"Muy bajo" = "#dbeafe",
|
| 101 |
+
"Bajo" = "#93c5fd",
|
| 102 |
+
"Medio" = "#3b82f6",
|
| 103 |
+
"Alto" = "#1d4ed8",
|
| 104 |
+
"Muy alto" = "#1e3a8a"
|
|
|
|
|
|
|
| 105 |
)
|
| 106 |
|
| 107 |
MIGRANTS_PAL <- c("#ffffb2", "#fecc5c", "#fd8d3c",
|
|
|
|
| 227 |
|
| 228 |
# PATP sub-types (kind controls paint family: "num" = step, "cat" = match).
|
| 229 |
PATP_COLS <- list(
|
| 230 |
+
"t_part" = list(col = "t_part", kind = "num", label = "Economic participation rate",
|
| 231 |
breaks = c(35, 45, 50, 55, 60, 70)),
|
| 232 |
"t_ocup" = list(col = "t_ocup", kind = "num", label = "Occupation rate",
|
| 233 |
breaks = c(92, 95, 97, 98, 99, 99.5)),
|
| 234 |
+
"pr" = list(col = "pr", kind = "num", label = "Remittance receipt per capita",
|
| 235 |
breaks = c(0.5, 2, 5, 10, 25, 50)),
|
| 236 |
+
"ind_conec" = list(col = "ind_conec", kind = "num", label = "Road connectivity index",
|
| 237 |
breaks = c(0.15, 0.25, 0.35, 0.45, 0.6, 0.75)),
|
| 238 |
"pr_pred_loc_rur" = list(col = "pr_pred_loc_rur", kind = "cat",
|
| 239 |
label = "Predominant rural problem",
|
|
|
|
| 264 |
# step through one month at a time. Decode: `year = ym %/% 12`, `month = ym %% 12 + 1`.
|
| 265 |
# Example: Jan 2010 = 24120, Dec 2026 = 24323.
|
| 266 |
TEMPORAL_LAYERS <- list(
|
| 267 |
+
"migrants-pts" = list(label = "Death during migration", color = COL_YEL,
|
| 268 |
table = "migrants",
|
| 269 |
# ym_expr drives both the chart cache and the point
|
| 270 |
# loader; same DuckDB expression in both places so the
|
| 271 |
+
# chart axis and the map filter agree. agg switches the
|
| 272 |
+
# strip from raw incidents (COUNT(*)) to confirmed
|
| 273 |
+
# deaths (SUM n_dead) -- the indicator of interest.
|
| 274 |
ym_expr = "incident_year * 12 + CAST(SUBSTR(month, 6, 2) AS INTEGER) - 1",
|
| 275 |
point_ym_expr = "incident_year * 12 + CAST(SUBSTR(month, 6, 2) AS INTEGER) - 1",
|
| 276 |
+
agg = "SUM(COALESCE(n_dead, 0))",
|
| 277 |
kind = "point"),
|
| 278 |
"ocved-pts" = list(label = "OCVED violence", color = COL_AMBER,
|
| 279 |
table = "ocved_events",
|
|
|
|
| 292 |
table = "dim_pam_mun",
|
| 293 |
ym_expr = "CAST(date_part('year', date) AS INTEGER) * 12 + CAST(date_part('month', date) AS INTEGER) - 1",
|
| 294 |
agg = "SUM(pam_n)",
|
| 295 |
+
kind = "polygon"),
|
| 296 |
+
# Clandestine graves are recorded at annual resolution (one (CVEGEO, year)
|
| 297 |
+
# row per muni). Encode year as ym = year * 12 (Jan of that year) so the
|
| 298 |
+
# bars land cleanly on year tick marks even though the slider counts months.
|
| 299 |
+
"fosas-choro" = list(label = "Clandestine graves",
|
| 300 |
+
color = "#fec44f", # FOSAS_PAL mid (amber)
|
| 301 |
+
table = "dim_fosas_mun",
|
| 302 |
+
ym_expr = "year * 12",
|
| 303 |
+
agg = "SUM(fosas_total)",
|
| 304 |
kind = "polygon")
|
| 305 |
)
|
| 306 |
# Outer bounds for the slider in ym encoding: Jan 2010 .. Dec 2026.
|
|
|
|
| 634 |
GROUP BY CVEGEO
|
| 635 |
) p ON c.CVEGEO = p.CVEGEO",
|
| 636 |
|
| 637 |
+
# ym-window-filtered fosas aggregate. dim_fosas_mun is annual (one row per
|
| 638 |
+
# CVEGEO with that muni's recorded year of activity), so the ym filter
|
| 639 |
+
# operates on year * 12. fosas_total / cuerpos_total are SUMmed in case
|
| 640 |
+
# any muni ever ends up with multiple rows.
|
| 641 |
+
src_fosas_ym =
|
| 642 |
+
"SELECT c.municipio, c.entidad, c.CVEGEO,
|
| 643 |
+
COALESCE(CAST(f.fosas_sum AS INTEGER), 0) AS fosas_total,
|
| 644 |
+
COALESCE(CAST(f.cuerpos_sum AS INTEGER), 0) AS cuerpos_total,
|
| 645 |
+
c.wkt
|
| 646 |
+
FROM crime_mun c
|
| 647 |
+
LEFT JOIN (
|
| 648 |
+
SELECT CVEGEO,
|
| 649 |
+
SUM(fosas_total) AS fosas_sum,
|
| 650 |
+
SUM(cuerpos_total) AS cuerpos_sum
|
| 651 |
+
FROM dim_fosas_mun
|
| 652 |
+
WHERE year IS NOT NULL
|
| 653 |
+
AND (year * 12) BETWEEN {ym_lo} AND {ym_hi}
|
| 654 |
+
GROUP BY CVEGEO
|
| 655 |
+
) f ON c.CVEGEO = f.CVEGEO",
|
| 656 |
+
|
| 657 |
temporal_yr_all =
|
| 658 |
"SELECT {yr_expr} AS yr, COUNT(*) AS n FROM {tbl}
|
| 659 |
WHERE {yr_col} IS NOT NULL
|
|
|
|
| 866 |
as.numeric(DBI::dbGetQuery(con, web_sql_qbreaks(tbl, col, n))[1L, ]),
|
| 867 |
error = function(e) seq_len(n - 1L)
|
| 868 |
)
|
| 869 |
+
# Arbitrary-SQL variant: lets a layer compute its breaks on a derived view
|
| 870 |
+
# (e.g. per-muni averages) so the colour ramp aligns with the painted
|
| 871 |
+
# values rather than raw row-level distribution.
|
| 872 |
+
qbq_sql <- function(sql) tryCatch(
|
| 873 |
+
as.numeric(DBI::dbGetQuery(con, sql)[1L, ]),
|
| 874 |
+
error = function(e) NULL
|
| 875 |
+
)
|
| 876 |
list(
|
| 877 |
crime_sf = tryCatch(
|
| 878 |
DBI::dbGetQuery(con, web_sql_simplified("src_crime", tol = 0.005)) |>
|
|
|
|
| 883 |
qb = list(
|
| 884 |
crime = lapply(CRIME_COLS, function(info)
|
| 885 |
qbq("crime_mun", info$col, length(CRIME_STEP_PAL))),
|
| 886 |
+
# desap is averaged per-CVEGEO in src_desap, so the ramp also has to be
|
| 887 |
+
# computed on per-muni averages -- not on the raw row distribution
|
| 888 |
+
# (which collapses 5 of 6 buckets into a narrow low-end window).
|
| 889 |
+
desap = qbq_sql(paste0(
|
| 890 |
+
"WITH m AS (SELECT CVEGEO, AVG(final_desap_nl_per_100k) AS r ",
|
| 891 |
+
" FROM dim_desap_mun GROUP BY CVEGEO HAVING r > 0) ",
|
| 892 |
+
"SELECT ",
|
| 893 |
+
paste0("PERCENTILE_CONT(", round(seq_len(length(DESAP_PAL) - 1L) / length(DESAP_PAL), 8),
|
| 894 |
+
") WITHIN GROUP (ORDER BY r)",
|
| 895 |
+
collapse = ", "),
|
| 896 |
+
" FROM m"
|
| 897 |
+
)) %||% qbq("dim_desap_mun", "final_desap_nl_per_100k", length(DESAP_PAL)),
|
| 898 |
pam = qbq("dim_pam_mun", "pam_n", length(PAM_PAL)),
|
| 899 |
pop = qbq("crime_mun", "poblacion", length(POP_PAL))
|
| 900 |
),
|
|
|
|
| 1101 |
list(id = "hazards", label = "Hazards", open = TRUE, items = list(
|
| 1102 |
list(kind = "layer", value = "crime-choro",
|
| 1103 |
label = "Criminal incidence by municipality",
|
| 1104 |
+
tooltip = "Rate of crimes per 100,000 people in each municipality. Select crime type using the buttons below. Source: Registro Nacional de Incidencia Delictiva (RNID)",
|
| 1105 |
+
legend = list(type = "ramp", pal = "crime", unit = " /100k",
|
| 1106 |
+
labels = "qualitative"),
|
| 1107 |
checked = TRUE),
|
| 1108 |
list(kind = "radios", group = "crime"),
|
| 1109 |
list(kind = "layer", value = "migrants-pts",
|
| 1110 |
+
label = "Deaths during migration",
|
| 1111 |
+
tooltip = "Each dot is a location where migrants went died, recorded by the International Organization for Migration.",
|
| 1112 |
+
legend = list(type = "dot", color = COL_YEL, label = "Death location")),
|
| 1113 |
list(kind = "layer", value = "desap-choro",
|
| 1114 |
+
label = "Missing and no located persons per 100k",
|
| 1115 |
+
tooltip = "People officially reported as disappeared and no located in each municipality, shown per 100,000 residents (2010 to 2024). Source:
|
| 1116 |
+
Registro Nacional de Personas Desaparecidas y No Localizadas (RNPDNO)",
|
| 1117 |
legend = list(type = "ramp", pal = "desap", unit = " /100k")),
|
| 1118 |
list(kind = "layer", value = "fosas-choro",
|
| 1119 |
label = "Clandestine graves by municipality",
|
| 1120 |
tooltip = "Number of clandestine graves and bodies found per municipality. Source: Plataforma Ciudadana de Fosas (2006-2024)",
|
| 1121 |
+
legend = list(type = "ramp", pal = "fosas", unit = " pits/bodies",
|
| 1122 |
+
labels = "qualitative")),
|
| 1123 |
list(kind = "radios", group = "fosas"),
|
| 1124 |
list(kind = "layer", value = "graves-pts",
|
| 1125 |
label = "Clandestine burial sites",
|
| 1126 |
tooltip = "Locations where clandestine grave sites have been discovered across Mexico.",
|
| 1127 |
+
legend = list(type = "dot", color = COL_RED, label = "Site location")),
|
| 1128 |
list(kind = "layer", value = "ocved-pts",
|
| 1129 |
+
label = "Organised criminal violence events",
|
| 1130 |
+
tooltip = "Incidents of organised crime violence such as confrontations, attacks and criminal activities. Source: (OCVED 2.0)",
|
| 1131 |
legend = list(type = "dot", color = COL_AMBER, label = "Violence event")),
|
| 1132 |
list(kind = "layer", value = "ged-pts",
|
| 1133 |
+
label = "Armed conflict events",
|
| 1134 |
+
tooltip = "Armed conflict events recorded by Uppsala University's global conflict research dataset. Source: (UCDP-GED)",
|
| 1135 |
legend = list(type = "dot", color = COL_LIGHT_BLUE, label = "Conflict event")),
|
| 1136 |
list(kind = "layer", value = "envipe-state",
|
| 1137 |
+
label = "Crime victimisation survey by state",
|
| 1138 |
+
tooltip = "State-level survey on how safe people feel and how many crimes go unreported. Select metric below. Source: National Survey of Victimization and Perception of Public Safety (ENVIPE)",
|
| 1139 |
+
legend = list(type = "ramp", pal = "envipe", unit = " %",
|
| 1140 |
+
labels = "qualitative")),
|
| 1141 |
list(kind = "radios", group = "envipe"),
|
| 1142 |
list(kind = "soon", label = "Heat stress index")
|
| 1143 |
)),
|
| 1144 |
list(id = "exposure", label = "Exposure", open = FALSE, items = list(
|
| 1145 |
list(kind = "layer", value = "pam-choro",
|
| 1146 |
+
label = "Irregular migration events by municipality",
|
| 1147 |
+
tooltip = "Events involving people who entered Mexican territory without immigration documents proving their legal stay and were subjected to an Administrative Immigration Procedure (PAM). Source: Unidad de Política Migratoria. Gobierno de México",
|
| 1148 |
legend = list(type = "ramp", pal = "pam", unit = " events")),
|
| 1149 |
list(kind = "layer", value = "remesas-choro",
|
| 1150 |
+
label = "Degree of Migration Intensity MEX-USA (GIIM)",
|
| 1151 |
+
tooltip = "Index measuring the intensity of migration from Mexico to the United States, from none to very high. Source: CONAPO",
|
| 1152 |
+
legend = list(type = "cats", pal = "giim", filterable = "remesas")),
|
| 1153 |
list(kind = "layer", value = "routes-lines",
|
| 1154 |
label = "Main migration routes through Mexico",
|
| 1155 |
tooltip = "Key routes migrants use to travel through Mexico towards the northern border.",
|
|
|
|
| 1159 |
tooltip = "Railway lines used by migrants, including freight routes known as 'La Bestia'.",
|
| 1160 |
legend = list(type = "line", color = "#9E2A3A", label = "Railway", dashed = TRUE)),
|
| 1161 |
list(kind = "layer", value = "pop-choro",
|
| 1162 |
+
label = "Population (Census 2020)",
|
| 1163 |
tooltip = "Census 2020 population counts. With the grid view off, each municipality shows the sum of its CPV cells (3 sub-types: total, born abroad, resident abroad in 2015). With the grid view on, each rHEALPix cell shows its own value at the active nivel (n4-n7).",
|
| 1164 |
+
legend = list(type = "ramp", pal = "pop", unit = " people",
|
| 1165 |
+
labels = "qualitative")),
|
| 1166 |
list(kind = "radios", group = "pop")
|
| 1167 |
)),
|
| 1168 |
list(id = "vulnerability", label = "Vulnerability", open = FALSE, items = list(
|
|
|
|
| 1171 |
tooltip = "Shelters, food, health, government, legal and religious assistance points along migrant routes. Click a swatch in the legend to filter.",
|
| 1172 |
legend = list(type = "cats", pal = "infra_cats", filterable = "infra")),
|
| 1173 |
list(kind = "layer", value = "patp-choro",
|
| 1174 |
+
label = "Territorial analysis of poverty",
|
| 1175 |
+
tooltip = "Multi-dimensional poverty analysis, including sociodemographics, infrastructure and government. Source: CONEVAL",
|
| 1176 |
+
legend = list(type = "ramp", pal = "patp", unit = "",
|
| 1177 |
+
labels = "qualitative"),
|
| 1178 |
# Variant legends shown when the matching categorical chip is active.
|
| 1179 |
# data-variant-key on each wrap is matched by setPATPType in JS.
|
| 1180 |
legend_extras = list(
|
|
|
|
| 1187 |
list(kind = "layer", value = "pobreza-choro",
|
| 1188 |
label = "Poverty (CONEVAL 2020)",
|
| 1189 |
tooltip = "CONEVAL 2020 poverty / extreme poverty share by municipality. Enable the grid view to see this layer.",
|
| 1190 |
+
legend = list(type = "ramp", pal = "pobreza", unit = " %",
|
| 1191 |
+
labels = "qualitative")),
|
| 1192 |
list(kind = "radios", group = "pobreza")
|
| 1193 |
))
|
| 1194 |
),
|
|
|
|
| 1211 |
),
|
| 1212 |
pop = list(
|
| 1213 |
list(label = "Total population", value = "POBTOT"),
|
| 1214 |
+
list(label = "Foreign population", value = "PNACOP"),
|
| 1215 |
+
list(label = "Recent resident (abroad in 2015)", value = "PRESOP2015")
|
| 1216 |
),
|
| 1217 |
patp = list(
|
| 1218 |
+
list(label = "Economic Participation rate", value = "t_part"),
|
| 1219 |
list(label = "Occupation rate", value = "t_ocup"),
|
| 1220 |
list(label = "PR indicator", value = "pr"),
|
| 1221 |
+
list(label = "connectivity index", value = "ind_conec"),
|
| 1222 |
list(label = "Predominant rural problem", value = "pr_pred_loc_rur"),
|
| 1223 |
list(label = "Common-jurisdiction crime", value = "delitos_fc_b_j_a")
|
| 1224 |
),
|
|
|
|
| 1234 |
subtitle = "Summary of main indicators for migrants across Mexico.",
|
| 1235 |
groups = list(
|
| 1236 |
list(id = "hazards", label = "Hazards", open = TRUE, cards = list(
|
| 1237 |
+
list(title = "Missing Migrants Incidents (IOM)", kind = "stat",
|
| 1238 |
stats = list(
|
| 1239 |
list(id = "n_incidents", label = "Recorded incidents", color = COL_YEL),
|
| 1240 |
list(id = "n_fatalities", label = "Confirmed dead", color = COL_RED)
|
| 1241 |
+
),
|
| 1242 |
+
charts = list(
|
| 1243 |
+
list(id = "migrants_treemap_cause", label = "Cause of death",
|
| 1244 |
+
height = "70px"),
|
| 1245 |
+
list(id = "migrants_treemap_demo", label = "Demographics (children / female / male)",
|
| 1246 |
+
height = "44px")
|
| 1247 |
)),
|
| 1248 |
list(title = "Missing persons and not located (RNPDNO)", kind = "stat",
|
| 1249 |
stats = list(list(id = "n_desap", label = "Disappearances & no located", color = COL_RISK))),
|
|
|
|
| 1589 |
// first). The chart follows only the most-recent one. ymLo/ymHi drive the
|
| 1590 |
// map filter (client-side for points, server for PAM). ym encoding:
|
| 1591 |
// ym = year * 12 + month - 1 (Jan 2010 = 24120, Dec 2026 = 24323).
|
| 1592 |
+
var TEMPORAL_LAYER_IDS = ['migrants-pts','ocved-pts','ged-pts',
|
| 1593 |
+
'pam-choro','fosas-choro'];
|
| 1594 |
// Point layers carry a unified 'ym' feature property via load_pts_with_ym
|
| 1595 |
// so the client filter doesn't need to know each table's date format.
|
| 1596 |
var TEMPORAL_POINT_COL = {
|
|
|
|
| 2483 |
}
|
| 2484 |
|
| 2485 |
ind_card <- function(title, body, chart_id = NULL, note = NULL, soon = FALSE,
|
| 2486 |
+
half = FALSE, chart_height = "55px",
|
| 2487 |
+
charts = NULL) {
|
| 2488 |
cls <- paste("ind-card", if (isTRUE(half)) "ind-card--half" else "")
|
| 2489 |
+
# `charts` (plural) is a list of list(id, label, height) for cards that need
|
| 2490 |
+
# more than one plot slot (e.g. the migrants card pairs a cause-of-death
|
| 2491 |
+
# treemap with a demographic-split treemap). If both `chart_id` and `charts`
|
| 2492 |
+
# are NULL the card renders body-only.
|
| 2493 |
+
chart_block <- if (!is.null(charts)) {
|
| 2494 |
+
tagList(lapply(charts, function(c) {
|
| 2495 |
+
div(class = "ind-card-chart",
|
| 2496 |
+
if (!is.null(c$label))
|
| 2497 |
+
tags$div(class = "ind-card-chart-label", c$label),
|
| 2498 |
+
plotOutput(c$id, width = "100%",
|
| 2499 |
+
height = c$height %||% chart_height))
|
| 2500 |
+
}))
|
| 2501 |
+
} else if (!is.null(chart_id)) {
|
| 2502 |
+
div(class = "ind-card-chart",
|
| 2503 |
+
plotOutput(chart_id, width = "100%", height = chart_height))
|
| 2504 |
+
}
|
| 2505 |
div(class = cls,
|
| 2506 |
div(class = "ind-card-hdr",
|
| 2507 |
span(class = "ind-card-title", title),
|
| 2508 |
if (soon) tags$span(class = "acc-coming-tag", "Soon")
|
| 2509 |
),
|
| 2510 |
if (!is.null(body)) div(class = "ind-card-body", body),
|
| 2511 |
+
chart_block,
|
|
|
|
|
|
|
| 2512 |
if (!is.null(note))
|
| 2513 |
div(class = "ind-card-note", note)
|
| 2514 |
)
|
|
|
|
| 2530 |
pal <- if (!is.null(lg$pal)) WEB_SPEC$palettes[[lg$pal]] else NULL
|
| 2531 |
switch(lg$type,
|
| 2532 |
ramp = {
|
| 2533 |
+
# Layers that switch the painted column via a chip (crime, fosas, ...)
|
| 2534 |
+
# can't show a single column's min/max -- the bounds change with every
|
| 2535 |
+
# chip pick. `labels = "qualitative"` opts those layers into static
|
| 2536 |
+
# "Low"/"High" labels.
|
| 2537 |
+
if (identical(lg$labels, "qualitative")) {
|
| 2538 |
+
acc_legend_ramp(pal,
|
| 2539 |
+
lo = lg$lo %||% "Low",
|
| 2540 |
+
hi = lg$hi %||% "High")
|
| 2541 |
+
} else {
|
| 2542 |
+
# Prefer hardcoded lo/hi when provided (back-compat); otherwise pull
|
| 2543 |
+
# the real min/max from the cached data and format with the unit.
|
| 2544 |
+
bnds <- legend_bounds(lg$pal, lg$unit %||% "")
|
| 2545 |
+
acc_legend_ramp(pal,
|
| 2546 |
+
lo = lg$lo %||% bnds$lo,
|
| 2547 |
+
hi = lg$hi %||% bnds$hi)
|
| 2548 |
+
}
|
| 2549 |
},
|
| 2550 |
cats = acc_legend_cats(pal, filterable_key = lg$filterable),
|
| 2551 |
dot = acc_legend_dot(lg$color, lg$label %||% "Event / point"),
|
|
|
|
| 2614 |
)
|
| 2615 |
ind_card(card$title, body,
|
| 2616 |
chart_id = card$chart,
|
| 2617 |
+
charts = card$charts,
|
| 2618 |
note = card$note,
|
| 2619 |
half = isTRUE(card$half),
|
| 2620 |
chart_height = "55px")
|
|
|
|
| 4343 |
as.character(pl$cats %||% character(0)))
|
| 4344 |
}, ignoreInit = TRUE, ignoreNULL = TRUE)
|
| 4345 |
|
| 4346 |
+
# --- Legend filter: remesas-choro by GIIM category --------------------------
|
| 4347 |
+
# Same click-a-swatch-to-toggle pattern; column is `giim_mun` (Spanish
|
| 4348 |
+
# category strings stored in dim_remesas_mun.giim_mun). "No aplica" and
|
| 4349 |
+
# "Nulo" are not in GIIM_COLORS at all, so they paint transparent via
|
| 4350 |
+
# match-fallback and never appear in the legend.
|
| 4351 |
+
observeEvent(input$remesas_filter, {
|
| 4352 |
+
pl <- input$remesas_filter
|
| 4353 |
+
if (is.null(pl) || !isTRUE(lazy_added$remesas)) return()
|
| 4354 |
+
visible <- as.character(pl$cats %||% character(0))
|
| 4355 |
+
prx <- maplibre_proxy("map")
|
| 4356 |
+
if (length(visible) == 0) {
|
| 4357 |
+
prx |> set_filter("remesas-choro",
|
| 4358 |
+
list("==", list("get", "giim_mun"), "__none__"))
|
| 4359 |
+
} else if (length(visible) >= length(GIIM_COLORS)) {
|
| 4360 |
+
prx |> set_filter("remesas-choro", NULL)
|
| 4361 |
+
} else {
|
| 4362 |
+
prx |> set_filter("remesas-choro",
|
| 4363 |
+
list("in", list("get", "giim_mun"),
|
| 4364 |
+
list("literal", as.list(visible))))
|
| 4365 |
+
}
|
| 4366 |
+
}, ignoreInit = TRUE, ignoreNULL = TRUE)
|
| 4367 |
+
|
| 4368 |
# --- Nav-bar search: fly to picked result -----------------------------------
|
| 4369 |
observeEvent(input$search_flyto, {
|
| 4370 |
pt <- input$search_flyto
|
|
|
|
| 4506 |
)
|
| 4507 |
})
|
| 4508 |
|
| 4509 |
+
# --- Migrants treemaps (replaces the old single time-series plot) ----------
|
| 4510 |
+
# Two slot card: cause-of-death and demographic split. Both honour the
|
| 4511 |
+
# current panel selection (whole-country when no polygon is drawn, restricted
|
| 4512 |
+
# to the in-polygon subset when one is). Treemap geometry is a 2-row slice-
|
| 4513 |
+
# and-dice -- the dominant category gets the top row, smaller categories
|
| 4514 |
+
# tile the bottom row. Avoids the treemapify dependency.
|
| 4515 |
+
CAUSE_PAL <- c("Other / unknown" = "#6b7280",
|
| 4516 |
+
"Drowning" = "#0ea5e9",
|
| 4517 |
+
"Harsh environment" = "#f59e0b",
|
| 4518 |
+
"Vehicle accident" = "#a855f7",
|
| 4519 |
+
"Violence" = "#ef4444")
|
| 4520 |
+
DEMO_PAL <- c(Children = "#f97316",
|
| 4521 |
+
Female = "#ec4899",
|
| 4522 |
+
Male = "#0987A8")
|
| 4523 |
+
|
| 4524 |
+
treemap_layout <- function(df) {
|
| 4525 |
+
# df: data.frame(label, value, color); returns df with xmin/xmax/ymin/ymax
|
| 4526 |
+
df <- df[!is.na(df$value) & df$value > 0, , drop = FALSE]
|
| 4527 |
+
if (nrow(df) == 0) return(NULL)
|
| 4528 |
+
df <- df[order(-df$value), , drop = FALSE]
|
| 4529 |
+
total <- sum(df$value)
|
| 4530 |
+
# Top row holds the leader; remainder tiles the bottom row.
|
| 4531 |
+
leader <- df[1, , drop = FALSE]
|
| 4532 |
+
rest <- if (nrow(df) > 1) df[-1, , drop = FALSE] else NULL
|
| 4533 |
+
leader$xmin <- 0; leader$xmax <- 1
|
| 4534 |
+
leader_share <- leader$value / total
|
| 4535 |
+
leader$ymax <- 1; leader$ymin <- 1 - leader_share
|
| 4536 |
+
if (!is.null(rest) && nrow(rest) > 0) {
|
| 4537 |
+
rest_share <- rest$value / sum(rest$value)
|
| 4538 |
+
rest$xmax <- cumsum(rest_share)
|
| 4539 |
+
rest$xmin <- c(0, head(rest$xmax, -1))
|
| 4540 |
+
rest$ymin <- 0; rest$ymax <- 1 - leader_share
|
| 4541 |
+
rbind(leader, rest)
|
| 4542 |
+
} else {
|
| 4543 |
+
leader$ymin <- 0; leader$ymax <- 1
|
| 4544 |
+
leader
|
| 4545 |
+
}
|
| 4546 |
+
}
|
| 4547 |
|
| 4548 |
+
treemap_plot <- function(df, pal, base_size = 7) {
|
| 4549 |
+
laid <- treemap_layout(df)
|
| 4550 |
+
if (is.null(laid)) return(NULL)
|
| 4551 |
+
laid$pct <- round(100 * laid$value / sum(laid$value))
|
| 4552 |
+
laid$label_full <- paste0(laid$label, "\n", laid$pct, "%")
|
| 4553 |
+
ggplot(laid, aes(xmin = xmin, xmax = xmax,
|
| 4554 |
+
ymin = ymin, ymax = ymax,
|
| 4555 |
+
fill = label)) +
|
| 4556 |
+
geom_rect(color = "#0f172a", linewidth = 0.4) +
|
| 4557 |
+
geom_text(aes(x = (xmin + xmax) / 2,
|
| 4558 |
+
y = (ymin + ymax) / 2,
|
| 4559 |
+
label = label_full),
|
| 4560 |
+
size = base_size / .pt * 2.4, color = "white",
|
| 4561 |
+
fontface = "bold", lineheight = 0.85) +
|
| 4562 |
+
scale_fill_manual(values = pal, na.value = "#334155") +
|
| 4563 |
+
coord_cartesian(expand = FALSE, xlim = c(0, 1), ylim = c(0, 1)) +
|
| 4564 |
+
theme_void() +
|
| 4565 |
+
theme(legend.position = "none",
|
| 4566 |
+
plot.background = element_rect(fill = "transparent", color = NA),
|
| 4567 |
+
panel.background = element_rect(fill = "transparent", color = NA),
|
| 4568 |
+
plot.margin = margin(0, 0, 0, 0))
|
| 4569 |
+
}
|
| 4570 |
+
|
| 4571 |
+
# Source data: the whole migrants table by default, or just the in-polygon
|
| 4572 |
+
# subset when the user has drawn a selection (mirrors n_incidents handling).
|
| 4573 |
+
migrants_filtered <- reactive({
|
| 4574 |
+
poly_wkt <- rv$temporal_filter_wkt
|
| 4575 |
+
if (is.null(poly_wkt)) {
|
| 4576 |
+
tryCatch(DBI::dbGetQuery(con,
|
| 4577 |
+
"SELECT cause_cat, n_dead, n_children, n_females, n_males
|
| 4578 |
+
FROM migrants"),
|
| 4579 |
+
error = function(e) { message("migrants_filtered: ", e$message); NULL })
|
| 4580 |
+
} else {
|
| 4581 |
+
sql <- glue::glue(
|
| 4582 |
+
"SELECT m.cause_cat, m.n_dead, m.n_children, m.n_females, m.n_males
|
| 4583 |
+
FROM migrants m
|
| 4584 |
+
WHERE m.lon IS NOT NULL AND m.lat IS NOT NULL
|
| 4585 |
+
AND ST_Within(ST_Point(m.lon, m.lat), ST_GeomFromText('{poly_wkt}'))",
|
| 4586 |
+
.open = "{", .close = "}")
|
| 4587 |
+
tryCatch(DBI::dbGetQuery(con, sql),
|
| 4588 |
+
error = function(e) { message("migrants_filtered poly: ", e$message); NULL })
|
| 4589 |
}
|
| 4590 |
+
})
|
| 4591 |
+
|
| 4592 |
+
output$migrants_treemap_cause <- renderPlot({
|
| 4593 |
+
df <- migrants_filtered()
|
| 4594 |
+
if (is.null(df) || nrow(df) == 0) return(NULL)
|
| 4595 |
+
df$cause_cat[is.na(df$cause_cat) | df$cause_cat == ""] <- "Other / unknown"
|
| 4596 |
+
agg <- aggregate(n_dead ~ cause_cat, data = df, FUN = function(x) sum(x, na.rm = TRUE))
|
| 4597 |
+
summary_df <- data.frame(label = agg$cause_cat,
|
| 4598 |
+
value = agg$n_dead,
|
| 4599 |
+
color = unname(CAUSE_PAL[agg$cause_cat]),
|
| 4600 |
+
stringsAsFactors = FALSE)
|
| 4601 |
+
treemap_plot(summary_df, CAUSE_PAL, base_size = 8)
|
| 4602 |
+
}, bg = "transparent", res = 110)
|
| 4603 |
+
|
| 4604 |
+
output$migrants_treemap_demo <- renderPlot({
|
| 4605 |
+
df <- migrants_filtered()
|
| 4606 |
+
if (is.null(df) || nrow(df) == 0) return(NULL)
|
| 4607 |
+
summary_df <- data.frame(
|
| 4608 |
+
label = c("Children", "Female", "Male"),
|
| 4609 |
+
value = c(sum(df$n_children, na.rm = TRUE),
|
| 4610 |
+
sum(df$n_females, na.rm = TRUE),
|
| 4611 |
+
sum(df$n_males, na.rm = TRUE)),
|
| 4612 |
+
color = unname(DEMO_PAL),
|
| 4613 |
+
stringsAsFactors = FALSE
|
| 4614 |
+
)
|
| 4615 |
+
treemap_plot(summary_df, DEMO_PAL, base_size = 8)
|
| 4616 |
+
}, bg = "transparent", res = 110)
|
| 4617 |
|
| 4618 |
# Shared helper: temporal bar chart by year for point event tables
|
| 4619 |
make_temporal_yr <- function(tbl, yr_col, line_color) {
|
|
|
|
| 4728 |
))
|
| 4729 |
}, ignoreInit = TRUE)
|
| 4730 |
|
| 4731 |
+
# Generic ym-window re-aggregation for polygon temporal layers. Each entry
|
| 4732 |
+
# below pairs a layer id with: the lazy-load key (so we don't try to swap a
|
| 4733 |
+
# source on a layer that hasn't been added yet) and the SQL template that
|
| 4734 |
+
# rebuilds the per-muni aggregate filtered to [ym_lo, ym_hi]. The
|
| 4735 |
+
# short-circuit, the JS debounce, and the set_source swap from the PAM fix
|
| 4736 |
+
# all apply uniformly.
|
| 4737 |
+
TEMPORAL_POLY_SOURCES <- list(
|
| 4738 |
+
"pam-choro" = list(lazy_key = "pam", sql = "src_pam_ym"),
|
| 4739 |
+
"fosas-choro" = list(lazy_key = "fosas", sql = "src_fosas_ym")
|
| 4740 |
+
)
|
| 4741 |
observeEvent(input$temporal_filter, {
|
| 4742 |
pl <- input$temporal_filter
|
| 4743 |
if (is.null(pl)) return()
|
| 4744 |
active <- pl$active %||% character(0)
|
|
|
|
| 4745 |
ym_lo <- pl$start %||% TEMPORAL_RANGE[1]
|
| 4746 |
ym_hi <- pl$end %||% TEMPORAL_RANGE[2]
|
| 4747 |
+
for (lid in intersect(active, names(TEMPORAL_POLY_SOURCES))) {
|
| 4748 |
+
spec <- TEMPORAL_POLY_SOURCES[[lid]]
|
| 4749 |
+
if (!isTRUE(lazy_added[[spec$lazy_key]])) next
|
| 4750 |
+
# Full-range = unfiltered layer already on the map IS the answer.
|
| 4751 |
+
df_cache <- .APP_CACHE$temporal[[lid]]
|
| 4752 |
+
data_lo <- if (!is.null(df_cache)) min(df_cache$ym) else TEMPORAL_RANGE[1]
|
| 4753 |
+
data_hi <- if (!is.null(df_cache)) max(df_cache$ym) else TEMPORAL_RANGE[2]
|
| 4754 |
+
if (ym_lo <= data_lo && ym_hi >= data_hi) next
|
| 4755 |
+
sf_data <- tryCatch({
|
| 4756 |
+
DBI::dbGetQuery(con,
|
| 4757 |
+
web_sql(spec$sql, ym_lo = ym_lo, ym_hi = ym_hi)) |>
|
| 4758 |
+
sf::st_as_sf(wkt = "wkt", crs = 4326) |>
|
| 4759 |
+
dplyr::select(-wkt)
|
| 4760 |
+
}, error = function(e) {
|
| 4761 |
+
message(lid, " ym re-query: ", e$message); NULL
|
| 4762 |
+
})
|
| 4763 |
+
if (is.null(sf_data)) next
|
| 4764 |
+
# set_source swaps the source data on the existing layer without
|
| 4765 |
+
# touching the paint expression / tooltip -- avoids the source-already-
|
| 4766 |
+
# exists collision that the previous remove + re-add pattern caused.
|
| 4767 |
+
maplibre_proxy("map") |>
|
| 4768 |
+
set_source(layer_id = lid, source = sf_data)
|
| 4769 |
+
}
|
|
|
|
|
|
|
|
|
|
| 4770 |
}, ignoreNULL = TRUE, ignoreInit = TRUE)
|
| 4771 |
|
| 4772 |
# --- PAM temporal plot (irregular migration events over time) ---------------
|
|
|
|
| 4878 |
# (otherwise hidden outputs stay suspended and read "---"). Cost: one extra
|
| 4879 |
# render per output per session -- negligible (cached strings + small plots).
|
| 4880 |
for (.oid in c(
|
| 4881 |
+
"temporal_pam", "migrants_treemap_cause", "migrants_treemap_demo",
|
| 4882 |
+
"temporal_ocved", "temporal_ged",
|
| 4883 |
"breakdown_giim", "breakdown_infra",
|
| 4884 |
"n_incidents", "n_fatalities", "n_graves", "n_facilities", "n_events",
|
| 4885 |
"ocved_n", "n_ged", "n_mun", "n_desap", "n_pam", "n_fosas", "n_cuerpos",
|