Upload 2 files
Browse files- 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 |
-
|
|
|
|
|
|
|
| 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:
|
| 1614 |
-
|
| 1615 |
-
|
| 1616 |
-
|
| 1617 |
-
|
| 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
|
| 4432 |
-
# selected window and saturated inside it. Single layer
|
| 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 |
-
|
|
|
|
|
|
|
| 4444 |
pl <- input$temporal_filter
|
| 4445 |
-
ym_lo <- if (is.null(pl))
|
| 4446 |
-
ym_hi <- if (is.null(pl))
|
| 4447 |
df_all$in_win <- df_all$ym >= ym_lo & df_all$ym <= ym_hi
|
| 4448 |
|
| 4449 |
-
# Year-only x ticks
|
| 4450 |
-
|
| 4451 |
-
|
| 4452 |
-
|
| 4453 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 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)) |>
|