edgarodriguez commited on
Commit
a9ea738
·
verified ·
1 Parent(s): 457aa8d

Upload 2 files

Browse files
Files changed (1) hide show
  1. depth_interactive_map_alpha.R +119 -22
depth_interactive_map_alpha.R CHANGED
@@ -1116,8 +1116,10 @@ WEB_SPEC <- list(
1116
  # Variant legends shown when the matching categorical chip is active.
1117
  # data-variant-key on each wrap is matched by setPATPType in JS.
1118
  legend_extras = list(
1119
- pr_pred_loc_rur = list(type = "cats", pal = "patp_rural"),
1120
- delitos_fc_b_j_a = list(type = "cats", pal = "patp_delito")
 
 
1121
  )),
1122
  list(kind = "radios", group = "patp"),
1123
  list(kind = "layer", value = "pobreza-choro",
@@ -1497,6 +1499,20 @@ LUCIDE_JS <- tags$head(
1497
  }
1498
  }
1499
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1500
  }
1501
 
1502
  // -- Temporal strip: bottom-of-map month-year range filter ------------------
@@ -1598,6 +1614,11 @@ LUCIDE_JS <- tags$head(
1598
  }
1599
  }
1600
 
 
 
 
 
 
1601
  function pushTemporalFilter() {
1602
  var startEl = document.getElementById('temporal-start');
1603
  var endEl = document.getElementById('temporal-end');
@@ -1606,16 +1627,22 @@ LUCIDE_JS <- tags$head(
1606
  var ymHi = parseInt(endEl.value, 10);
1607
  if (ymLo > ymHi) { var t = ymLo; ymLo = ymHi; ymHi = t; }
1608
  _temporal.ymLo = ymLo; _temporal.ymHi = ymHi;
1609
- // Client: apply for each active POINT layer
1610
  _temporal.active.forEach(function(id) {
1611
  if (TEMPORAL_POINT_COL[id]) applyTemporalFilterClient(id, ymLo, ymHi);
1612
  });
1613
- // Server: send range so PAM can re-query if it's active
1614
- if (typeof Shiny !== 'undefined') {
1615
- Shiny.setInputValue('temporal_filter', {
1616
- start: ymLo, end: ymHi, active: _temporal.active, ts: Date.now()
1617
- }, { priority: 'event' });
1618
- }
 
 
 
 
 
 
1619
  }
1620
 
1621
  function onTemporalSliderInput() {
@@ -4186,6 +4213,40 @@ server <- function(input, output, session) {
4186
  }
4187
  }, ignoreInit = TRUE, ignoreNULL = TRUE)
4188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4189
  # --- Nav-bar search: fly to picked result -----------------------------------
