Upload depth_interactive_map_alpha.R
Browse files- depth_interactive_map_alpha.R +130 -82
depth_interactive_map_alpha.R
CHANGED
|
@@ -340,14 +340,14 @@ WEB_SQL_TEMPLATES <- list(
|
|
| 340 |
|
| 341 |
temporal_pam_all =
|
| 342 |
"SELECT CAST(date_part('year', date) AS INTEGER) AS yr,
|
| 343 |
-
COALESCE(SUM(
|
| 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(
|
| 351 |
FROM dim_pam_mun
|
| 352 |
WHERE date IS NOT NULL
|
| 353 |
AND CVEGEO IN ({cvegeo_list})
|
|
@@ -745,12 +745,12 @@ WEB_SPEC <- list(
|
|
| 745 |
tooltip = "Each dot marks a location where migrants went missing or died, recorded by the International Organization for Migration.",
|
| 746 |
legend = list(type = "dot", color = COL_YEL, label = "Incident location")),
|
| 747 |
list(kind = "layer", value = "desap-choro",
|
| 748 |
-
label = "
|
| 749 |
-
tooltip = "People officially reported as
|
| 750 |
legend = list(type = "ramp", pal = "desap", unit = " /100k")),
|
| 751 |
list(kind = "layer", value = "fosas-choro",
|
| 752 |
-
label = "Clandestine
|
| 753 |
-
tooltip = "Number of clandestine
|
| 754 |
legend = list(type = "ramp", pal = "fosas", unit = " pits")),
|
| 755 |
list(kind = "radios", group = "fosas"),
|
| 756 |
list(kind = "layer", value = "graves-pts",
|
|
@@ -788,7 +788,7 @@ WEB_SPEC <- list(
|
|
| 788 |
list(kind = "layer", value = "viaferrea-lines",
|
| 789 |
label = "Railway network",
|
| 790 |
tooltip = "Railway lines used by migrants, including freight routes known as 'La Bestia'.",
|
| 791 |
-
legend = list(type = "line", color = "#
|
| 792 |
list(kind = "soon", label = "Foreign population")
|
| 793 |
)),
|
| 794 |
list(id = "vulnerability", label = "Vulnerability", open = FALSE, items = list(
|
|
@@ -819,8 +819,8 @@ WEB_SPEC <- list(
|
|
| 819 |
|
| 820 |
# Analysis panel (converter -> docs/js/panels.js; Shiny renders this too)
|
| 821 |
panel = list(
|
| 822 |
-
title = "
|
| 823 |
-
subtitle = "
|
| 824 |
groups = list(
|
| 825 |
list(id = "hazards", label = "Hazards", open = TRUE, cards = list(
|
| 826 |
list(title = "Missing Migrants Incidents (IOM)", kind = "stat", chart = "temporal_plot",
|
|
@@ -828,15 +828,15 @@ WEB_SPEC <- list(
|
|
| 828 |
list(id = "n_incidents", label = "Recorded incidents", color = COL_YEL),
|
| 829 |
list(id = "n_fatalities", label = "Confirmed dead", color = COL_RED)
|
| 830 |
)),
|
| 831 |
-
list(title = "
|
| 832 |
-
stats = list(list(id = "n_desap", label = "
|
| 833 |
# Side-by-side burial pair: graves (points) + fosas (municipality sums)
|
| 834 |
-
list(title = "
|
| 835 |
-
stats = list(list(id = "n_graves", label = "Clandestine
|
| 836 |
-
list(title = "
|
| 837 |
stats = list(
|
| 838 |
-
list(id = "n_fosas", label = "Clandestine
|
| 839 |
-
list(id = "n_cuerpos", label = "Bodies
|
| 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))),
|
|
@@ -865,7 +865,12 @@ WEB_SPEC <- list(
|
|
| 865 |
LUCIDE_JS <- tags$head(
|
| 866 |
tags$script(src = "https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"),
|
| 867 |
tags$script(HTML("
|
| 868 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 869 |
|
| 870 |
document.addEventListener('DOMContentLoaded', function() {
|
| 871 |
setTimeout(function() {
|
|
@@ -925,11 +930,8 @@ LUCIDE_JS <- tags$head(
|
|
| 925 |
if (picker) picker.style.display = 'none';
|
| 926 |
}
|
| 927 |
var gridWrap = document.querySelector('.grid-btn-wrap');
|
| 928 |
-
if (gridWrap && !gridWrap.contains(e.target) &&
|
| 929 |
-
|
| 930 |
-
closeGridPanel();
|
| 931 |
-
var gridBtn = document.getElementById('grid-icon-btn');
|
| 932 |
-
if (gridBtn) gridBtn.classList.remove('active');
|
| 933 |
}
|
| 934 |
var statePanel = document.getElementById('state-picker-panel');
|
| 935 |
var stateBtn = document.getElementById('btn_state_filter');
|
|
@@ -1051,39 +1053,46 @@ LUCIDE_JS <- tags$head(
|
|
| 1051 |
var panel = document.getElementById('grid-panel');
|
| 1052 |
if (!panel) return;
|
| 1053 |
panel.classList.remove('open');
|
| 1054 |
-
|
|
|
|
|
|
|
|
|
|
| 1055 |
}
|
| 1056 |
|
|
|
|
|
|
|
| 1057 |
function toggleGridPanel() {
|
| 1058 |
var panel = document.getElementById('grid-panel');
|
| 1059 |
if (!panel) return;
|
| 1060 |
-
|
| 1061 |
-
|
|
|
|
|
|
|
| 1062 |
panel.style.display = 'flex';
|
| 1063 |
requestAnimationFrame(function() { panel.classList.add('open'); });
|
| 1064 |
-
} else {
|
| 1065 |
-
closeGridPanel();
|
| 1066 |
}
|
| 1067 |
-
var btn = document.getElementById('grid-icon-btn');
|
| 1068 |
-
if (btn) btn.classList.toggle('active', gridOn);
|
| 1069 |
-
if (typeof Shiny !== 'undefined')
|
| 1070 |
-
Shiny.setInputValue('grid_icon_click', Date.now(), { priority: 'event' });
|
| 1071 |
}
|
| 1072 |
|
|
|
|
|
|
|
| 1073 |
function setGridNivel(nivel) {
|
| 1074 |
-
|
| 1075 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1076 |
document.querySelectorAll('.grid-nivel-item').forEach(function(btn) {
|
| 1077 |
-
btn.classList.toggle('active', btn.dataset.nivel === nivel);
|
| 1078 |
});
|
| 1079 |
-
//
|
| 1080 |
var trigBadge = document.getElementById('grid-trig-label');
|
| 1081 |
-
if (trigBadge) trigBadge.textContent = 'N' + nivel;
|
| 1082 |
-
// Auto-collapse grid panel after selection
|
| 1083 |
-
closeGridPanel();
|
| 1084 |
var iconBtn = document.getElementById('grid-icon-btn');
|
| 1085 |
-
if (iconBtn) iconBtn.classList.
|
| 1086 |
-
|
| 1087 |
}
|
| 1088 |
|
| 1089 |
// -- Legend filter: click a categorical swatch to toggle that category --
|
|
@@ -1823,7 +1832,7 @@ ui <- page_fillable(
|
|
| 1823 |
div(class = "grid-btn-wrap",
|
| 1824 |
tags$span(
|
| 1825 |
class = "acc-info-wrap grid-info-btn",
|
| 1826 |
-
`data-tooltip` = "Overlay a spatial grid on the map. Select resolution: N4 (~
|
| 1827 |
style = "display:none;",
|
| 1828 |
HTML('<i data-lucide="info" style="width:11px;height:11px;stroke-width:2.5;color:var(--c-text-mid)"></i>')
|
| 1829 |
),
|
|
@@ -1835,22 +1844,22 @@ ui <- page_fillable(
|
|
| 1835 |
`aria-label` = "Toggle spatial grid overlay",
|
| 1836 |
title = "Toggle grid resolution",
|
| 1837 |
HTML('<i data-lucide="grid-3x3" style="width:16px;height:16px;stroke-width:2" aria-hidden="true"></i>'),
|
| 1838 |
-
tags$span(id = "grid-trig-label", class = "nav-btn-badge", "
|
| 1839 |
),
|
| 1840 |
div(id = "grid-panel", class = "grid-panel grid-panel--rich", style = "display:none;",
|
| 1841 |
div(class = "grid-panel-current", "Grid resolution"),
|
| 1842 |
local({
|
| 1843 |
nivel_info <- list(
|
| 1844 |
-
"4" = list(name = "Nivel 4", size = "~
|
| 1845 |
-
"5" = list(name = "Nivel 5", size = "~
|
| 1846 |
-
"6" = list(name = "Nivel 6", size = "~
|
| 1847 |
-
"7" = list(name = "Nivel 7", size = "~4 km", desc = "Local detail")
|
| 1848 |
)
|
| 1849 |
lapply(WEB_SPEC$niveles, function(nv) {
|
| 1850 |
info <- nivel_info[[nv]]
|
| 1851 |
tags$button(
|
| 1852 |
type = "button",
|
| 1853 |
-
class =
|
| 1854 |
`data-nivel` = nv,
|
| 1855 |
onclick = paste0("setGridNivel('", nv, "')"),
|
| 1856 |
`aria-label` = paste0("Set grid to ", info$name, ", ", info$size, " squared, ", info$desc),
|
|
@@ -2109,7 +2118,7 @@ ui <- page_fillable(
|
|
| 2109 |
`aria-label` = "Toggle Transit Migration Risk Index",
|
| 2110 |
onclick = "togglePanel('tmri')",
|
| 2111 |
HTML('<i data-lucide="sigma" style="width:18px;height:18px;stroke-width:2" aria-hidden="true"></i>'),
|
| 2112 |
-
tags$span(class = "side-rail-tab-label", "
|
| 2113 |
tags$button(class = "side-rail-tab",
|
| 2114 |
id = "tab-risk",
|
| 2115 |
type = "button",
|
|
@@ -2119,8 +2128,8 @@ ui <- page_fillable(
|
|
| 2119 |
`aria-selected` = "false",
|
| 2120 |
`aria-label` = "Toggle Risk Index Adjustments",
|
| 2121 |
onclick = "togglePanel('risk')",
|
| 2122 |
-
HTML('<i data-lucide="
|
| 2123 |
-
tags$span(class = "side-rail-tab-label", "
|
| 2124 |
tags$button(class = "side-rail-tab",
|
| 2125 |
id = "tab-assist",
|
| 2126 |
type = "button",
|
|
@@ -2763,7 +2772,7 @@ server <- function(input, output, session) {
|
|
| 2763 |
fill_color = fosas_step_expr("fosas"),
|
| 2764 |
fill_opacity = 0.65, visibility = "none",
|
| 2765 |
tooltip = list("concat", "<b>", g("municipio"), "</b>",
|
| 2766 |
-
"<br>
|
| 2767 |
"<br>Bodies (cuerpos): ", ts("cuerpos_total"))
|
| 2768 |
)
|
| 2769 |
}
|
|
@@ -3033,9 +3042,10 @@ server <- function(input, output, session) {
|
|
| 3033 |
}
|
| 3034 |
}, once = TRUE)
|
| 3035 |
|
| 3036 |
-
|
| 3037 |
-
|
| 3038 |
-
|
|
|
|
| 3039 |
}, ignoreNULL = TRUE, ignoreInit = TRUE)
|
| 3040 |
|
| 3041 |
observeEvent(
|
|
@@ -3209,10 +3219,10 @@ server <- function(input, output, session) {
|
|
| 3209 |
output$mun_section_lbl <- renderText({
|
| 3210 |
lbl <- tolower(input$crime_type %||% "homicide rate")
|
| 3211 |
if (isTRUE(rv$is_national)) {
|
| 3212 |
-
glue("Top 5 municipalities
|
| 3213 |
} else {
|
| 3214 |
n <- if (!is.null(rv$mun_df)) nrow(rv$mun_df) else 0
|
| 3215 |
-
glue("Top 5 municipalities
|
| 3216 |
}
|
| 3217 |
})
|
| 3218 |
|
|
@@ -3446,38 +3456,76 @@ server <- function(input, output, session) {
|
|
| 3446 |
total_label = "Total facilities")
|
| 3447 |
})
|
| 3448 |
|
| 3449 |
-
# Keep all
|
| 3450 |
-
# they
|
| 3451 |
-
#
|
| 3452 |
-
|
| 3453 |
-
|
| 3454 |
-
|
| 3455 |
-
|
| 3456 |
-
|
| 3457 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3458 |
|
| 3459 |
output$dl_xlsx <- downloadHandler(
|
| 3460 |
filename = function()
|
| 3461 |
glue("depth_selection_{format(Sys.time(), '%Y%m%d_%H%M%S')}.xlsx"),
|
|
|
|
|
|
|
| 3462 |
content = function(file) {
|
| 3463 |
-
|
| 3464 |
-
|
| 3465 |
-
|
| 3466 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3467 |
}
|
| 3468 |
-
dl <- rv$dl_list
|
| 3469 |
-
keep <- list(
|
| 3470 |
-
migrants = dl$migrants,
|
| 3471 |
-
graves = dl$graves,
|
| 3472 |
-
support_infra = dl$infra,
|
| 3473 |
-
violence_ocved = dl$ocved,
|
| 3474 |
-
conflict_ged = dl$ged,
|
| 3475 |
-
municipalities_crime = dl$mun
|
| 3476 |
-
)
|
| 3477 |
-
sheets <- Filter(function(x) !is.null(x) && nrow(x) > 0, keep)
|
| 3478 |
-
if (length(sheets) == 0)
|
| 3479 |
-
sheets <- list(note = data.frame(note = "No features in selection."))
|
| 3480 |
-
writexl::write_xlsx(sheets, path = file)
|
| 3481 |
}
|
| 3482 |
)
|
| 3483 |
}
|
|
|
|
| 340 |
|
| 341 |
temporal_pam_all =
|
| 342 |
"SELECT CAST(date_part('year', date) AS INTEGER) AS yr,
|
| 343 |
+
COALESCE(SUM(pam_presentados_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_presentados_n), 0) AS n
|
| 351 |
FROM dim_pam_mun
|
| 352 |
WHERE date IS NOT NULL
|
| 353 |
AND CVEGEO IN ({cvegeo_list})
|
|
|
|
| 745 |
tooltip = "Each dot marks a location where migrants went missing or died, recorded by the International Organization for Migration.",
|
| 746 |
legend = list(type = "dot", color = COL_YEL, label = "Incident location")),
|
| 747 |
list(kind = "layer", value = "desap-choro",
|
| 748 |
+
label = "Missing and no located persons per 100k (RNPDNO)",
|
| 749 |
+
tooltip = "People officially reported as disappeared and no located in each municipality, shown per 100,000 residents.",
|
| 750 |
legend = list(type = "ramp", pal = "desap", unit = " /100k")),
|
| 751 |
list(kind = "layer", value = "fosas-choro",
|
| 752 |
+
label = "Clandestine graves by municipality",
|
| 753 |
+
tooltip = "Number of clandestine graves and bodies found per municipality. Source: Plataforma Ciudadana de Fosas (2006-2024)",
|
| 754 |
legend = list(type = "ramp", pal = "fosas", unit = " pits")),
|
| 755 |
list(kind = "radios", group = "fosas"),
|
| 756 |
list(kind = "layer", value = "graves-pts",
|
|
|
|
| 788 |
list(kind = "layer", value = "viaferrea-lines",
|
| 789 |
label = "Railway network",
|
| 790 |
tooltip = "Railway lines used by migrants, including freight routes known as 'La Bestia'.",
|
| 791 |
+
legend = list(type = "line", color = "#9E2A3A", label = "Railway", dashed = TRUE)),
|
| 792 |
list(kind = "soon", label = "Foreign population")
|
| 793 |
)),
|
| 794 |
list(id = "vulnerability", label = "Vulnerability", open = FALSE, items = list(
|
|
|
|
| 819 |
|
| 820 |
# Analysis panel (converter -> docs/js/panels.js; Shiny renders this too)
|
| 821 |
panel = list(
|
| 822 |
+
title = "Layer aggregations",
|
| 823 |
+
subtitle = "Summary of main indicators for migrants across Mexico.",
|
| 824 |
groups = list(
|
| 825 |
list(id = "hazards", label = "Hazards", open = TRUE, cards = list(
|
| 826 |
list(title = "Missing Migrants Incidents (IOM)", kind = "stat", chart = "temporal_plot",
|
|
|
|
| 828 |
list(id = "n_incidents", label = "Recorded incidents", color = COL_YEL),
|
| 829 |
list(id = "n_fatalities", label = "Confirmed dead", color = COL_RED)
|
| 830 |
)),
|
| 831 |
+
list(title = "Missing persons and not located (RNPDNO)", kind = "stat",
|
| 832 |
+
stats = list(list(id = "n_desap", label = "Disappearances & no located", color = COL_RISK))),
|
| 833 |
# Side-by-side burial pair: graves (points) + fosas (municipality sums)
|
| 834 |
+
list(title = "Clandestine graves (Vera, 2023)", kind = "stat", half = TRUE,
|
| 835 |
+
stats = list(list(id = "n_graves", label = "Clandestine graves", color = COL_RED))),
|
| 836 |
+
list(title = "Clandestine graves (PCF)", kind = "stat", half = TRUE,
|
| 837 |
stats = list(
|
| 838 |
+
list(id = "n_fosas", label = "Clandestine graves", color = COL_RED),
|
| 839 |
+
list(id = "n_cuerpos", label = "Bodies identified", 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))),
|
|
|
|
| 865 |
LUCIDE_JS <- tags$head(
|
| 866 |
tags$script(src = "https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"),
|
| 867 |
tags$script(HTML("
|
| 868 |
+
// gridMenuOpen = is the N4-N8 resolution menu visible (purely visual);
|
| 869 |
+
// gridActive = is the grid overlay currently shown on the map.
|
| 870 |
+
// These are independent: opening the menu must NOT change the grid.
|
| 871 |
+
var gridMenuOpen = false;
|
| 872 |
+
var gridActive = false;
|
| 873 |
+
var gridNivel = '6';
|
| 874 |
|
| 875 |
document.addEventListener('DOMContentLoaded', function() {
|
| 876 |
setTimeout(function() {
|
|
|
|
| 930 |
if (picker) picker.style.display = 'none';
|
| 931 |
}
|
| 932 |
var gridWrap = document.querySelector('.grid-btn-wrap');
|
| 933 |
+
if (gridWrap && !gridWrap.contains(e.target) && gridMenuOpen) {
|
| 934 |
+
closeGridPanel(); // close the menu only; grid state is untouched
|
|
|
|
|
|
|
|
|
|
| 935 |
}
|
| 936 |
var statePanel = document.getElementById('state-picker-panel');
|
| 937 |
var stateBtn = document.getElementById('btn_state_filter');
|
|
|
|
| 1053 |
var panel = document.getElementById('grid-panel');
|
| 1054 |
if (!panel) return;
|
| 1055 |
panel.classList.remove('open');
|
| 1056 |
+
gridMenuOpen = false;
|
| 1057 |
+
setTimeout(function() {
|
| 1058 |
+
if (!panel.classList.contains('open')) panel.style.display = 'none';
|
| 1059 |
+
}, 160);
|
| 1060 |
}
|
| 1061 |
|
| 1062 |
+
// Grid trigger button: opens/closes the resolution menu ONLY.
|
| 1063 |
+
// It never changes the grid overlay -- that happens on resolution click.
|
| 1064 |
function toggleGridPanel() {
|
| 1065 |
var panel = document.getElementById('grid-panel');
|
| 1066 |
if (!panel) return;
|
| 1067 |
+
if (gridMenuOpen) {
|
| 1068 |
+
closeGridPanel();
|
| 1069 |
+
} else {
|
| 1070 |
+
gridMenuOpen = true;
|
| 1071 |
panel.style.display = 'flex';
|
| 1072 |
requestAnimationFrame(function() { panel.classList.add('open'); });
|
|
|
|
|
|
|
| 1073 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1074 |
}
|
| 1075 |
|
| 1076 |
+
// Resolution click: applies the grid at that nivel. Clicking the nivel
|
| 1077 |
+
// that is already active toggles the grid OFF.
|
| 1078 |
function setGridNivel(nivel) {
|
| 1079 |
+
var turnOff = (gridActive && nivel === gridNivel);
|
| 1080 |
+
gridActive = !turnOff;
|
| 1081 |
+
gridNivel = nivel;
|
| 1082 |
+
if (typeof Shiny !== 'undefined') {
|
| 1083 |
+
Shiny.setInputValue('grid_nivel', nivel, { priority: 'event' });
|
| 1084 |
+
Shiny.setInputValue('grid_enabled', gridActive, { priority: 'event' });
|
| 1085 |
+
}
|
| 1086 |
+
// Highlight the active resolution only while the grid is on
|
| 1087 |
document.querySelectorAll('.grid-nivel-item').forEach(function(btn) {
|
| 1088 |
+
btn.classList.toggle('active', gridActive && btn.dataset.nivel === nivel);
|
| 1089 |
});
|
| 1090 |
+
// Trigger badge + button reflect the GRID state, not the menu state
|
| 1091 |
var trigBadge = document.getElementById('grid-trig-label');
|
| 1092 |
+
if (trigBadge) trigBadge.textContent = gridActive ? ('N' + nivel) : 'Off';
|
|
|
|
|
|
|
| 1093 |
var iconBtn = document.getElementById('grid-icon-btn');
|
| 1094 |
+
if (iconBtn) iconBtn.classList.toggle('active', gridActive);
|
| 1095 |
+
closeGridPanel();
|
| 1096 |
}
|
| 1097 |
|
| 1098 |
// -- Legend filter: click a categorical swatch to toggle that category --
|
|
|
|
| 1832 |
div(class = "grid-btn-wrap",
|
| 1833 |
tags$span(
|
| 1834 |
class = "acc-info-wrap grid-info-btn",
|
| 1835 |
+
`data-tooltip` = "Overlay a spatial grid on the map. Select resolution: N4 (~114 km cell length, national overview) to N7 (~4 km cell length, local detail). Grid colours follow the active layer.",
|
| 1836 |
style = "display:none;",
|
| 1837 |
HTML('<i data-lucide="info" style="width:11px;height:11px;stroke-width:2.5;color:var(--c-text-mid)"></i>')
|
| 1838 |
),
|
|
|
|
| 1844 |
`aria-label` = "Toggle spatial grid overlay",
|
| 1845 |
title = "Toggle grid resolution",
|
| 1846 |
HTML('<i data-lucide="grid-3x3" style="width:16px;height:16px;stroke-width:2" aria-hidden="true"></i>'),
|
| 1847 |
+
tags$span(id = "grid-trig-label", class = "nav-btn-badge", "Off")
|
| 1848 |
),
|
| 1849 |
div(id = "grid-panel", class = "grid-panel grid-panel--rich", style = "display:none;",
|
| 1850 |
div(class = "grid-panel-current", "Grid resolution"),
|
| 1851 |
local({
|
| 1852 |
nivel_info <- list(
|
| 1853 |
+
"4" = list(name = "Nivel 4", size = "~144 km cell length", desc = "National overview"),
|
| 1854 |
+
"5" = list(name = "Nivel 5", size = "~38 km cell length", desc = "Regional"),
|
| 1855 |
+
"6" = list(name = "Nivel 6", size = "~13 km cell length", desc = "Municipality"),
|
| 1856 |
+
"7" = list(name = "Nivel 7", size = "~4 km cell length", desc = "Local detail")
|
| 1857 |
)
|
| 1858 |
lapply(WEB_SPEC$niveles, function(nv) {
|
| 1859 |
info <- nivel_info[[nv]]
|
| 1860 |
tags$button(
|
| 1861 |
type = "button",
|
| 1862 |
+
class = "grid-nivel-item",
|
| 1863 |
`data-nivel` = nv,
|
| 1864 |
onclick = paste0("setGridNivel('", nv, "')"),
|
| 1865 |
`aria-label` = paste0("Set grid to ", info$name, ", ", info$size, " squared, ", info$desc),
|
|
|
|
| 2118 |
`aria-label` = "Toggle Transit Migration Risk Index",
|
| 2119 |
onclick = "togglePanel('tmri')",
|
| 2120 |
HTML('<i data-lucide="sigma" style="width:18px;height:18px;stroke-width:2" aria-hidden="true"></i>'),
|
| 2121 |
+
tags$span(class = "side-rail-tab-label", "Agg")),
|
| 2122 |
tags$button(class = "side-rail-tab",
|
| 2123 |
id = "tab-risk",
|
| 2124 |
type = "button",
|
|
|
|
| 2128 |
`aria-selected` = "false",
|
| 2129 |
`aria-label` = "Toggle Risk Index Adjustments",
|
| 2130 |
onclick = "togglePanel('risk')",
|
| 2131 |
+
HTML('<i data-lucide="combine" style="width:18px;height:18px;stroke-width:2" aria-hidden="true"></i>'),
|
| 2132 |
+
tags$span(class = "side-rail-tab-label", "Risk")),
|
| 2133 |
tags$button(class = "side-rail-tab",
|
| 2134 |
id = "tab-assist",
|
| 2135 |
type = "button",
|
|
|
|
| 2772 |
fill_color = fosas_step_expr("fosas"),
|
| 2773 |
fill_opacity = 0.65, visibility = "none",
|
| 2774 |
tooltip = list("concat", "<b>", g("municipio"), "</b>",
|
| 2775 |
+
"<br>Clandestine graves (PCF): ", ts("fosas_total"),
|
| 2776 |
"<br>Bodies (cuerpos): ", ts("cuerpos_total"))
|
| 2777 |
)
|
| 2778 |
}
|
|
|
|
| 3042 |
}
|
| 3043 |
}, once = TRUE)
|
| 3044 |
|
| 3045 |
+
# Grid overlay on/off is driven explicitly by resolution clicks in the menu
|
| 3046 |
+
# (input$grid_enabled), never by opening the menu.
|
| 3047 |
+
observeEvent(input$grid_enabled, {
|
| 3048 |
+
is_grid_on(isTRUE(input$grid_enabled))
|
| 3049 |
}, ignoreNULL = TRUE, ignoreInit = TRUE)
|
| 3050 |
|
| 3051 |
observeEvent(
|
|
|
|
| 3219 |
output$mun_section_lbl <- renderText({
|
| 3220 |
lbl <- tolower(input$crime_type %||% "homicide rate")
|
| 3221 |
if (isTRUE(rv$is_national)) {
|
| 3222 |
+
glue("Top 5 municipalities: {lbl} (all Mexico)")
|
| 3223 |
} else {
|
| 3224 |
n <- if (!is.null(rv$mun_df)) nrow(rv$mun_df) else 0
|
| 3225 |
+
glue("Top 5 municipalities: {lbl} ({n} in selection)")
|
| 3226 |
}
|
| 3227 |
})
|
| 3228 |
|
|
|
|
| 3456 |
total_label = "Total facilities")
|
| 3457 |
})
|
| 3458 |
|
| 3459 |
+
# Keep all panel outputs computing even when their parent accordion group is
|
| 3460 |
+
# collapsed, so they show real values the moment the group is expanded
|
| 3461 |
+
# (otherwise hidden outputs stay suspended and read "---"). Cost: one extra
|
| 3462 |
+
# render per output per session -- negligible (cached strings + small plots).
|
| 3463 |
+
for (.oid in c(
|
| 3464 |
+
"temporal_pam", "temporal_plot", "temporal_ocved", "temporal_ged",
|
| 3465 |
+
"breakdown_giim", "breakdown_infra",
|
| 3466 |
+
"n_incidents", "n_fatalities", "n_graves", "n_facilities", "n_events",
|
| 3467 |
+
"ocved_n", "n_ged", "n_mun", "n_desap", "n_pam", "n_fosas", "n_cuerpos",
|
| 3468 |
+
"sub_incidents", "sub_fatalities", "panel_subtitle", "mun_section_lbl",
|
| 3469 |
+
"mun_list"
|
| 3470 |
+
)) {
|
| 3471 |
+
outputOptions(output, .oid, suspendWhenHidden = FALSE)
|
| 3472 |
+
}
|
| 3473 |
|
| 3474 |
output$dl_xlsx <- downloadHandler(
|
| 3475 |
filename = function()
|
| 3476 |
glue("depth_selection_{format(Sys.time(), '%Y%m%d_%H%M%S')}.xlsx"),
|
| 3477 |
+
contentType =
|
| 3478 |
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 3479 |
content = function(file) {
|
| 3480 |
+
# writexl only accepts plain atomic-column data frames. A query result
|
| 3481 |
+
# can carry a geometry / WKB blob or a list column, which makes
|
| 3482 |
+
# write_xlsx() throw -- Shiny then serves its HTML error page and the
|
| 3483 |
+
# browser saves that with the .xlsx name. Sanitise every sheet and never
|
| 3484 |
+
# let the content function throw, so the download is always valid xlsx.
|
| 3485 |
+
flatten_df <- function(df) {
|
| 3486 |
+
if (is.null(df)) return(NULL)
|
| 3487 |
+
df <- tryCatch(as.data.frame(df), error = function(e) NULL)
|
| 3488 |
+
if (is.null(df) || nrow(df) == 0) return(NULL)
|
| 3489 |
+
keep <- vapply(df, function(col)
|
| 3490 |
+
is.atomic(col) && !is.list(col), logical(1))
|
| 3491 |
+
df <- df[, keep, drop = FALSE]
|
| 3492 |
+
if (ncol(df) == 0) return(NULL)
|
| 3493 |
+
df
|
| 3494 |
+
}
|
| 3495 |
+
sheets <- tryCatch({
|
| 3496 |
+
if (is.null(rv$dl_list)) {
|
| 3497 |
+
list(note = data.frame(
|
| 3498 |
+
note = "No selection made -- draw an area on the map first."))
|
| 3499 |
+
} else {
|
| 3500 |
+
dl <- rv$dl_list
|
| 3501 |
+
cand <- list(
|
| 3502 |
+
migrants = flatten_df(dl$migrants),
|
| 3503 |
+
graves = flatten_df(dl$graves),
|
| 3504 |
+
support_infra = flatten_df(dl$infra),
|
| 3505 |
+
violence_ocved = flatten_df(dl$ocved),
|
| 3506 |
+
conflict_ged = flatten_df(dl$ged),
|
| 3507 |
+
municipalities_crime = flatten_df(dl$mun)
|
| 3508 |
+
)
|
| 3509 |
+
s <- Filter(function(x) !is.null(x) && nrow(x) > 0, cand)
|
| 3510 |
+
if (length(s) == 0)
|
| 3511 |
+
list(note = data.frame(note = "No features in the selection."))
|
| 3512 |
+
else s
|
| 3513 |
+
}
|
| 3514 |
+
}, error = function(e)
|
| 3515 |
+
list(error = data.frame(
|
| 3516 |
+
error = paste("Could not assemble export:", conditionMessage(e)))))
|
| 3517 |
+
|
| 3518 |
+
ok <- tryCatch({ writexl::write_xlsx(sheets, path = file); TRUE },
|
| 3519 |
+
error = function(e) {
|
| 3520 |
+
message("[dl_xlsx] ", conditionMessage(e)); FALSE
|
| 3521 |
+
})
|
| 3522 |
+
if (!ok) {
|
| 3523 |
+
# Last-resort: a guaranteed-valid one-sheet workbook.
|
| 3524 |
+
writexl::write_xlsx(
|
| 3525 |
+
list(error = data.frame(
|
| 3526 |
+
error = "Export failed -- please retry or report this.")),
|
| 3527 |
+
path = file)
|
| 3528 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3529 |
}
|
| 3530 |
)
|
| 3531 |
}
|