edgarodriguez commited on
Commit
c2e7b41
·
verified ·
1 Parent(s): 4c64a74

Upload depth_interactive_map_alpha.R

Browse files
Files changed (1) hide show
  1. 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(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})
@@ -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 = "Enforced disappearances per 100k (RNPDNO)",
749
- tooltip = "People officially reported as forcibly disappeared 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 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",
@@ -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 = "#94a3b8", label = "Railway", dashed = TRUE)),
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 = "Transit Migration Risk Index",
823
- subtitle = "Multi-variable analysis of components of vulnerability 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,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 = "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))),
@@ -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
- var gridOn = false;
 
 
 
 
 
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) && gridOn) {
929
- gridOn = false;
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
- setTimeout(function() { if (!panel.classList.contains('open')) panel.style.display = 'none'; }, 160);
 
 
 
1055
  }
1056
 
 
 
1057
  function toggleGridPanel() {
1058
  var panel = document.getElementById('grid-panel');
1059
  if (!panel) return;
1060
- gridOn = !gridOn;
1061
- if (gridOn) {
 
 
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
- if (typeof Shiny !== 'undefined')
1075
- Shiny.setInputValue('grid_nivel', nivel, { priority: 'event' });
 
 
 
 
 
 
1076
  document.querySelectorAll('.grid-nivel-item').forEach(function(btn) {
1077
- btn.classList.toggle('active', btn.dataset.nivel === nivel);
1078
  });
1079
- // Update the trigger button's badge to reflect current nivel
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.remove('active');
1086
- gridOn = false;
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 (~500 km2, national overview) to N7 (~4 km2, local detail). Grid colours follow the active layer.",
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", "N6")
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 = "~500 km", desc = "National overview"),
1845
- "5" = list(name = "Nivel 5", size = "~100 km", desc = "Regional"),
1846
- "6" = list(name = "Nivel 6", size = "~20 km", desc = "Municipality"),
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 = paste0("grid-nivel-item", if (nv == "6") " active" else ""),
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", "Summary")),
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="network" style="width:18px;height:18px;stroke-width:2" aria-hidden="true"></i>'),
2123
- tags$span(class = "side-rail-tab-label", "Adjust")),
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>Burial pits (fosas): ", ts("fosas_total"),
2767
  "<br>Bodies (cuerpos): ", ts("cuerpos_total"))
2768
  )
2769
  }
@@ -3033,9 +3042,10 @@ server <- function(input, output, session) {
3033
  }
3034
  }, once = TRUE)
3035
 
3036
- observeEvent(input$grid_icon_click, {
3037
- new_on <- !is_grid_on()
3038
- is_grid_on(new_on)
 
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 -- {lbl} (all Mexico)")
3213
  } else {
3214
  n <- if (!is.null(rv$mun_df)) nrow(rv$mun_df) else 0
3215
- glue("Top 5 municipalities -- {lbl} ({n} in selection)")
3216
  }
3217
  })
3218
 
@@ -3446,38 +3456,76 @@ server <- function(input, output, session) {
3446
  total_label = "Total facilities")
3447
  })
3448
 
3449
- # Keep all plots/UIs computing even when their parent group is collapsed so
3450
- # they're ready the moment the user expands the group (no "blank on first
3451
- # expand"). Cost: a single extra render per session.
3452
- outputOptions(output, "temporal_pam", suspendWhenHidden = FALSE)
3453
- outputOptions(output, "temporal_plot", suspendWhenHidden = FALSE)
3454
- outputOptions(output, "temporal_ocved", suspendWhenHidden = FALSE)
3455
- outputOptions(output, "temporal_ged", suspendWhenHidden = FALSE)
3456
- outputOptions(output, "breakdown_giim", suspendWhenHidden = FALSE)
3457
- outputOptions(output, "breakdown_infra", suspendWhenHidden = FALSE)
 
 
 
 
 
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
- if (is.null(rv$dl_list)) {
3464
- writexl::write_xlsx(list(note = data.frame(note = "No selection made.")),
3465
- path = file)
3466
- return()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
  }