4190
  observeEvent(input$search_flyto, {
4191
  pt <- input$search_flyto
@@ -4428,9 +4489,9 @@ server <- function(input, output, session) {
4428
  output$temporal_ged <- make_temporal_yr("ged_events", "year", COL_LIGHT_BLUE)
4429
 
4430
  # --- Temporal strip chart (bottom-of-map month-year range filter) -----------
4431
- # Honeycomb-style: monthly bars across the full timespan, dim outside the
4432
- # selected window and saturated inside it. Single layer (most-recently
4433
- # toggled). No y-axis, no legend, compact theme.
4434
  output$temporal_strip_chart <- renderPlot({
4435
  active <- input$temporal_active %||% character(0)
4436
  if (length(active) == 0) return(NULL)
@@ -4440,17 +4501,23 @@ server <- function(input, output, session) {
4440
  info <- TEMPORAL_LAYERS[[primary_id]]
4441
  bar_color <- info$color
4442
 
4443
- ym_range <- TEMPORAL_RANGE
 
 
4444
  pl <- input$temporal_filter
4445
- ym_lo <- if (is.null(pl)) ym_range[1] else (pl$start %||% ym_range[1])
4446
- ym_hi <- if (is.null(pl)) ym_range[2] else (pl$end %||% ym_range[2])
4447
  df_all$in_win <- df_all$ym >= ym_lo & df_all$ym <= ym_hi
4448
 
4449
- # Year-only x ticks across the full slider span.
4450
- yr_lo <- ym_range[1] %/% 12L
4451
- yr_hi <- ym_range[2] %/% 12L
4452
- yr_seq <- seq(yr_lo, yr_hi,
4453
- by = if (yr_hi - yr_lo > 12) 2L else 1L)
 
 
 
 
4454
  yr_breaks <- yr_seq * 12L
4455
  yr_labels <- as.character(yr_seq)
4456
 
@@ -4459,7 +4526,7 @@ server <- function(input, output, session) {
4459
  scale_fill_manual(values = c(`FALSE` = "#ffffff22",
4460
  `TRUE` = bar_color)) +
4461
  scale_x_continuous(breaks = yr_breaks, labels = yr_labels,
4462
- limits = c(ym_range[1] - 0.5, ym_range[2] + 0.5),
4463
  expand = expansion(mult = c(0, 0))) +
4464
  scale_y_continuous(expand = expansion(mult = c(0, 0.05))) +
4465
  labs(x = NULL, y = NULL) +
@@ -4475,9 +4542,28 @@ server <- function(input, output, session) {
4475
  plot.margin = margin(0, 4, 0, 4))
4476
  }, bg = "transparent", res = 110)
4477
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4478
  # PAM month-window re-aggregation: when the strip range changes and PAM is
4479
  # active, re-query src_pam_ym + push the new fill via the proxy. The current
4480
- # layer is removed and re-added with the ym-filtered source.
 
 
 
4481
  observeEvent(input$temporal_filter, {
4482
  pl <- input$temporal_filter
4483
  if (is.null(pl)) return()
@@ -4485,6 +4571,17 @@ server <- function(input, output, session) {
4485
  if (!("pam-choro" %in% active) || !isTRUE(lazy_added$pam)) return()
4486
  ym_lo <- pl$start %||% TEMPORAL_RANGE[1]
4487
  ym_hi <- pl$end %||% TEMPORAL_RANGE[2]
 
 
 
 
 
 
 
 
 
 
 
4488
  sf_data <- tryCatch({
4489
  DBI::dbGetQuery(con,
4490
  web_sql("src_pam_ym", ym_lo = ym_lo, ym_hi = ym_hi)) |>
 
1116
  # Variant legends shown when the matching categorical chip is active.
1117
  # data-variant-key on each wrap is matched by setPATPType in JS.
1118
  legend_extras = list(
1119
+ pr_pred_loc_rur = list(type = "cats", pal = "patp_rural",
1120
+ filterable = "patp_rural"),
1121
+ delitos_fc_b_j_a = list(type = "cats", pal = "patp_delito",
1122
+ filterable = "patp_delito")
1123
  )),
1124
  list(kind = "radios", group = "patp"),
1125
  list(kind = "layer", value = "pobreza-choro",
 
1499
  }
1500
  }
1501
  });
1502
+ // Server sends `temporal_range` whenever the most-recent temporal layer
1503
+ // changes; payload is the layer's actual ym data span. Resize the
1504
+ // slider, reset the handles to the new full range, and clear any
1505
+ // residual map filter so the user sees the whole layer immediately.
1506
+ Shiny.addCustomMessageHandler('temporal_range', function(msg) {
1507
+ if (!msg) return;
1508
+ var startEl = document.getElementById('temporal-start');
1509
+ var endEl = document.getElementById('temporal-end');
1510
+ if (!startEl || !endEl) return;
1511
+ var lo = parseInt(msg.min, 10), hi = parseInt(msg.max, 10);
1512
+ startEl.min = lo; startEl.max = hi; startEl.value = lo;
1513
+ endEl.min = lo; endEl.max = hi; endEl.value = hi;
1514
+ onTemporalSliderInput();
1515
+ });
1516
  }
1517
 
1518
  // -- Temporal strip: bottom-of-map month-year range filter ------------------
 
1614
  }
1615
  }
1616
 
1617
+ // Server-side filter (PAM polygon re-aggregation) is heavy: each call runs
1618
+ // a 2475-row SQL query + remove/re-add of the WebGL layer. Dragging the
1619
+ // slider would otherwise queue dozens of these. Client-side point filters
1620
+ // are cheap (setFilter on existing features) and stay synchronous.
1621
+ var _temporalServerTimer = null;
1622
  function pushTemporalFilter() {
1623
  var startEl = document.getElementById('temporal-start');
1624
  var endEl = document.getElementById('temporal-end');
 
1627
  var ymHi = parseInt(endEl.value, 10);
1628
  if (ymLo > ymHi) { var t = ymLo; ymLo = ymHi; ymHi = t; }
1629
  _temporal.ymLo = ymLo; _temporal.ymHi = ymHi;
1630
+ // Client: apply for each active POINT layer (cheap, every tick).
1631
  _temporal.active.forEach(function(id) {
1632
  if (TEMPORAL_POINT_COL[id]) applyTemporalFilterClient(id, ymLo, ymHi);
1633
  });
1634
+ // Server: debounce so PAM re-aggregation only runs after the user
1635
+ // pauses for 200ms. Always send the latest range when the timer fires.
1636
+ if (_temporalServerTimer) clearTimeout(_temporalServerTimer);
1637
+ _temporalServerTimer = setTimeout(function() {
1638
+ _temporalServerTimer = null;
1639
+ if (typeof Shiny !== 'undefined') {
1640
+ Shiny.setInputValue('temporal_filter', {
1641
+ start: _temporal.ymLo, end: _temporal.ymHi,
1642
+ active: _temporal.active, ts: Date.now()
1643
+ }, { priority: 'event' });
1644
+ }
1645
+ }, 200);
1646
  }
1647
 
1648
  function onTemporalSliderInput() {
 
4213
  }
4214
  }, ignoreInit = TRUE, ignoreNULL = TRUE)
4215
 
4216
+ # --- Legend filter: PATP-CONEVAL categorical chips --------------------------
4217
+ # Same pattern as infra: clicking a swatch toggles its category off; the
4218
+ # filter is applied to the muni-level `patp-choro` layer on the column that
4219
+ # matches the variant (pr_pred_loc_rur for rural problem, delitos_fc_b_j_a
4220
+ # for crime focus). Grid-mode patp layers are not filtered -- they aggregate
4221
+ # per cell and don't carry the raw categorical column.
4222
+ apply_patp_cat_filter <- function(col_name, pal_cats, visible) {
4223
+ if (!isTRUE(lazy_added$patp)) return()
4224
+ prx <- maplibre_proxy("map")
4225
+ if (length(visible) == 0) {
4226
+ prx |> set_filter("patp-choro", list("==", list("get", col_name), "__none__"))
4227
+ } else if (length(visible) >= length(pal_cats)) {
4228
+ prx |> set_filter("patp-choro", NULL)
4229
+ } else {
4230
+ prx |> set_filter("patp-choro",
4231
+ list("in", list("get", col_name),
4232
+ list("literal", as.list(visible))))
4233
+ }
4234
+ }
4235
+ observeEvent(input$patp_rural_filter, {
4236
+ pl <- input$patp_rural_filter
4237
+ if (is.null(pl)) return()
4238
+ apply_patp_cat_filter("pr_pred_loc_rur",
4239
+ PATP_RURAL_PROBLEM_PAL,
4240
+ as.character(pl$cats %||% character(0)))
4241
+ }, ignoreInit = TRUE, ignoreNULL = TRUE)
4242
+ observeEvent(input$patp_delito_filter, {
4243
+ pl <- input$patp_delito_filter
4244
+ if (is.null(pl)) return()
4245
+ apply_patp_cat_filter("delitos_fc_b_j_a",
4246
+ PATP_DELITO_PAL,
4247
+ as.character(pl$cats %||% character(0)))
4248
+ }, ignoreInit = TRUE, ignoreNULL = TRUE)
4249
+
4250
  # --- Nav-bar search: fly to picked result -----------------------------------
4251
  observeEvent(input$search_flyto, {
4252
  pt <- input$search_flyto
 
4489
  output$temporal_ged <- make_temporal_yr("ged_events", "year", COL_LIGHT_BLUE)
4490
 
4491
  # --- Temporal strip chart (bottom-of-map month-year range filter) -----------
4492
+ # Honeycomb-style: monthly bars across the layer's actual data span, dim
4493
+ # outside the selected window and saturated inside it. Single layer
4494
+ # (most-recently toggled). No y-axis, no legend, compact theme.
4495
  output$temporal_strip_chart <- renderPlot({
4496
  active <- input$temporal_active %||% character(0)
4497
  if (length(active) == 0) return(NULL)
 
4501
  info <- TEMPORAL_LAYERS[[primary_id]]
4502
  bar_color <- info$color
4503
 
4504
+ # X-axis spans the layer's actual data range, not the global slider span.
4505
+ ym_data_lo <- min(df_all$ym, na.rm = TRUE)
4506
+ ym_data_hi <- max(df_all$ym, na.rm = TRUE)
4507
  pl <- input$temporal_filter
4508
+ ym_lo <- if (is.null(pl)) ym_data_lo else (pl$start %||% ym_data_lo)
4509
+ ym_hi <- if (is.null(pl)) ym_data_hi else (pl$end %||% ym_data_hi)
4510
  df_all$in_win <- df_all$ym >= ym_lo & df_all$ym <= ym_hi
4511
 
4512
+ # Year-only x ticks scaled to the actual data span. Pick a step that
4513
+ # produces 5-9 labels so they don't crowd on short spans.
4514
+ yr_lo <- ym_data_lo %/% 12L
4515
+ yr_hi <- ym_data_hi %/% 12L
4516
+ yr_span <- yr_hi - yr_lo
4517
+ yr_step <- if (yr_span <= 6) 1L
4518
+ else if (yr_span <= 14) 2L
4519
+ else 3L
4520
+ yr_seq <- seq(yr_lo, yr_hi, by = yr_step)
4521
  yr_breaks <- yr_seq * 12L
4522
  yr_labels <- as.character(yr_seq)
4523
 
 
4526
  scale_fill_manual(values = c(`FALSE` = "#ffffff22",
4527
  `TRUE` = bar_color)) +
4528
  scale_x_continuous(breaks = yr_breaks, labels = yr_labels,
4529
+ limits = c(ym_data_lo - 0.5, ym_data_hi + 0.5),
4530
  expand = expansion(mult = c(0, 0))) +
4531
  scale_y_continuous(expand = expansion(mult = c(0, 0.05))) +
4532
  labs(x = NULL, y = NULL) +
 
4542
  plot.margin = margin(0, 4, 0, 4))
4543
  }, bg = "transparent", res = 110)
4544
 
4545
+ # When the active temporal layer changes (most-recent toggled), push that
4546
+ # layer's actual data span to the JS slider so the handles + x-axis labels
4547
+ # bracket only the months that have data. Switching from OCVED (2010-2018)
4548
+ # to GED (2019-2024) re-scopes both the slider and the chart in one step.
4549
+ observeEvent(input$temporal_active, {
4550
+ ids <- input$temporal_active %||% character(0)
4551
+ if (length(ids) == 0) return()
4552
+ primary <- ids[1]
4553
+ df <- .APP_CACHE$temporal[[primary]]
4554
+ if (is.null(df) || nrow(df) == 0) return()
4555
+ session$sendCustomMessage("temporal_range", list(
4556
+ min = as.integer(min(df$ym, na.rm = TRUE)),
4557
+ max = as.integer(max(df$ym, na.rm = TRUE))
4558
+ ))
4559
+ }, ignoreInit = TRUE)
4560
+
4561
  # PAM month-window re-aggregation: when the strip range changes and PAM is
4562
  # active, re-query src_pam_ym + push the new fill via the proxy. The current
4563
+ # layer is removed and re-added with the ym-filtered source. The observer is
4564
+ # JS-debounced (200ms) so dragging the slider doesn't queue dozens of heavy
4565
+ # re-aggregations. Full-range windows short-circuit -- the unfiltered layer
4566
+ # already on the map IS the answer for the full range, no re-query needed.
4567
  observeEvent(input$temporal_filter, {
4568
  pl <- input$temporal_filter
4569
  if (is.null(pl)) return()
 
4571
  if (!("pam-choro" %in% active) || !isTRUE(lazy_added$pam)) return()
4572
  ym_lo <- pl$start %||% TEMPORAL_RANGE[1]
4573
  ym_hi <- pl$end %||% TEMPORAL_RANGE[2]
4574
+ # Short-circuit: full-range (relative to PAM's actual data span) = no
4575
+ # filtering = leave the unfiltered layer alone. This is what prevents
4576
+ # PAM-on freezing the app: the very first syncTemporalStrip after
4577
+ # lazy_add_fill fires temporal_filter with the default full range, and
4578
+ # without this guard the heavy 2475-row aggregate ran a second time
4579
+ # and triggered a remove+re-add on a layer still being committed to
4580
+ # the MapLibre source.
4581
+ df_pam <- .APP_CACHE$temporal[["pam-choro"]]
4582
+ pam_data_lo <- if (!is.null(df_pam)) min(df_pam$ym) else TEMPORAL_RANGE[1]
4583
+ pam_data_hi <- if (!is.null(df_pam)) max(df_pam$ym) else TEMPORAL_RANGE[2]
4584
+ if (ym_lo <= pam_data_lo && ym_hi >= pam_data_hi) return()
4585
  sf_data <- tryCatch({
4586
  DBI::dbGetQuery(con,
4587
  web_sql("src_pam_ym", ym_lo = ym_lo, ym_hi = ym_hi)) |>