Upload 2 files
Browse files- depth_interactive_map_alpha.R +79 -19
- 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
"ocved-pts" = list(label = "OCVED violence", color = COL_AMBER,
|
| 264 |
table = "ocved_events", year_col = "year",
|
| 265 |
-
|
|
|
|
|
|
|
| 266 |
"ged-pts" = list(label = "GED conflict", color = COL_LIGHT_BLUE,
|
| 267 |
table = "ged_events", year_col = "year",
|
| 268 |
-
|
|
|
|
|
|
|
| 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
|
| 839 |
-
|
| 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 =
|
| 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': '
|
| 1461 |
-
'ocved-pts': '
|
| 1462 |
-
'ged-pts': '
|
| 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 =
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 <-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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:
|
| 2473 |
-
left
|
| 2474 |
-
|
| 2475 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 2480 |
-
|
|
|
|
|
|
|
| 2481 |
display: flex;
|
| 2482 |
flex-direction: column;
|
| 2483 |
-
gap:
|
| 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 |
}
|