Upload 2 files
Browse files- depth_interactive_map_alpha.R +392 -71
- styles.css +134 -16
depth_interactive_map_alpha.R
CHANGED
|
@@ -321,17 +321,72 @@ WEB_SQL_TEMPLATES <- list(
|
|
| 321 |
"SELECT wkt FROM state_polygons WHERE CVE_ENT = '{cve_ent}' LIMIT 1",
|
| 322 |
|
| 323 |
temporal_mig_all =
|
| 324 |
-
"SELECT {yr_expr} AS yr,
|
|
|
|
|
|
|
|
|
|
| 325 |
WHERE incident_date_ts IS NOT NULL
|
| 326 |
GROUP BY yr HAVING yr > 2000 AND yr <= 2030 ORDER BY yr",
|
| 327 |
|
| 328 |
temporal_mig_sel =
|
| 329 |
-
"SELECT {yr_expr} AS yr,
|
|
|
|
|
|
|
|
|
|
| 330 |
WHERE incident_date_ts IS NOT NULL
|
| 331 |
AND lon IS NOT NULL AND lat IS NOT NULL
|
| 332 |
AND ST_Within(ST_Point(lon, lat), ST_GeomFromText('{poly_wkt}'))
|
| 333 |
GROUP BY yr HAVING yr > 2000 AND yr <= 2030 ORDER BY yr",
|
| 334 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
temporal_yr_all =
|
| 336 |
"SELECT {yr_expr} AS yr, COUNT(*) AS n FROM {tbl}
|
| 337 |
WHERE {yr_col} IS NOT NULL
|
|
@@ -548,7 +603,7 @@ legend_bounds <- function(pal_name, unit = "") {
|
|
| 548 |
#######################################################################################
|
| 549 |
|
| 550 |
.NOMINATIM_CACHE <- new.env(parent = emptyenv())
|
| 551 |
-
.NOMINATIM_UA <- "DEPTH-RiskMap/
|
| 552 |
|
| 553 |
nominatim_search <- function(q, limit = 6, country = "mx") {
|
| 554 |
q <- trimws(q %||% "")
|
|
@@ -695,7 +750,7 @@ WEB_SPEC <- list(
|
|
| 695 |
legend = list(type = "ramp", pal = "desap", unit = " /100k")),
|
| 696 |
list(kind = "layer", value = "fosas-choro",
|
| 697 |
label = "Clandestine burial pits by municipality",
|
| 698 |
-
tooltip = "Number of clandestine burial pits (fosas) and bodies found per municipality. Source:
|
| 699 |
legend = list(type = "ramp", pal = "fosas", unit = " pits")),
|
| 700 |
list(kind = "radios", group = "fosas"),
|
| 701 |
list(kind = "layer", value = "graves-pts",
|
|
@@ -775,8 +830,14 @@ WEB_SPEC <- list(
|
|
| 775 |
)),
|
| 776 |
list(title = "Enforced Disappearances (RNPDNO)", kind = "stat",
|
| 777 |
stats = list(list(id = "n_desap", label = "Registered disappearances", color = COL_RISK))),
|
| 778 |
-
|
|
|
|
| 779 |
stats = list(list(id = "n_graves", label = "Clandestine burial sites", color = COL_RED))),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 780 |
list(title = "Organised Crime Violence (OCVED 2.0)", kind = "stat", chart = "temporal_ocved",
|
| 781 |
stats = list(list(id = "ocved_n", label = "Recorded violence events", color = COL_AMBER))),
|
| 782 |
list(title = "Armed Conflict Events (UCDP-GED)", kind = "stat", chart = "temporal_ged",
|
|
@@ -784,12 +845,15 @@ WEB_SPEC <- list(
|
|
| 784 |
list(title = "Criminal Incidence by Municipality", kind = "munlist")
|
| 785 |
)),
|
| 786 |
list(id = "exposure", label = "Exposure", open = FALSE, cards = list(
|
| 787 |
-
list(title = "Irregular Migration Events (PAM)", kind = "stat",
|
| 788 |
-
stats = list(list(id = "n_pam", label = "Recorded irregular migration events", color = COL_COUNT)))
|
|
|
|
|
|
|
| 789 |
)),
|
| 790 |
list(id = "vulnerability", label = "Vulnerability", open = FALSE, cards = list(
|
| 791 |
-
list(title = "Migrant Support Facilities", kind = "stat",
|
| 792 |
-
stats = list(list(id = "n_facilities", label = "Active support facilities", color = COL_GREEN))
|
|
|
|
| 793 |
))
|
| 794 |
)
|
| 795 |
)
|
|
@@ -839,12 +903,15 @@ LUCIDE_JS <- tags$head(
|
|
| 839 |
Shiny.setInputValue('grid_nivel', '6', { priority: 'event' });
|
| 840 |
}, 300);
|
| 841 |
|
| 842 |
-
//
|
| 843 |
-
//
|
| 844 |
var navTries = 0;
|
| 845 |
var navTimer = setInterval(function() {
|
| 846 |
navTries++;
|
| 847 |
-
if (
|
|
|
|
|
|
|
|
|
|
| 848 |
}, 100);
|
| 849 |
});
|
| 850 |
|
|
@@ -1037,7 +1104,7 @@ LUCIDE_JS <- tags$head(
|
|
| 1037 |
}
|
| 1038 |
}
|
| 1039 |
|
| 1040 |
-
// -- Side panel toggle (sigma / sparkles tab rail) ---------------
|
| 1041 |
function togglePanel(name) {
|
| 1042 |
var panel = document.getElementById('panel-' + name);
|
| 1043 |
var tab = document.getElementById('tab-' + name);
|
|
@@ -1047,12 +1114,13 @@ LUCIDE_JS <- tags$head(
|
|
| 1047 |
panel.classList.toggle('is-collapsed', !willOpen);
|
| 1048 |
tab.classList.toggle('is-active', willOpen);
|
| 1049 |
tab.setAttribute('aria-selected', String(willOpen));
|
| 1050 |
-
|
| 1051 |
-
var
|
| 1052 |
-
|
| 1053 |
-
|
|
|
|
| 1054 |
if (stack) stack.classList.toggle('is-empty', !anyOpen);
|
| 1055 |
-
// Re-render Lucide icons in case the
|
| 1056 |
if (willOpen && window.lucide) {
|
| 1057 |
setTimeout(function() { lucide.createIcons(); }, 30);
|
| 1058 |
}
|
|
@@ -1079,41 +1147,49 @@ LUCIDE_JS <- tags$head(
|
|
| 1079 |
} catch(e) {}
|
| 1080 |
}
|
| 1081 |
|
| 1082 |
-
// --
|
| 1083 |
-
// mapgl mounts its
|
| 1084 |
-
//
|
| 1085 |
-
//
|
| 1086 |
-
|
| 1087 |
-
|
| 1088 |
-
var
|
| 1089 |
-
|
| 1090 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1091 |
'.mapbox-gl-draw_polygon, .mapbox-gl-draw_rectangle, .mapbox-gl-draw_radius, .mapbox-gl-draw_line_string'
|
| 1092 |
);
|
| 1093 |
-
if (!
|
| 1094 |
-
var
|
| 1095 |
-
|
| 1096 |
-
|
| 1097 |
-
btn.dataset.navMoved = '1';
|
| 1098 |
-
btn.classList.add('nav-btn');
|
| 1099 |
-
if (!btn.getAttribute('aria-label')) {
|
| 1100 |
-
btn.setAttribute('aria-label', btn.getAttribute('title') || 'Draw');
|
| 1101 |
-
}
|
| 1102 |
-
if (firstCustom && firstCustom.parentNode === nav) {
|
| 1103 |
-
nav.insertBefore(btn, firstCustom);
|
| 1104 |
-
} else {
|
| 1105 |
-
nav.appendChild(btn);
|
| 1106 |
-
}
|
| 1107 |
});
|
| 1108 |
-
// Hide now-empty mapgl ctrl-group containers on the map
|
| 1109 |
-
mapEl.querySelectorAll('.maplibregl-ctrl-top-right .maplibregl-ctrl-group')
|
| 1110 |
-
.forEach(function(g) {
|
| 1111 |
-
var hasVisible = false;
|
| 1112 |
-
g.querySelectorAll('button').forEach(function(b) {
|
| 1113 |
-
if (b.offsetParent !== null && !b.dataset.navMoved) hasVisible = true;
|
| 1114 |
-
});
|
| 1115 |
-
if (!hasVisible) g.style.display = 'none';
|
| 1116 |
-
});
|
| 1117 |
return true;
|
| 1118 |
}
|
| 1119 |
|
|
@@ -1501,18 +1577,20 @@ dp_stat <- function(val_id, label, color = "#1e293b") {
|
|
| 1501 |
)
|
| 1502 |
}
|
| 1503 |
|
| 1504 |
-
ind_card <- function(title, body, chart_id = NULL, note = NULL, soon = FALSE
|
| 1505 |
-
|
|
|
|
|
|
|
| 1506 |
div(class = "ind-card-hdr",
|
| 1507 |
span(class = "ind-card-title", title),
|
| 1508 |
if (soon) tags$span(class = "acc-coming-tag", "Soon")
|
| 1509 |
),
|
| 1510 |
-
div(class = "ind-card-body", body),
|
| 1511 |
-
if (!is.null(note))
|
| 1512 |
-
div(class = "ind-card-note", note),
|
| 1513 |
if (!is.null(chart_id))
|
| 1514 |
div(class = "ind-card-chart",
|
| 1515 |
-
plotOutput(chart_id, width = "100%", height =
|
|
|
|
|
|
|
| 1516 |
)
|
| 1517 |
}
|
| 1518 |
|
|
@@ -1592,9 +1670,17 @@ render_panel_card <- function(card) {
|
|
| 1592 |
uiOutput("mun_list")),
|
| 1593 |
stat = tagList(
|
| 1594 |
lapply(card$stats, function(s)
|
| 1595 |
-
dp_stat(s$id, s$label, s$color %||% "#1e293b")))
|
|
|
|
| 1596 |
)
|
| 1597 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1598 |
}
|
| 1599 |
|
| 1600 |
render_panel_groups <- function() {
|
|
@@ -1656,9 +1742,37 @@ ui <- page_fillable(
|
|
| 1656 |
div(class = "nav-divider"),
|
| 1657 |
|
| 1658 |
# Selection / drawing tool group.
|
| 1659 |
-
#
|
| 1660 |
-
#
|
| 1661 |
div(class = "nav-group", id = "nav-tools-select",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1662 |
tags$button(id = "btn_mode_click", class = "nav-btn",
|
| 1663 |
type = "button",
|
| 1664 |
`aria-label` = "Click a municipality to analyse it",
|
|
@@ -1866,6 +1980,71 @@ ui <- page_fillable(
|
|
| 1866 |
)
|
| 1867 |
),
|
| 1868 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1869 |
# ---- Assistance panel (collapsed by default) -----------------------
|
| 1870 |
tags$section(class = "side-panel side-panel--assist is-collapsed",
|
| 1871 |
id = "panel-assist",
|
|
@@ -1923,7 +2102,18 @@ ui <- page_fillable(
|
|
| 1923 |
`aria-label` = "Toggle Transit Migration Risk Index",
|
| 1924 |
onclick = "togglePanel('tmri')",
|
| 1925 |
HTML('<i data-lucide="sigma" style="width:18px;height:18px;stroke-width:2" aria-hidden="true"></i>'),
|
| 1926 |
-
tags$span(class = "side-rail-tab-label", "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1927 |
tags$button(class = "side-rail-tab",
|
| 1928 |
id = "tab-assist",
|
| 1929 |
type = "button",
|
|
@@ -1983,6 +2173,7 @@ server <- function(input, output, session) {
|
|
| 1983 |
n_events = "---", n_mun = "---",
|
| 1984 |
n_desap = "---", n_pam = "---",
|
| 1985 |
ocved_n = "---", n_ged = "---",
|
|
|
|
| 1986 |
sub_incidents = " ", sub_fatalities = " ",
|
| 1987 |
mun_df = NULL,
|
| 1988 |
dl_list = NULL,
|
|
@@ -2213,6 +2404,11 @@ server <- function(input, output, session) {
|
|
| 2213 |
rv$n_desap <- fmt_n(mun_agg$desap_n)
|
| 2214 |
rv$n_pam <- fmt_n(mun_agg$pam_n)
|
| 2215 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2216 |
rv$mun_df <- tryCatch(DBI::dbGetQuery(con, web_sql("mun_table")),
|
| 2217 |
error = function(e) data.frame())
|
| 2218 |
}
|
|
@@ -2283,6 +2479,23 @@ server <- function(input, output, session) {
|
|
| 2283 |
rv$n_pam <- fmt_n(sum(mun_df$pam_n, na.rm = TRUE))
|
| 2284 |
rv$mun_df <- mun_df
|
| 2285 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2286 |
rv$dl_list <- list(
|
| 2287 |
migrants = migrants_df, graves = graves_df,
|
| 2288 |
infra = infra_df, ocved = ocved_df,
|
|
@@ -2936,6 +3149,8 @@ server <- function(input, output, session) {
|
|
| 2936 |
output$n_mun <- renderText(rv$n_mun)
|
| 2937 |
output$n_desap <- renderText(rv$n_desap)
|
| 2938 |
output$n_pam <- renderText(rv$n_pam)
|
|
|
|
|
|
|
| 2939 |
output$sub_incidents <- renderText(rv$sub_incidents)
|
| 2940 |
output$sub_fatalities <- renderText(rv$sub_fatalities)
|
| 2941 |
|
|
@@ -2977,7 +3192,7 @@ server <- function(input, output, session) {
|
|
| 2977 |
|
| 2978 |
output$temporal_plot <- renderPlot({
|
| 2979 |
req(con)
|
| 2980 |
-
#
|
| 2981 |
yr_expr_ts <- "CAST(date_part('year', incident_date_ts) AS INTEGER)"
|
| 2982 |
yr_expr_raw <- "CAST(date_part('year', CAST(incident_date AS TIMESTAMP)) AS INTEGER)"
|
| 2983 |
df_all <- tryCatch(
|
|
@@ -2995,17 +3210,27 @@ server <- function(input, output, session) {
|
|
| 2995 |
yr_expr = yr_expr_ts, poly_wkt = filter_wkt)), error = function(e) NULL)
|
| 2996 |
} else NULL
|
| 2997 |
|
| 2998 |
-
|
| 2999 |
-
|
| 3000 |
-
|
| 3001 |
-
|
| 3002 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3003 |
if (!is.null(df_sel) && nrow(df_sel) > 0) {
|
| 3004 |
p <- p +
|
| 3005 |
-
geom_line(data
|
| 3006 |
-
color =
|
| 3007 |
-
geom_point(data = df_sel, aes(
|
| 3008 |
-
color =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3009 |
fill = "white", stroke = 0.7)
|
| 3010 |
}
|
| 3011 |
p + theme_minimal(base_size = 8) +
|
|
@@ -3065,6 +3290,102 @@ server <- function(input, output, session) {
|
|
| 3065 |
output$temporal_ocved <- make_temporal_yr("ocved_events", "year", COL_AMBER)
|
| 3066 |
output$temporal_ged <- make_temporal_yr("ged_events", "year", COL_LIGHT_BLUE)
|
| 3067 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3068 |
output$dl_xlsx <- downloadHandler(
|
| 3069 |
filename = function()
|
| 3070 |
glue("depth_selection_{format(Sys.time(), '%Y%m%d_%H%M%S')}.xlsx"),
|
|
|
|
| 321 |
"SELECT wkt FROM state_polygons WHERE CVE_ENT = '{cve_ent}' LIMIT 1",
|
| 322 |
|
| 323 |
temporal_mig_all =
|
| 324 |
+
"SELECT {yr_expr} AS yr,
|
| 325 |
+
COUNT(*) AS n,
|
| 326 |
+
COALESCE(SUM(CAST(n_dead AS INTEGER)), 0) AS n_dead
|
| 327 |
+
FROM migrants
|
| 328 |
WHERE incident_date_ts IS NOT NULL
|
| 329 |
GROUP BY yr HAVING yr > 2000 AND yr <= 2030 ORDER BY yr",
|
| 330 |
|
| 331 |
temporal_mig_sel =
|
| 332 |
+
"SELECT {yr_expr} AS yr,
|
| 333 |
+
COUNT(*) AS n,
|
| 334 |
+
COALESCE(SUM(CAST(n_dead AS INTEGER)), 0) AS n_dead
|
| 335 |
+
FROM migrants
|
| 336 |
WHERE incident_date_ts IS NOT NULL
|
| 337 |
AND lon IS NOT NULL AND lat IS NOT NULL
|
| 338 |
AND ST_Within(ST_Point(lon, lat), ST_GeomFromText('{poly_wkt}'))
|
| 339 |
GROUP BY yr HAVING yr > 2000 AND yr <= 2030 ORDER BY yr",
|
| 340 |
|
| 341 |
+
temporal_pam_all =
|
| 342 |
+
"SELECT CAST(date_part('year', date) AS INTEGER) AS yr,
|
| 343 |
+
COALESCE(SUM(pam_n), 0) AS n
|
| 344 |
+
FROM dim_pam_mun
|
| 345 |
+
WHERE date IS NOT NULL
|
| 346 |
+
GROUP BY yr HAVING yr > 2000 AND yr <= 2030 ORDER BY yr",
|
| 347 |
+
|
| 348 |
+
temporal_pam_sel =
|
| 349 |
+
"SELECT CAST(date_part('year', date) AS INTEGER) AS yr,
|
| 350 |
+
COALESCE(SUM(pam_n), 0) AS n
|
| 351 |
+
FROM dim_pam_mun
|
| 352 |
+
WHERE date IS NOT NULL
|
| 353 |
+
AND CVEGEO IN ({cvegeo_list})
|
| 354 |
+
GROUP BY yr HAVING yr > 2000 AND yr <= 2030 ORDER BY yr",
|
| 355 |
+
|
| 356 |
+
donut_giim =
|
| 357 |
+
"SELECT COALESCE(giim_mun, 'No aplica') AS cat,
|
| 358 |
+
COUNT(*) AS n
|
| 359 |
+
FROM dim_remesas_mun
|
| 360 |
+
GROUP BY cat ORDER BY n DESC",
|
| 361 |
+
|
| 362 |
+
donut_infra =
|
| 363 |
+
"SELECT
|
| 364 |
+
CASE
|
| 365 |
+
WHEN category = 'Shelter' THEN 'Shelter'
|
| 366 |
+
WHEN LOWER(category) IN ('food','restaurant','lodging') THEN 'Food & Lodging'
|
| 367 |
+
WHEN LOWER(category) IN ('government','embassy') THEN 'Government'
|
| 368 |
+
WHEN category = 'Legal/NGO' OR LOWER(category) = 'charity' THEN 'Legal & Aid'
|
| 369 |
+
WHEN LOWER(category) = 'police' THEN 'Police'
|
| 370 |
+
WHEN LOWER(category) IN ('hospital','medical') THEN 'Health'
|
| 371 |
+
WHEN LOWER(category) IN ('religious','place-of-worship') THEN 'Religious'
|
| 372 |
+
ELSE 'Other'
|
| 373 |
+
END AS cat,
|
| 374 |
+
COUNT(*) AS n
|
| 375 |
+
FROM support_infrastructure
|
| 376 |
+
WHERE category IS NOT NULL
|
| 377 |
+
GROUP BY cat ORDER BY n DESC",
|
| 378 |
+
|
| 379 |
+
fosas_nat =
|
| 380 |
+
"SELECT COALESCE(SUM(fosas_total), 0) AS fosas_n,
|
| 381 |
+
COALESCE(SUM(cuerpos_total), 0) AS cuerpos_n
|
| 382 |
+
FROM dim_fosas_mun",
|
| 383 |
+
|
| 384 |
+
fosas_sel =
|
| 385 |
+
"SELECT COALESCE(SUM(fosas_total), 0) AS fosas_n,
|
| 386 |
+
COALESCE(SUM(cuerpos_total), 0) AS cuerpos_n
|
| 387 |
+
FROM dim_fosas_mun
|
| 388 |
+
WHERE CVEGEO IN ({cvegeo_list})",
|
| 389 |
+
|
| 390 |
temporal_yr_all =
|
| 391 |
"SELECT {yr_expr} AS yr, COUNT(*) AS n FROM {tbl}
|
| 392 |
WHERE {yr_col} IS NOT NULL
|
|
|
|
| 603 |
#######################################################################################
|
| 604 |
|
| 605 |
.NOMINATIM_CACHE <- new.env(parent = emptyenv())
|
| 606 |
+
.NOMINATIM_UA <- "DEPTH-RiskMap/0.001"
|
| 607 |
|
| 608 |
nominatim_search <- function(q, limit = 6, country = "mx") {
|
| 609 |
q <- trimws(q %||% "")
|
|
|
|
| 750 |
legend = list(type = "ramp", pal = "desap", unit = " /100k")),
|
| 751 |
list(kind = "layer", value = "fosas-choro",
|
| 752 |
label = "Clandestine burial pits by municipality",
|
| 753 |
+
tooltip = "Number of clandestine burial pits (fosas) and bodies found per municipality. Source: Plataforma Fosas Clandestinas.",
|
| 754 |
legend = list(type = "ramp", pal = "fosas", unit = " pits")),
|
| 755 |
list(kind = "radios", group = "fosas"),
|
| 756 |
list(kind = "layer", value = "graves-pts",
|
|
|
|
| 830 |
)),
|
| 831 |
list(title = "Enforced Disappearances (RNPDNO)", kind = "stat",
|
| 832 |
stats = list(list(id = "n_desap", label = "Registered disappearances", color = COL_RISK))),
|
| 833 |
+
# Side-by-side burial pair: graves (points) + fosas (municipality sums)
|
| 834 |
+
list(title = "Burial Sites", kind = "stat", half = TRUE,
|
| 835 |
stats = list(list(id = "n_graves", label = "Clandestine burial sites", color = COL_RED))),
|
| 836 |
+
list(title = "Burial Pits (Fosas)", kind = "stat", half = TRUE,
|
| 837 |
+
stats = list(
|
| 838 |
+
list(id = "n_fosas", label = "Clandestine pits", color = COL_RED),
|
| 839 |
+
list(id = "n_cuerpos", label = "Bodies recovered", color = COL_AMBER)
|
| 840 |
+
)),
|
| 841 |
list(title = "Organised Crime Violence (OCVED 2.0)", kind = "stat", chart = "temporal_ocved",
|
| 842 |
stats = list(list(id = "ocved_n", label = "Recorded violence events", color = COL_AMBER))),
|
| 843 |
list(title = "Armed Conflict Events (UCDP-GED)", kind = "stat", chart = "temporal_ged",
|
|
|
|
| 845 |
list(title = "Criminal Incidence by Municipality", kind = "munlist")
|
| 846 |
)),
|
| 847 |
list(id = "exposure", label = "Exposure", open = FALSE, cards = list(
|
| 848 |
+
list(title = "Irregular Migration Events (PAM)", kind = "stat", chart = "temporal_pam",
|
| 849 |
+
stats = list(list(id = "n_pam", label = "Recorded irregular migration events", color = COL_COUNT))),
|
| 850 |
+
list(title = "Migration Intensity (GIIM)", kind = "donut", chart = "donut_giim",
|
| 851 |
+
note = "Share of Mexican municipalities by intensity of migration to the USA.")
|
| 852 |
)),
|
| 853 |
list(id = "vulnerability", label = "Vulnerability", open = FALSE, cards = list(
|
| 854 |
+
list(title = "Migrant Support Facilities", kind = "stat", chart = "donut_infra",
|
| 855 |
+
stats = list(list(id = "n_facilities", label = "Active support facilities", color = COL_GREEN)),
|
| 856 |
+
note = "Composition of support facilities by type (national).")
|
| 857 |
))
|
| 858 |
)
|
| 859 |
)
|
|
|
|
| 903 |
Shiny.setInputValue('grid_nivel', '6', { priority: 'event' });
|
| 904 |
}, 300);
|
| 905 |
|
| 906 |
+
// Hook up the native draw buttons so our custom ones can mirror their
|
| 907 |
+
// active state. Retries until mapgl has mounted the draw control.
|
| 908 |
var navTries = 0;
|
| 909 |
var navTimer = setInterval(function() {
|
| 910 |
navTries++;
|
| 911 |
+
if (watchNativeDrawButtons() || navTries >= 40) {
|
| 912 |
+
clearInterval(navTimer);
|
| 913 |
+
syncDrawButtonStates();
|
| 914 |
+
}
|
| 915 |
}, 100);
|
| 916 |
});
|
| 917 |
|
|
|
|
| 1104 |
}
|
| 1105 |
}
|
| 1106 |
|
| 1107 |
+
// -- Side panel toggle (sigma / network / sparkles tab rail) ---------------
|
| 1108 |
function togglePanel(name) {
|
| 1109 |
var panel = document.getElementById('panel-' + name);
|
| 1110 |
var tab = document.getElementById('tab-' + name);
|
|
|
|
| 1114 |
panel.classList.toggle('is-collapsed', !willOpen);
|
| 1115 |
tab.classList.toggle('is-active', willOpen);
|
| 1116 |
tab.setAttribute('aria-selected', String(willOpen));
|
| 1117 |
+
// Stack collapses to 0 width when ALL three panels are closed
|
| 1118 |
+
var anyOpen = ['tmri', 'risk', 'assist'].some(function(k) {
|
| 1119 |
+
var p = document.getElementById('panel-' + k);
|
| 1120 |
+
return p && !p.classList.contains('is-collapsed');
|
| 1121 |
+
});
|
| 1122 |
if (stack) stack.classList.toggle('is-empty', !anyOpen);
|
| 1123 |
+
// Re-render Lucide icons in case the panel mounted icons just now
|
| 1124 |
if (willOpen && window.lucide) {
|
| 1125 |
setTimeout(function() { lucide.createIcons(); }, 30);
|
| 1126 |
}
|
|
|
|
| 1147 |
} catch(e) {}
|
| 1148 |
}
|
| 1149 |
|
| 1150 |
+
// -- Custom draw buttons: proxy-click the (hidden) native mapgl buttons ----
|
| 1151 |
+
// mapgl mounts its native MapboxDraw buttons inside the map control group.
|
| 1152 |
+
// We hide them via CSS but keep them in the DOM so .click() still triggers
|
| 1153 |
+
// the underlying draw.changeMode() / draw mode behaviour. Our Lucide-icon
|
| 1154 |
+
// buttons in #nav-tools-select call drawMode() which forwards the click.
|
| 1155 |
+
function drawMode(modeKey) {
|
| 1156 |
+
var sel = '.mapbox-gl-draw_' + modeKey;
|
| 1157 |
+
var nativeBtn = document.querySelector(sel);
|
| 1158 |
+
if (!nativeBtn) {
|
| 1159 |
+
// Native draw control not yet mounted -- retry briefly
|
| 1160 |
+
setTimeout(function() {
|
| 1161 |
+
var b = document.querySelector(sel);
|
| 1162 |
+
if (b) { b.click(); setTimeout(syncDrawButtonStates, 60); }
|
| 1163 |
+
}, 200);
|
| 1164 |
+
return;
|
| 1165 |
+
}
|
| 1166 |
+
nativeBtn.click();
|
| 1167 |
+
// Sync our custom buttons' .active state from the natives after click
|
| 1168 |
+
setTimeout(syncDrawButtonStates, 60);
|
| 1169 |
+
}
|
| 1170 |
+
|
| 1171 |
+
function syncDrawButtonStates() {
|
| 1172 |
+
['polygon', 'rectangle', 'radius', 'line_string'].forEach(function(mode) {
|
| 1173 |
+
var native = document.querySelector('.mapbox-gl-draw_' + mode);
|
| 1174 |
+
var custom = document.querySelector('[data-draw=\"' + mode + '\"]');
|
| 1175 |
+
if (native && custom) {
|
| 1176 |
+
custom.classList.toggle('active', native.classList.contains('active'));
|
| 1177 |
+
}
|
| 1178 |
+
});
|
| 1179 |
+
}
|
| 1180 |
+
|
| 1181 |
+
// Watch native draw buttons so our custom state mirrors mode changes
|
| 1182 |
+
// triggered outside our buttons (e.g. when mapgl reverts to simple_select
|
| 1183 |
+
// after a polygon is completed).
|
| 1184 |
+
function watchNativeDrawButtons() {
|
| 1185 |
+
var natives = document.querySelectorAll(
|
| 1186 |
'.mapbox-gl-draw_polygon, .mapbox-gl-draw_rectangle, .mapbox-gl-draw_radius, .mapbox-gl-draw_line_string'
|
| 1187 |
);
|
| 1188 |
+
if (!natives.length) return false;
|
| 1189 |
+
var obs = new MutationObserver(syncDrawButtonStates);
|
| 1190 |
+
natives.forEach(function(b) {
|
| 1191 |
+
obs.observe(b, { attributes: true, attributeFilter: ['class'] });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1192 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1193 |
return true;
|
| 1194 |
}
|
| 1195 |
|
|
|
|
| 1577 |
)
|
| 1578 |
}
|
| 1579 |
|
| 1580 |
+
ind_card <- function(title, body, chart_id = NULL, note = NULL, soon = FALSE,
|
| 1581 |
+
half = FALSE, chart_height = "55px") {
|
| 1582 |
+
cls <- paste("ind-card", if (isTRUE(half)) "ind-card--half" else "")
|
| 1583 |
+
div(class = cls,
|
| 1584 |
div(class = "ind-card-hdr",
|
| 1585 |
span(class = "ind-card-title", title),
|
| 1586 |
if (soon) tags$span(class = "acc-coming-tag", "Soon")
|
| 1587 |
),
|
| 1588 |
+
if (!is.null(body)) div(class = "ind-card-body", body),
|
|
|
|
|
|
|
| 1589 |
if (!is.null(chart_id))
|
| 1590 |
div(class = "ind-card-chart",
|
| 1591 |
+
plotOutput(chart_id, width = "100%", height = chart_height)),
|
| 1592 |
+
if (!is.null(note))
|
| 1593 |
+
div(class = "ind-card-note", note)
|
| 1594 |
)
|
| 1595 |
}
|
| 1596 |
|
|
|
|
| 1670 |
uiOutput("mun_list")),
|
| 1671 |
stat = tagList(
|
| 1672 |
lapply(card$stats, function(s)
|
| 1673 |
+
dp_stat(s$id, s$label, s$color %||% "#1e293b"))),
|
| 1674 |
+
donut = NULL # donut renders entirely via plotOutput(chart_id)
|
| 1675 |
)
|
| 1676 |
+
# Donut cards are taller (the chart IS the body); other cards keep the
|
| 1677 |
+
# compact 55px sparkline. Half cards have slightly less chrome.
|
| 1678 |
+
chart_h <- if (identical(card$kind, "donut")) "180px" else "55px"
|
| 1679 |
+
ind_card(card$title, body,
|
| 1680 |
+
chart_id = card$chart,
|
| 1681 |
+
note = card$note,
|
| 1682 |
+
half = isTRUE(card$half),
|
| 1683 |
+
chart_height = chart_h)
|
| 1684 |
}
|
| 1685 |
|
| 1686 |
render_panel_groups <- function() {
|
|
|
|
| 1742 |
div(class = "nav-divider"),
|
| 1743 |
|
| 1744 |
# Selection / drawing tool group.
|
| 1745 |
+
# Custom Lucide buttons proxy-click the (hidden) native mapgl draw buttons
|
| 1746 |
+
# via drawMode() in JS, so the look is consistent with the rest of the nav.
|
| 1747 |
div(class = "nav-group", id = "nav-tools-select",
|
| 1748 |
+
tags$button(id = "btn_draw_polygon", class = "nav-btn",
|
| 1749 |
+
type = "button",
|
| 1750 |
+
`data-draw` = "polygon",
|
| 1751 |
+
`aria-label` = "Lasso / freehand polygon",
|
| 1752 |
+
title = "Lasso (freehand polygon)",
|
| 1753 |
+
onclick = "drawMode('polygon')",
|
| 1754 |
+
HTML('<i data-lucide="lasso" style="width:16px;height:16px;stroke-width:2"></i>')),
|
| 1755 |
+
tags$button(id = "btn_draw_rectangle", class = "nav-btn",
|
| 1756 |
+
type = "button",
|
| 1757 |
+
`data-draw` = "rectangle",
|
| 1758 |
+
`aria-label` = "Rectangle selection",
|
| 1759 |
+
title = "Rectangle",
|
| 1760 |
+
onclick = "drawMode('rectangle')",
|
| 1761 |
+
HTML('<i data-lucide="square-dashed" style="width:16px;height:16px;stroke-width:2"></i>')),
|
| 1762 |
+
tags$button(id = "btn_draw_radius", class = "nav-btn",
|
| 1763 |
+
type = "button",
|
| 1764 |
+
`data-draw` = "radius",
|
| 1765 |
+
`aria-label` = "Circle / radius selection",
|
| 1766 |
+
title = "Circle (radius)",
|
| 1767 |
+
onclick = "drawMode('radius')",
|
| 1768 |
+
HTML('<i data-lucide="circle-dashed" style="width:16px;height:16px;stroke-width:2"></i>')),
|
| 1769 |
+
tags$button(id = "btn_draw_line", class = "nav-btn",
|
| 1770 |
+
type = "button",
|
| 1771 |
+
`data-draw` = "line_string",
|
| 1772 |
+
`aria-label` = "Line selection",
|
| 1773 |
+
title = "Line",
|
| 1774 |
+
onclick = "drawMode('line_string')",
|
| 1775 |
+
HTML('<i data-lucide="spline" style="width:16px;height:16px;stroke-width:2"></i>')),
|
| 1776 |
tags$button(id = "btn_mode_click", class = "nav-btn",
|
| 1777 |
type = "button",
|
| 1778 |
`aria-label` = "Click a municipality to analyse it",
|
|
|
|
| 1980 |
)
|
| 1981 |
),
|
| 1982 |
|
| 1983 |
+
# ---- Risk Index Adjustments panel (collapsed by default) ------------
|
| 1984 |
+
tags$section(class = "side-panel side-panel--risk is-collapsed",
|
| 1985 |
+
id = "panel-risk",
|
| 1986 |
+
role = "region",
|
| 1987 |
+
`aria-label` = "Risk Index Adjustments",
|
| 1988 |
+
div(class = "side-panel-header",
|
| 1989 |
+
tags$span(class = "side-panel-icon",
|
| 1990 |
+
HTML('<i data-lucide="network" style="width:18px;height:18px;stroke-width:2" aria-hidden="true"></i>')),
|
| 1991 |
+
tags$h2(class = "side-panel-title", "Risk Adjustments"),
|
| 1992 |
+
tags$span(class = "side-panel-tag", "Idea"),
|
| 1993 |
+
tags$button(class = "side-panel-collapse",
|
| 1994 |
+
type = "button",
|
| 1995 |
+
onclick = "togglePanel('risk')",
|
| 1996 |
+
`aria-label` = "Collapse Risk Adjustments",
|
| 1997 |
+
title = "Collapse",
|
| 1998 |
+
HTML('<i data-lucide="panel-right-close" style="width:16px;height:16px;stroke-width:2" aria-hidden="true"></i>'))
|
| 1999 |
+
),
|
| 2000 |
+
div(class = "side-panel-body risk-body",
|
| 2001 |
+
div(class = "assist-card",
|
| 2002 |
+
tags$p(class = "assist-intro",
|
| 2003 |
+
"A long-shot idea -- ", tags$em("may not be tractable."),
|
| 2004 |
+
" The goal is to let researchers re-weight each component of the Transit Migration Risk Index and watch the composite score recompute over the map."
|
| 2005 |
+
),
|
| 2006 |
+
tags$div(class = "assist-status",
|
| 2007 |
+
HTML('<i data-lucide="flask-conical" style="width:12px;height:12px;stroke-width:2;color:var(--c-text-mid)" aria-hidden="true"></i>'),
|
| 2008 |
+
tags$span("Research preview -- weighting + composition under exploration")
|
| 2009 |
+
),
|
| 2010 |
+
tags$div(class = "risk-weights",
|
| 2011 |
+
tags$div(class = "risk-weight",
|
| 2012 |
+
tags$label(`for` = "rw-hazards",
|
| 2013 |
+
span(class = "risk-weight-label", "Hazards"),
|
| 2014 |
+
span(class = "risk-weight-val", "0.50")),
|
| 2015 |
+
tags$input(type = "range", id = "rw-hazards", min = "0", max = "1",
|
| 2016 |
+
step = "0.05", value = "0.50",
|
| 2017 |
+
`aria-label` = "Hazards weight",
|
| 2018 |
+
disabled = "disabled")
|
| 2019 |
+
),
|
| 2020 |
+
tags$div(class = "risk-weight",
|
| 2021 |
+
tags$label(`for` = "rw-exposure",
|
| 2022 |
+
span(class = "risk-weight-label", "Exposure"),
|
| 2023 |
+
span(class = "risk-weight-val", "0.30")),
|
| 2024 |
+
tags$input(type = "range", id = "rw-exposure", min = "0", max = "1",
|
| 2025 |
+
step = "0.05", value = "0.30",
|
| 2026 |
+
`aria-label` = "Exposure weight",
|
| 2027 |
+
disabled = "disabled")
|
| 2028 |
+
),
|
| 2029 |
+
tags$div(class = "risk-weight",
|
| 2030 |
+
tags$label(`for` = "rw-vuln",
|
| 2031 |
+
span(class = "risk-weight-label", "Vulnerability"),
|
| 2032 |
+
span(class = "risk-weight-val", "0.20")),
|
| 2033 |
+
tags$input(type = "range", id = "rw-vuln", min = "0", max = "1",
|
| 2034 |
+
step = "0.05", value = "0.20",
|
| 2035 |
+
`aria-label` = "Vulnerability weight",
|
| 2036 |
+
disabled = "disabled")
|
| 2037 |
+
)
|
| 2038 |
+
),
|
| 2039 |
+
tags$div(class = "assist-input-mock", role = "presentation",
|
| 2040 |
+
HTML('<i data-lucide="play" style="width:14px;height:14px;stroke-width:2;color:var(--c-text-dim)" aria-hidden="true"></i>'),
|
| 2041 |
+
tags$span("Recompute index"),
|
| 2042 |
+
tags$span(class = "assist-soon-tag", "Soon")
|
| 2043 |
+
)
|
| 2044 |
+
)
|
| 2045 |
+
)
|
| 2046 |
+
),
|
| 2047 |
+
|
| 2048 |
# ---- Assistance panel (collapsed by default) -----------------------
|
| 2049 |
tags$section(class = "side-panel side-panel--assist is-collapsed",
|
| 2050 |
id = "panel-assist",
|
|
|
|
| 2102 |
`aria-label` = "Toggle Transit Migration Risk Index",
|
| 2103 |
onclick = "togglePanel('tmri')",
|
| 2104 |
HTML('<i data-lucide="sigma" style="width:18px;height:18px;stroke-width:2" aria-hidden="true"></i>'),
|
| 2105 |
+
tags$span(class = "side-rail-tab-label", "Summary")),
|
| 2106 |
+
tags$button(class = "side-rail-tab",
|
| 2107 |
+
id = "tab-risk",
|
| 2108 |
+
type = "button",
|
| 2109 |
+
`data-panel` = "risk",
|
| 2110 |
+
role = "tab",
|
| 2111 |
+
`aria-controls` = "panel-risk",
|
| 2112 |
+
`aria-selected` = "false",
|
| 2113 |
+
`aria-label` = "Toggle Risk Index Adjustments",
|
| 2114 |
+
onclick = "togglePanel('risk')",
|
| 2115 |
+
HTML('<i data-lucide="network" style="width:18px;height:18px;stroke-width:2" aria-hidden="true"></i>'),
|
| 2116 |
+
tags$span(class = "side-rail-tab-label", "Adjust")),
|
| 2117 |
tags$button(class = "side-rail-tab",
|
| 2118 |
id = "tab-assist",
|
| 2119 |
type = "button",
|
|
|
|
| 2173 |
n_events = "---", n_mun = "---",
|
| 2174 |
n_desap = "---", n_pam = "---",
|
| 2175 |
ocved_n = "---", n_ged = "---",
|
| 2176 |
+
n_fosas = "---", n_cuerpos = "---",
|
| 2177 |
sub_incidents = " ", sub_fatalities = " ",
|
| 2178 |
mun_df = NULL,
|
| 2179 |
dl_list = NULL,
|
|
|
|
| 2404 |
rv$n_desap <- fmt_n(mun_agg$desap_n)
|
| 2405 |
rv$n_pam <- fmt_n(mun_agg$pam_n)
|
| 2406 |
|
| 2407 |
+
fosas <- tryCatch(DBI::dbGetQuery(con, web_sql("fosas_nat")),
|
| 2408 |
+
error = function(e) data.frame(fosas_n = 0, cuerpos_n = 0))
|
| 2409 |
+
rv$n_fosas <- fmt_n(fosas$fosas_n)
|
| 2410 |
+
rv$n_cuerpos <- fmt_n(fosas$cuerpos_n)
|
| 2411 |
+
|
| 2412 |
rv$mun_df <- tryCatch(DBI::dbGetQuery(con, web_sql("mun_table")),
|
| 2413 |
error = function(e) data.frame())
|
| 2414 |
}
|
|
|
|
| 2479 |
rv$n_pam <- fmt_n(sum(mun_df$pam_n, na.rm = TRUE))
|
| 2480 |
rv$mun_df <- mun_df
|
| 2481 |
|
| 2482 |
+
# Fosas / cuerpos sums for the selected municipalities (or zero if none).
|
| 2483 |
+
fosas_sum <- list(fosas_n = 0, cuerpos_n = 0)
|
| 2484 |
+
if (nrow(mun_df) > 0 && !is.null(mun_df$CVEGEO)) {
|
| 2485 |
+
cvegeos <- unique(mun_df$CVEGEO[nzchar(mun_df$CVEGEO)])
|
| 2486 |
+
if (length(cvegeos) > 0) {
|
| 2487 |
+
vals <- paste0("'", gsub("'", "''", cvegeos), "'", collapse = ",")
|
| 2488 |
+
fs <- tryCatch(DBI::dbGetQuery(con, web_sql("fosas_sel", cvegeo_list = vals)),
|
| 2489 |
+
error = function(e) NULL)
|
| 2490 |
+
if (!is.null(fs) && nrow(fs) == 1) {
|
| 2491 |
+
fosas_sum$fosas_n <- as.numeric(fs$fosas_n)
|
| 2492 |
+
fosas_sum$cuerpos_n <- as.numeric(fs$cuerpos_n)
|
| 2493 |
+
}
|
| 2494 |
+
}
|
| 2495 |
+
}
|
| 2496 |
+
rv$n_fosas <- fmt_n(fosas_sum$fosas_n)
|
| 2497 |
+
rv$n_cuerpos <- fmt_n(fosas_sum$cuerpos_n)
|
| 2498 |
+
|
| 2499 |
rv$dl_list <- list(
|
| 2500 |
migrants = migrants_df, graves = graves_df,
|
| 2501 |
infra = infra_df, ocved = ocved_df,
|
|
|
|
| 3149 |
output$n_mun <- renderText(rv$n_mun)
|
| 3150 |
output$n_desap <- renderText(rv$n_desap)
|
| 3151 |
output$n_pam <- renderText(rv$n_pam)
|
| 3152 |
+
output$n_fosas <- renderText(rv$n_fosas)
|
| 3153 |
+
output$n_cuerpos <- renderText(rv$n_cuerpos)
|
| 3154 |
output$sub_incidents <- renderText(rv$sub_incidents)
|
| 3155 |
output$sub_fatalities <- renderText(rv$sub_fatalities)
|
| 3156 |
|
|
|
|
| 3192 |
|
| 3193 |
output$temporal_plot <- renderPlot({
|
| 3194 |
req(con)
|
| 3195 |
+
# IOM temporal: incidents (COL_YEL) + confirmed dead (COL_RED) overlay
|
| 3196 |
yr_expr_ts <- "CAST(date_part('year', incident_date_ts) AS INTEGER)"
|
| 3197 |
yr_expr_raw <- "CAST(date_part('year', CAST(incident_date AS TIMESTAMP)) AS INTEGER)"
|
| 3198 |
df_all <- tryCatch(
|
|
|
|
| 3210 |
yr_expr = yr_expr_ts, poly_wkt = filter_wkt)), error = function(e) NULL)
|
| 3211 |
} else NULL
|
| 3212 |
|
| 3213 |
+
COL_INC <- COL_YEL # incidents
|
| 3214 |
+
COL_DEAD <- COL_RED # confirmed dead
|
| 3215 |
+
|
| 3216 |
+
p <- ggplot(df_all, aes(x = yr)) +
|
| 3217 |
+
# Incidents area + line
|
| 3218 |
+
geom_area(aes(y = n), fill = COL_INC, alpha = 0.10, color = NA) +
|
| 3219 |
+
geom_line(aes(y = n), color = COL_INC, linewidth = 0.7, alpha = 0.55) +
|
| 3220 |
+
# Confirmed dead overlay
|
| 3221 |
+
geom_line(aes(y = n_dead), color = COL_DEAD, linewidth = 0.7,
|
| 3222 |
+
alpha = 0.6, linetype = "22")
|
| 3223 |
if (!is.null(df_sel) && nrow(df_sel) > 0) {
|
| 3224 |
p <- p +
|
| 3225 |
+
geom_line(data = df_sel, aes(y = n),
|
| 3226 |
+
color = COL_INC, linewidth = 1.1) +
|
| 3227 |
+
geom_point(data = df_sel, aes(y = n),
|
| 3228 |
+
color = COL_INC, size = 1.5, shape = 21,
|
| 3229 |
+
fill = "white", stroke = 0.7) +
|
| 3230 |
+
geom_line(data = df_sel, aes(y = n_dead),
|
| 3231 |
+
color = COL_DEAD, linewidth = 1.0, linetype = "22") +
|
| 3232 |
+
geom_point(data = df_sel, aes(y = n_dead),
|
| 3233 |
+
color = COL_DEAD, size = 1.3, shape = 21,
|
| 3234 |
fill = "white", stroke = 0.7)
|
| 3235 |
}
|
| 3236 |
p + theme_minimal(base_size = 8) +
|
|
|
|
| 3290 |
output$temporal_ocved <- make_temporal_yr("ocved_events", "year", COL_AMBER)
|
| 3291 |
output$temporal_ged <- make_temporal_yr("ged_events", "year", COL_LIGHT_BLUE)
|
| 3292 |
|
| 3293 |
+
# --- PAM temporal plot (irregular migration events over time) ---------------
|
| 3294 |
+
# dim_pam_mun is municipality-aggregated by date. National = SUM over all
|
| 3295 |
+
# munis; selection = SUM over munis in rv$mun_df (CVEGEO list).
|
| 3296 |
+
output$temporal_pam <- renderPlot({
|
| 3297 |
+
req(con)
|
| 3298 |
+
df_all <- tryCatch(DBI::dbGetQuery(con, web_sql("temporal_pam_all")),
|
| 3299 |
+
error = function(e) { message("pam temporal: ", e$message); NULL })
|
| 3300 |
+
if (is.null(df_all) || nrow(df_all) == 0) return(NULL)
|
| 3301 |
+
|
| 3302 |
+
df_sel <- NULL
|
| 3303 |
+
if (!rv$is_national && !is.null(rv$mun_df) && nrow(rv$mun_df) > 0) {
|
| 3304 |
+
cvegeos <- unique(rv$mun_df$CVEGEO)
|
| 3305 |
+
cvegeos <- cvegeos[nzchar(cvegeos)]
|
| 3306 |
+
if (length(cvegeos) > 0) {
|
| 3307 |
+
# Quote and join into a SQL IN list (CVEGEOs are 5-char digit codes,
|
| 3308 |
+
# safe to interpolate after the nzchar/unique pass above).
|
| 3309 |
+
vals <- paste0("'", gsub("'", "''", cvegeos), "'", collapse = ",")
|
| 3310 |
+
df_sel <- tryCatch(
|
| 3311 |
+
DBI::dbGetQuery(con, web_sql("temporal_pam_sel", cvegeo_list = vals)),
|
| 3312 |
+
error = function(e) NULL
|
| 3313 |
+
)
|
| 3314 |
+
}
|
| 3315 |
+
}
|
| 3316 |
+
|
| 3317 |
+
COL_LINE <- COL_COUNT
|
| 3318 |
+
p <- ggplot(df_all, aes(x = yr, y = n)) +
|
| 3319 |
+
geom_area(fill = COL_LINE, alpha = 0.08, color = NA) +
|
| 3320 |
+
geom_line(color = COL_LINE, linewidth = 0.7, alpha = 0.45)
|
| 3321 |
+
if (!is.null(df_sel) && nrow(df_sel) > 0) {
|
| 3322 |
+
p <- p +
|
| 3323 |
+
geom_line(data = df_sel, aes(x = yr, y = n),
|
| 3324 |
+
color = COL_LINE, linewidth = 1.1) +
|
| 3325 |
+
geom_point(data = df_sel, aes(x = yr, y = n),
|
| 3326 |
+
color = COL_LINE, size = 1.6, shape = 21,
|
| 3327 |
+
fill = "white", stroke = 0.7)
|
| 3328 |
+
}
|
| 3329 |
+
p + theme_minimal(base_size = 8) +
|
| 3330 |
+
theme(
|
| 3331 |
+
plot.background = element_rect(fill = "transparent", color = NA),
|
| 3332 |
+
panel.background = element_rect(fill = "transparent", color = NA),
|
| 3333 |
+
panel.grid = element_blank(),
|
| 3334 |
+
panel.grid.major.y = element_line(color = "#e2e8f0", linewidth = 0.3),
|
| 3335 |
+
axis.title = element_blank(),
|
| 3336 |
+
axis.text.x = element_text(size = 7, color = "#64748b"),
|
| 3337 |
+
axis.text.y = element_blank(),
|
| 3338 |
+
axis.ticks = element_blank(),
|
| 3339 |
+
plot.margin = margin(1, 4, 1, 4)
|
| 3340 |
+
)
|
| 3341 |
+
}, bg = "transparent", res = 96)
|
| 3342 |
+
|
| 3343 |
+
# --- Donut helper -----------------------------------------------------------
|
| 3344 |
+
# Renders a transparent-background donut with category-coloured slices.
|
| 3345 |
+
# `df` columns: cat (chr) and n (numeric). `palette` is a named vector
|
| 3346 |
+
# mapping category -> hex.
|
| 3347 |
+
donut_plot <- function(df, palette, center_text = NULL) {
|
| 3348 |
+
if (is.null(df) || nrow(df) == 0) return(NULL)
|
| 3349 |
+
df$cat <- factor(df$cat, levels = names(palette))
|
| 3350 |
+
df <- df[!is.na(df$cat) & df$n > 0, , drop = FALSE]
|
| 3351 |
+
if (nrow(df) == 0) return(NULL)
|
| 3352 |
+
total <- sum(df$n)
|
| 3353 |
+
p <- ggplot(df, aes(x = 2, y = n, fill = cat)) +
|
| 3354 |
+
geom_col(width = 1, color = "#ffffff", linewidth = 0.4) +
|
| 3355 |
+
coord_polar(theta = "y", start = 0) +
|
| 3356 |
+
scale_fill_manual(values = palette, drop = FALSE, guide = "none") +
|
| 3357 |
+
xlim(0.4, 2.5) +
|
| 3358 |
+
theme_void() +
|
| 3359 |
+
theme(plot.background = element_rect(fill = "transparent", color = NA),
|
| 3360 |
+
panel.background = element_rect(fill = "transparent", color = NA),
|
| 3361 |
+
plot.margin = margin(2, 2, 2, 2))
|
| 3362 |
+
if (!is.null(center_text)) {
|
| 3363 |
+
p <- p + annotate("text", x = 0.4, y = 0,
|
| 3364 |
+
label = center_text,
|
| 3365 |
+
family = "Inter", fontface = "bold",
|
| 3366 |
+
size = 4, color = "#0f172a")
|
| 3367 |
+
}
|
| 3368 |
+
p
|
| 3369 |
+
}
|
| 3370 |
+
|
| 3371 |
+
# --- GIIM donut (municipalities by migration-intensity category) -----------
|
| 3372 |
+
output$donut_giim <- renderPlot({
|
| 3373 |
+
req(con)
|
| 3374 |
+
df <- tryCatch(DBI::dbGetQuery(con, web_sql("donut_giim")),
|
| 3375 |
+
error = function(e) NULL)
|
| 3376 |
+
donut_plot(df, GIIM_COLORS,
|
| 3377 |
+
center_text = if (!is.null(df)) format(sum(df$n), big.mark = ",") else NULL)
|
| 3378 |
+
}, bg = "transparent", res = 96)
|
| 3379 |
+
|
| 3380 |
+
# --- Infra donut (facilities by INFRA_CATS bucket) -------------------------
|
| 3381 |
+
output$donut_infra <- renderPlot({
|
| 3382 |
+
req(con)
|
| 3383 |
+
df <- tryCatch(DBI::dbGetQuery(con, web_sql("donut_infra")),
|
| 3384 |
+
error = function(e) NULL)
|
| 3385 |
+
donut_plot(df, INFRA_CATS,
|
| 3386 |
+
center_text = if (!is.null(df)) format(sum(df$n), big.mark = ",") else NULL)
|
| 3387 |
+
}, bg = "transparent", res = 96)
|
| 3388 |
+
|
| 3389 |
output$dl_xlsx <- downloadHandler(
|
| 3390 |
filename = function()
|
| 3391 |
glue("depth_selection_{format(Sys.time(), '%Y%m%d_%H%M%S')}.xlsx"),
|
styles.css
CHANGED
|
@@ -1187,15 +1187,25 @@
|
|
| 1187 |
display: none;
|
| 1188 |
}
|
| 1189 |
|
| 1190 |
-
/* -- Hide MapLibre Draw buttons
|
| 1191 |
-
/*
|
| 1192 |
-
/*
|
| 1193 |
-
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1194 |
button.mapbox-gl-draw_point,
|
| 1195 |
button.mapbox-gl-draw_trash,
|
| 1196 |
button.mapbox-gl-draw_combine,
|
| 1197 |
button.mapbox-gl-draw_uncombine { display: none !important; }
|
| 1198 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1199 |
/* (draw-extra-ctrl position now set in the overrides section below) */
|
| 1200 |
|
| 1201 |
/* (Viewport coords moved into .nav-coords inside the top nav-bar) */
|
|
@@ -1314,8 +1324,8 @@ button.mapbox-gl-draw_uncombine { display: none !important; }
|
|
| 1314 |
|
| 1315 |
/* ---- Brand ---- */
|
| 1316 |
.nav-brand {
|
| 1317 |
-
font-size:
|
| 1318 |
-
font-weight:
|
| 1319 |
letter-spacing: 0.16em;
|
| 1320 |
text-transform: uppercase;
|
| 1321 |
color: var(--c-text);
|
|
@@ -1327,8 +1337,8 @@ button.mapbox-gl-draw_uncombine { display: none !important; }
|
|
| 1327 |
|
| 1328 |
.nav-brand-dot {
|
| 1329 |
display: inline-block;
|
| 1330 |
-
width:
|
| 1331 |
-
height:
|
| 1332 |
border-radius: 50%;
|
| 1333 |
background: var(--c-accent);
|
| 1334 |
margin-right: 8px;
|
|
@@ -1648,25 +1658,25 @@ button.mapbox-gl-draw_uncombine { display: none !important; }
|
|
| 1648 |
|
| 1649 |
.panel-stack {
|
| 1650 |
display: flex;
|
| 1651 |
-
flex-direction:
|
| 1652 |
-
width: 384px;
|
| 1653 |
-
border-right: 1px solid var(--c-glass-stroke);
|
| 1654 |
background: oklch(99% 0.002 250);
|
| 1655 |
box-shadow: inset 1px 0 0 oklch(100% 0 0 / 0.6);
|
| 1656 |
overflow: hidden;
|
| 1657 |
-
|
| 1658 |
-
|
| 1659 |
}
|
| 1660 |
|
| 1661 |
.panel-stack.is-empty {
|
| 1662 |
-
width: 0;
|
| 1663 |
border-right-color: transparent;
|
| 1664 |
}
|
| 1665 |
|
| 1666 |
.side-panel {
|
| 1667 |
display: flex;
|
| 1668 |
flex-direction: column;
|
| 1669 |
-
flex: 1 1
|
|
|
|
|
|
|
|
|
|
| 1670 |
min-height: 0;
|
| 1671 |
overflow: hidden;
|
| 1672 |
}
|
|
@@ -1674,7 +1684,7 @@ button.mapbox-gl-draw_uncombine { display: none !important; }
|
|
| 1674 |
.side-panel.is-collapsed { display: none; }
|
| 1675 |
|
| 1676 |
.side-panel + .side-panel {
|
| 1677 |
-
border-
|
| 1678 |
}
|
| 1679 |
|
| 1680 |
.side-panel-header {
|
|
@@ -2146,3 +2156,111 @@ button.mapbox-gl-draw_uncombine { display: none !important; }
|
|
| 2146 |
text-decoration: line-through;
|
| 2147 |
color: var(--c-text-dim);
|
| 2148 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1187 |
display: none;
|
| 1188 |
}
|
| 1189 |
|
| 1190 |
+
/* -- Hide ALL MapLibre Draw native buttons -------------------------------- */
|
| 1191 |
+
/* Custom Lucide-icon buttons in the nav-bar proxy-click these natives via */
|
| 1192 |
+
/* drawMode() in JS. The natives stay in the DOM so click() still triggers */
|
| 1193 |
+
/* the mapgl draw API; they are just visually hidden. */
|
| 1194 |
+
button.mapbox-gl-draw_polygon,
|
| 1195 |
+
button.mapbox-gl-draw_rectangle,
|
| 1196 |
+
button.mapbox-gl-draw_radius,
|
| 1197 |
+
button.mapbox-gl-draw_line_string,
|
| 1198 |
button.mapbox-gl-draw_point,
|
| 1199 |
button.mapbox-gl-draw_trash,
|
| 1200 |
button.mapbox-gl-draw_combine,
|
| 1201 |
button.mapbox-gl-draw_uncombine { display: none !important; }
|
| 1202 |
|
| 1203 |
+
/* Also hide the empty mapgl ctrl-group container once buttons are hidden */
|
| 1204 |
+
.maplibregl-ctrl-top-right .maplibregl-ctrl-group:empty,
|
| 1205 |
+
.maplibregl-ctrl-top-right .maplibregl-ctrl-group:has(button:only-child[style*="none"]) {
|
| 1206 |
+
display: none !important;
|
| 1207 |
+
}
|
| 1208 |
+
|
| 1209 |
/* (draw-extra-ctrl position now set in the overrides section below) */
|
| 1210 |
|
| 1211 |
/* (Viewport coords moved into .nav-coords inside the top nav-bar) */
|
|
|
|
| 1324 |
|
| 1325 |
/* ---- Brand ---- */
|
| 1326 |
.nav-brand {
|
| 1327 |
+
font-size: 20px;
|
| 1328 |
+
font-weight: 800;
|
| 1329 |
letter-spacing: 0.16em;
|
| 1330 |
text-transform: uppercase;
|
| 1331 |
color: var(--c-text);
|
|
|
|
| 1337 |
|
| 1338 |
.nav-brand-dot {
|
| 1339 |
display: inline-block;
|
| 1340 |
+
width: 7px;
|
| 1341 |
+
height: 7px;
|
| 1342 |
border-radius: 50%;
|
| 1343 |
background: var(--c-accent);
|
| 1344 |
margin-right: 8px;
|
|
|
|
| 1658 |
|
| 1659 |
.panel-stack {
|
| 1660 |
display: flex;
|
| 1661 |
+
flex-direction: row;
|
|
|
|
|
|
|
| 1662 |
background: oklch(99% 0.002 250);
|
| 1663 |
box-shadow: inset 1px 0 0 oklch(100% 0 0 / 0.6);
|
| 1664 |
overflow: hidden;
|
| 1665 |
+
border-right: 1px solid var(--c-glass-stroke);
|
| 1666 |
+
transition: border-color var(--t-base) var(--ease-out);
|
| 1667 |
}
|
| 1668 |
|
| 1669 |
.panel-stack.is-empty {
|
|
|
|
| 1670 |
border-right-color: transparent;
|
| 1671 |
}
|
| 1672 |
|
| 1673 |
.side-panel {
|
| 1674 |
display: flex;
|
| 1675 |
flex-direction: column;
|
| 1676 |
+
flex: 1 1 360px;
|
| 1677 |
+
min-width: 320px;
|
| 1678 |
+
max-width: 420px;
|
| 1679 |
+
height: 100%;
|
| 1680 |
min-height: 0;
|
| 1681 |
overflow: hidden;
|
| 1682 |
}
|
|
|
|
| 1684 |
.side-panel.is-collapsed { display: none; }
|
| 1685 |
|
| 1686 |
.side-panel + .side-panel {
|
| 1687 |
+
border-left: 1px solid var(--c-glass-stroke);
|
| 1688 |
}
|
| 1689 |
|
| 1690 |
.side-panel-header {
|
|
|
|
| 2156 |
text-decoration: line-through;
|
| 2157 |
color: var(--c-text-dim);
|
| 2158 |
}
|
| 2159 |
+
|
| 2160 |
+
/* =========================================================================
|
| 2161 |
+
Card grid (full + half-width side-by-side cards)
|
| 2162 |
+
========================================================================= */
|
| 2163 |
+
.dp-group-body {
|
| 2164 |
+
display: grid;
|
| 2165 |
+
grid-template-columns: 1fr 1fr;
|
| 2166 |
+
gap: var(--space-2);
|
| 2167 |
+
padding: var(--space-1) 0 var(--space-2) 0;
|
| 2168 |
+
}
|
| 2169 |
+
|
| 2170 |
+
/* Default: a card spans both columns (full width) */
|
| 2171 |
+
.ind-card {
|
| 2172 |
+
grid-column: 1 / -1;
|
| 2173 |
+
}
|
| 2174 |
+
|
| 2175 |
+
/* Half-width card -- two consecutive .ind-card--half siblings sit side by side */
|
| 2176 |
+
.ind-card--half {
|
| 2177 |
+
grid-column: auto;
|
| 2178 |
+
display: flex;
|
| 2179 |
+
flex-direction: column;
|
| 2180 |
+
}
|
| 2181 |
+
|
| 2182 |
+
.ind-card--half .ind-card-body { padding: var(--space-2) 10px; }
|
| 2183 |
+
|
| 2184 |
+
/* =========================================================================
|
| 2185 |
+
Risk-weight sliders (placeholder UI inside the network panel)
|
| 2186 |
+
========================================================================= */
|
| 2187 |
+
.risk-body { padding: var(--space-4); overflow-y: auto; }
|
| 2188 |
+
|
| 2189 |
+
.risk-weights {
|
| 2190 |
+
display: flex;
|
| 2191 |
+
flex-direction: column;
|
| 2192 |
+
gap: var(--space-3);
|
| 2193 |
+
padding-top: var(--space-2);
|
| 2194 |
+
}
|
| 2195 |
+
|
| 2196 |
+
.risk-weight {
|
| 2197 |
+
display: flex;
|
| 2198 |
+
flex-direction: column;
|
| 2199 |
+
gap: 4px;
|
| 2200 |
+
}
|
| 2201 |
+
|
| 2202 |
+
.risk-weight label {
|
| 2203 |
+
display: flex;
|
| 2204 |
+
align-items: baseline;
|
| 2205 |
+
justify-content: space-between;
|
| 2206 |
+
font-family: var(--font-ui);
|
| 2207 |
+
font-size: 11.5px;
|
| 2208 |
+
color: var(--c-text);
|
| 2209 |
+
margin: 0;
|
| 2210 |
+
}
|
| 2211 |
+
|
| 2212 |
+
.risk-weight-label {
|
| 2213 |
+
font-weight: 600;
|
| 2214 |
+
letter-spacing: 0.02em;
|
| 2215 |
+
}
|
| 2216 |
+
|
| 2217 |
+
.risk-weight-val {
|
| 2218 |
+
font-family: var(--font-mono);
|
| 2219 |
+
font-size: 11px;
|
| 2220 |
+
font-variant-numeric: tabular-nums;
|
| 2221 |
+
color: var(--c-text-mid);
|
| 2222 |
+
}
|
| 2223 |
+
|
| 2224 |
+
/* Range input -- subtle, accent track */
|
| 2225 |
+
.risk-weight input[type="range"] {
|
| 2226 |
+
-webkit-appearance: none;
|
| 2227 |
+
appearance: none;
|
| 2228 |
+
width: 100%;
|
| 2229 |
+
height: 4px;
|
| 2230 |
+
background: var(--c-glass-stroke-strong);
|
| 2231 |
+
border-radius: var(--radius-pill);
|
| 2232 |
+
outline: none;
|
| 2233 |
+
cursor: not-allowed; /* disabled in this preview */
|
| 2234 |
+
opacity: 0.85;
|
| 2235 |
+
}
|
| 2236 |
+
.risk-weight input[type="range"]:disabled { opacity: 0.55; }
|
| 2237 |
+
|
| 2238 |
+
.risk-weight input[type="range"]::-webkit-slider-thumb {
|
| 2239 |
+
-webkit-appearance: none;
|
| 2240 |
+
appearance: none;
|
| 2241 |
+
width: 14px;
|
| 2242 |
+
height: 14px;
|
| 2243 |
+
border-radius: 50%;
|
| 2244 |
+
background: var(--c-accent);
|
| 2245 |
+
border: 2px solid #fff;
|
| 2246 |
+
box-shadow: var(--depth-1);
|
| 2247 |
+
cursor: not-allowed;
|
| 2248 |
+
}
|
| 2249 |
+
.risk-weight input[type="range"]::-moz-range-thumb {
|
| 2250 |
+
width: 14px;
|
| 2251 |
+
height: 14px;
|
| 2252 |
+
border-radius: 50%;
|
| 2253 |
+
background: var(--c-accent);
|
| 2254 |
+
border: 2px solid #fff;
|
| 2255 |
+
box-shadow: var(--depth-1);
|
| 2256 |
+
cursor: not-allowed;
|
| 2257 |
+
}
|
| 2258 |
+
|
| 2259 |
+
/* =========================================================================
|
| 2260 |
+
Hide the empty mapgl ctrl-group wrapper (after natives are display:none)
|
| 2261 |
+
========================================================================= */
|
| 2262 |
+
.maplibregl-ctrl-top-right .maplibregl-ctrl-group {
|
| 2263 |
+
background: transparent !important;
|
| 2264 |
+
box-shadow: none !important;
|
| 2265 |
+
border: none !important;
|
| 2266 |
+
}
|