edgarodriguez commited on
Commit
f3b35c8
·
verified ·
1 Parent(s): 1a5b84a

Upload 2 files

Browse files
Files changed (2) hide show
  1. depth_interactive_map_alpha.R +79 -19
  2. styles.css +22 -7
depth_interactive_map_alpha.R CHANGED
@@ -259,13 +259,21 @@ POBREZA_COLS <- list(
259
  TEMPORAL_LAYERS <- list(
260
  "migrants-pts" = list(label = "Missing migrants", color = COL_YEL,
261
  table = "migrants", year_col = "incident_year",
262
- kind = "point"),
 
 
 
 
263
  "ocved-pts" = list(label = "OCVED violence", color = COL_AMBER,
264
  table = "ocved_events", year_col = "year",
265
- kind = "point"),
 
 
266
  "ged-pts" = list(label = "GED conflict", color = COL_LIGHT_BLUE,
267
  table = "ged_events", year_col = "year",
268
- kind = "point"),
 
 
269
  "pam-choro" = list(label = "PAM events", color = COL_COUNT,
270
  table = "dim_pam_mun",
271
  year_col = "yr",
@@ -330,6 +338,16 @@ WEB_SQL_TEMPLATES <- list(
330
  WHERE lon IS NOT NULL AND lat IS NOT NULL
331
  LIMIT {limit}",
332
 
 
 
 
 
 
 
 
 
 
 
333
  load_infra_pts =
334
  "SELECT row_number() OVER () AS row_id, lon, lat,
335
  CASE
@@ -835,13 +853,11 @@ ddb_minmax <- function(con, tbl, col, filter_zero = TRUE) {
835
  # Per-year counts for each temporal-strip layer. Each entry is a tidy
836
  # data.frame(yr, n) or NULL when the table isn't available.
837
  temporal = lapply(TEMPORAL_LAYERS, function(info) {
838
- yr_expr <- info$year_expr %||% info$year_col
839
- yr_chk <- info$year_col %||% "yr"
840
- yr_chk <- if (is.null(info$year_expr)) info$year_col else info$year_expr
841
- agg_expr <- info$agg %||% "COUNT(*)"
842
  sql <- glue::glue_data(
843
  list(yr_expr = yr_expr, agg = agg_expr,
844
- tbl = info$table, yr_col_or_expr = yr_chk),
845
  WEB_SQL_TEMPLATES$ts_yr_counts,
846
  .open = "{", .close = "}"
847
  )
@@ -1456,10 +1472,13 @@ LUCIDE_JS <- tags$head(
1456
  // first). The chart and the dual y-axis follow them. yrLo/yrHi drive the
1457
  // map filter for those layers (client-side for points, server for PAM).
1458
  var TEMPORAL_LAYER_IDS = ['migrants-pts','ocved-pts','ged-pts','pam-choro'];
 
 
 
1459
  var TEMPORAL_POINT_COL = {
1460
- 'migrants-pts': 'incident_year',
1461
- 'ocved-pts': 'year',
1462
- 'ged-pts': 'year'
1463
  };
1464
  var _temporal = {
1465
  active: [], // most-recent-first
@@ -1470,8 +1489,15 @@ LUCIDE_JS <- tags$head(
1470
  };
1471
 
1472
  function syncTemporalStrip(checkedIds) {
 
 
 
 
 
 
 
1473
  var prevActive = _temporal.active.slice();
1474
- var nowOn = (checkedIds || []).filter(function(id) {
1475
  return TEMPORAL_LAYER_IDS.indexOf(id) !== -1;
1476
  });
1477
  // Keep MRU order: anything newly turned on goes to the front; anything
@@ -1479,8 +1505,23 @@ LUCIDE_JS <- tags$head(
1479
  var stillOn = prevActive.filter(function(id) { return nowOn.indexOf(id) !== -1; });
1480
  var newOnes = nowOn.filter(function(id) { return stillOn.indexOf(id) === -1; });
1481
  _temporal.active = newOnes.concat(stillOn).slice(0, 2);
 
 
 
 
 
 
 
1482
  var strip = document.getElementById('temporal-strip');
1483
- if (strip) strip.style.display = _temporal.active.length ? 'flex' : 'none';
 
 
 
 
 
 
 
 
1484
  // Tell Shiny which layers the chart should draw (server reactive)
1485
  if (typeof Shiny !== 'undefined') {
1486
  Shiny.setInputValue('temporal_active', _temporal.active, { priority: 'event' });
@@ -1755,6 +1796,8 @@ LUCIDE_JS <- tags$head(
1755
  var iconBtn = document.getElementById('grid-icon-btn');
1756
  if (iconBtn) iconBtn.classList.toggle('active', gridActive);
1757
  closeGridPanel();
 
 
1758
  }
1759
 
1760
  // -- Legend filter: click a categorical swatch to toggle that category --
@@ -3342,6 +3385,18 @@ server <- function(input, output, session) {
3342
  error = function(e) data.frame(cat = character(0), n = integer(0)))
3343
  }
3344
 
 
 
 
 
 
 
 
 
 
 
 
 
3345
  load_pts <- function(tbl, limit = 80000) {
3346
  tryCatch({
3347
  DBI::dbGetQuery(con, web_sql("load_pts", tbl = tbl, limit = limit)) |>
@@ -3806,9 +3861,11 @@ server <- function(input, output, session) {
3806
  lazy_added[[key]] <<- TRUE
3807
  }
3808
  }
3809
- lazy_add_pts <- function(key, tbl, id, color, radius = 2, limit = 80000) {
 
3810
  if (isTRUE(lazy_added[[key]]) || is.null(con)) return()
3811
- pts <- load_pts(tbl, limit)
 
3812
  if (!is.null(pts)) {
3813
  maplibre_proxy("map") |>
3814
  add_circle_layer(
@@ -3954,11 +4011,14 @@ server <- function(input, output, session) {
3954
  line_dasharray = c(4, 2)))
3955
 
3956
  # Point layers
3957
- if ("migrants-pts" %in% active) lazy_add_pts("migrants", "migrants", "migrants-pts", COL_YEL, 2, 8000)
 
3958
  if ("graves-pts" %in% active) lazy_add_pts("graves", "graves", "graves-pts", COL_RED, 2, 5000)
3959
  if ("infra-pts" %in% active) lazy_add_infra()
3960
- if ("ocved-pts" %in% active) lazy_add_pts("ocved", "ocved_events", "ocved-pts", COL_AMBER, 1, 80000)
3961
- if ("ged-pts" %in% active) lazy_add_pts("ged", "ged_events", "ged-pts", COL_LIGHT_BLUE, 1, 80000)
 
 
3962
 
3963
  # Grid: only load the nivel the user actually selected. Loading all three
3964
  # eagerly used to make first-click block the session on HF (3 sequential
@@ -4540,7 +4600,7 @@ server <- function(input, output, session) {
4540
  "n_incidents", "n_fatalities", "n_graves", "n_facilities", "n_events",
4541
  "ocved_n", "n_ged", "n_mun", "n_desap", "n_pam", "n_fosas", "n_cuerpos",
4542
  "sub_incidents", "sub_fatalities", "panel_subtitle", "mun_section_lbl",
4543
- "mun_list"
4544
  )) {
4545
  outputOptions(output, .oid, suspendWhenHidden = FALSE)
4546
  }
 
259
  TEMPORAL_LAYERS <- list(
260
  "migrants-pts" = list(label = "Missing migrants", color = COL_YEL,
261
  table = "migrants", year_col = "incident_year",
262
+ # year_expr drives both cache + point loader. Same DuckDB
263
+ # expression in both places so the chart and the map agree.
264
+ year_expr = "incident_year",
265
+ point_yr_expr = "incident_year",
266
+ kind = "point"),
267
  "ocved-pts" = list(label = "OCVED violence", color = COL_AMBER,
268
  table = "ocved_events", year_col = "year",
269
+ year_expr = "CAST(SUBSTR(date_str, 1, 4) AS INTEGER)",
270
+ point_yr_expr = "CAST(SUBSTR(date_str, 1, 4) AS INTEGER)",
271
+ kind = "point"),
272
  "ged-pts" = list(label = "GED conflict", color = COL_LIGHT_BLUE,
273
  table = "ged_events", year_col = "year",
274
+ year_expr = "CAST(date_part('year', date_start) AS INTEGER)",
275
+ point_yr_expr = "CAST(date_part('year', date_start) AS INTEGER)",
276
+ kind = "point"),
277
  "pam-choro" = list(label = "PAM events", color = COL_COUNT,
278
  table = "dim_pam_mun",
279
  year_col = "yr",
 
338
  WHERE lon IS NOT NULL AND lat IS NOT NULL
339
  LIMIT {limit}",
340
 
341
+ # Year-bearing variant: returns a `yr` column the client filter can read.
342
+ # `yr_expr` is whatever DuckDB expression yields the integer year for that
343
+ # table (e.g. plain column, SUBSTR of date_str, date_part of timestamp).
344
+ load_pts_with_year =
345
+ "SELECT row_number() OVER () AS row_id, lon, lat,
346
+ {yr_expr} AS yr
347
+ FROM {tbl}
348
+ WHERE lon IS NOT NULL AND lat IS NOT NULL
349
+ LIMIT {limit}",
350
+
351
  load_infra_pts =
352
  "SELECT row_number() OVER () AS row_id, lon, lat,
353
  CASE
 
853
  # Per-year counts for each temporal-strip layer. Each entry is a tidy
854
  # data.frame(yr, n) or NULL when the table isn't available.
855
  temporal = lapply(TEMPORAL_LAYERS, function(info) {
856
+ yr_expr <- info$year_expr %||% info$year_col
857
+ agg_expr <- info$agg %||% "COUNT(*)"
 
 
858
  sql <- glue::glue_data(
859
  list(yr_expr = yr_expr, agg = agg_expr,
860
+ tbl = info$table, yr_col_or_expr = yr_expr),
861
  WEB_SQL_TEMPLATES$ts_yr_counts,
862
  .open = "{", .close = "}"
863
  )
 
1472
  // first). The chart and the dual y-axis follow them. yrLo/yrHi drive the
1473
  // map filter for those layers (client-side for points, server for PAM).
1474
  var TEMPORAL_LAYER_IDS = ['migrants-pts','ocved-pts','ged-pts','pam-choro'];
1475
+ // All three point layers now load a unified 'yr' property via the
1476
+ // load_pts_with_year SQL template, so the client filter doesn't have to
1477
+ // know each table's underlying year column.
1478
  var TEMPORAL_POINT_COL = {
1479
+ 'migrants-pts': 'yr',
1480
+ 'ocved-pts': 'yr',
1481
+ 'ged-pts': 'yr'
1482
  };
1483
  var _temporal = {
1484
  active: [], // most-recent-first
 
1489
  };
1490
 
1491
  function syncTemporalStrip(checkedIds) {
1492
+ // Derive the active layer set from the DOM if the caller didn't pass it.
1493
+ if (!checkedIds) {
1494
+ checkedIds = [];
1495
+ document.querySelectorAll('.layer-check:checked').forEach(function(cb) {
1496
+ checkedIds.push(cb.value);
1497
+ });
1498
+ }
1499
  var prevActive = _temporal.active.slice();
1500
+ var nowOn = checkedIds.filter(function(id) {
1501
  return TEMPORAL_LAYER_IDS.indexOf(id) !== -1;
1502
  });
1503
  // Keep MRU order: anything newly turned on goes to the front; anything
 
1505
  var stillOn = prevActive.filter(function(id) { return nowOn.indexOf(id) !== -1; });
1506
  var newOnes = nowOn.filter(function(id) { return stillOn.indexOf(id) === -1; });
1507
  _temporal.active = newOnes.concat(stillOn).slice(0, 2);
1508
+
1509
+ // Hide the strip when grid mode is on -- the year filter applies to the
1510
+ // mun-level point/polygon layers, which are not visible in grid mode.
1511
+ // (Grid layers aggregate all years; per-year grid filtering would need
1512
+ // a server re-aggregation pass that isn't built yet.)
1513
+ var stripVisible = (_temporal.active.length > 0) &&
1514
+ !(typeof gridActive !== 'undefined' && gridActive);
1515
  var strip = document.getElementById('temporal-strip');
1516
+ var becameVisible = stripVisible &&
1517
+ (!strip || strip.style.display !== 'flex');
1518
+ if (strip) strip.style.display = stripVisible ? 'flex' : 'none';
1519
+ // First time the strip is shown in this session, ensure Lucide swapped
1520
+ // the play-icon placeholder into an SVG. After that, refresh on every
1521
+ // paint so the play/pause toggle stays up to date.
1522
+ if (becameVisible && window.lucide) {
1523
+ setTimeout(function() { lucide.createIcons(); }, 30);
1524
+ }
1525
  // Tell Shiny which layers the chart should draw (server reactive)
1526
  if (typeof Shiny !== 'undefined') {
1527
  Shiny.setInputValue('temporal_active', _temporal.active, { priority: 'event' });
 
1796
  var iconBtn = document.getElementById('grid-icon-btn');
1797
  if (iconBtn) iconBtn.classList.toggle('active', gridActive);
1798
  closeGridPanel();
1799
+ // Strip auto-hides in grid mode; refresh visibility now.
1800
+ if (typeof syncTemporalStrip === 'function') syncTemporalStrip();
1801
  }
1802
 
1803
  // -- Legend filter: click a categorical swatch to toggle that category --
 
3385
  error = function(e) data.frame(cat = character(0), n = integer(0)))
3386
  }
3387
 
3388
+ # Year-bearing point loader: selects `yr` as an extra column so the client
3389
+ # filter can read it via ['get', 'yr'].
3390
+ load_pts_with_year <- function(tbl, yr_expr, limit = 80000) {
3391
+ tryCatch({
3392
+ DBI::dbGetQuery(con, web_sql("load_pts_with_year",
3393
+ tbl = tbl, yr_expr = yr_expr, limit = limit)) |>
3394
+ sf::st_as_sf(coords = c("lon", "lat"), crs = 4326)
3395
+ }, error = function(e) {
3396
+ message(" [pts+yr] ", tbl, ": ", e$message); NULL
3397
+ })
3398
+ }
3399
+
3400
  load_pts <- function(tbl, limit = 80000) {
3401
  tryCatch({
3402
  DBI::dbGetQuery(con, web_sql("load_pts", tbl = tbl, limit = limit)) |>
 
3861
  lazy_added[[key]] <<- TRUE
3862
  }
3863
  }
3864
+ lazy_add_pts <- function(key, tbl, id, color, radius = 2, limit = 80000,
3865
+ yr_expr = NULL) {
3866
  if (isTRUE(lazy_added[[key]]) || is.null(con)) return()
3867
+ pts <- if (!is.null(yr_expr)) load_pts_with_year(tbl, yr_expr, limit)
3868
+ else load_pts(tbl, limit)
3869
  if (!is.null(pts)) {
3870
  maplibre_proxy("map") |>
3871
  add_circle_layer(
 
4011
  line_dasharray = c(4, 2)))
4012
 
4013
  # Point layers
4014
+ if ("migrants-pts" %in% active) lazy_add_pts("migrants", "migrants", "migrants-pts", COL_YEL, 2, 8000,
4015
+ yr_expr = TEMPORAL_LAYERS[["migrants-pts"]]$point_yr_expr)
4016
  if ("graves-pts" %in% active) lazy_add_pts("graves", "graves", "graves-pts", COL_RED, 2, 5000)
4017
  if ("infra-pts" %in% active) lazy_add_infra()
4018
+ if ("ocved-pts" %in% active) lazy_add_pts("ocved", "ocved_events", "ocved-pts", COL_AMBER, 1, 80000,
4019
+ yr_expr = TEMPORAL_LAYERS[["ocved-pts"]]$point_yr_expr)
4020
+ if ("ged-pts" %in% active) lazy_add_pts("ged", "ged_events", "ged-pts", COL_LIGHT_BLUE, 1, 80000,
4021
+ yr_expr = TEMPORAL_LAYERS[["ged-pts"]]$point_yr_expr)
4022
 
4023
  # Grid: only load the nivel the user actually selected. Loading all three
4024
  # eagerly used to make first-click block the session on HF (3 sequential
 
4600
  "n_incidents", "n_fatalities", "n_graves", "n_facilities", "n_events",
4601
  "ocved_n", "n_ged", "n_mun", "n_desap", "n_pam", "n_fosas", "n_cuerpos",
4602
  "sub_incidents", "sub_fatalities", "panel_subtitle", "mun_section_lbl",
4603
+ "mun_list", "temporal_strip_chart"
4604
  )) {
4605
  outputOptions(output, .oid, suspendWhenHidden = FALSE)
4606
  }
styles.css CHANGED
@@ -2469,21 +2469,36 @@ body.tooltips-off .maplibregl-popup { display: none !important; }
2469
  ========================================================================= */
2470
  .temporal-strip {
2471
  position: absolute;
2472
- bottom: 0;
2473
- left: 0;
2474
- right: 0;
2475
- z-index: 950; /* below nav (1000) + popovers, above map */
 
 
 
 
 
2476
  background: oklch(15% 0.012 250 / 0.92);
2477
  backdrop-filter: blur(12px) saturate(140%);
2478
  -webkit-backdrop-filter: blur(12px) saturate(140%);
2479
- border-top: 1px solid oklch(100% 0 0 / 0.08);
2480
- padding: 6px 14px 8px;
 
 
2481
  display: flex;
2482
  flex-direction: column;
2483
- gap: 6px;
2484
  pointer-events: auto;
2485
  }
2486
 
 
 
 
 
 
 
 
 
2487
  .temporal-strip-chart {
2488
  width: 100%;
2489
  }
 
2469
  ========================================================================= */
2470
  .temporal-strip {
2471
  position: absolute;
2472
+ bottom: 18px;
2473
+ /* Leave room for the layer panel on the bottom-left and avoid covering
2474
+ the map controls on the right. Strip floats centred above the bottom. */
2475
+ left: 320px;
2476
+ right: 90px;
2477
+ max-width: 1080px;
2478
+ margin-left: auto;
2479
+ margin-right: auto;
2480
+ z-index: 950;
2481
  background: oklch(15% 0.012 250 / 0.92);
2482
  backdrop-filter: blur(12px) saturate(140%);
2483
  -webkit-backdrop-filter: blur(12px) saturate(140%);
2484
+ border: 1px solid oklch(100% 0 0 / 0.12);
2485
+ border-radius: var(--radius-md);
2486
+ box-shadow: var(--depth-3);
2487
+ padding: 6px 12px 8px;
2488
  display: flex;
2489
  flex-direction: column;
2490
+ gap: 4px;
2491
  pointer-events: auto;
2492
  }
2493
 
2494
+ /* On narrower viewports, give the strip the full bottom width */
2495
+ @media (max-width: 1100px) {
2496
+ .temporal-strip {
2497
+ left: 16px;
2498
+ right: 16px;
2499
+ }
2500
+ }
2501
+
2502
  .temporal-strip-chart {
2503
  width: 100%;
2504
  }