Upload 2 files
Browse files- depth_interactive_map_alpha.R +128 -58
- styles.css +126 -1
depth_interactive_map_alpha.R
CHANGED
|
@@ -847,13 +847,15 @@ WEB_SPEC <- list(
|
|
| 847 |
list(id = "exposure", label = "Exposure", open = FALSE, cards = list(
|
| 848 |
list(title = "Irregular Migration Events (PAM)", kind = "stat", chart = "temporal_pam",
|
| 849 |
stats = list(list(id = "n_pam", label = "Recorded irregular migration events", color = COL_COUNT))),
|
| 850 |
-
list(title = "Migration Intensity (GIIM)", kind = "
|
| 851 |
-
|
|
|
|
| 852 |
)),
|
| 853 |
-
list(id = "vulnerability", label = "Vulnerability", open =
|
| 854 |
-
list(title = "Migrant Support Facilities", kind = "
|
|
|
|
| 855 |
stats = list(list(id = "n_facilities", label = "Active support facilities", color = COL_GREEN)),
|
| 856 |
-
note = "Composition of support facilities by type
|
| 857 |
))
|
| 858 |
)
|
| 859 |
)
|
|
@@ -1152,8 +1154,15 @@ LUCIDE_JS <- tags$head(
|
|
| 1152 |
// We hide them via CSS but keep them in the DOM so .click() still triggers
|
| 1153 |
// the underlying draw.changeMode() / draw mode behaviour. Our Lucide-icon
|
| 1154 |
// buttons in #nav-tools-select call drawMode() which forwards the click.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1155 |
function drawMode(modeKey) {
|
| 1156 |
-
var sel =
|
| 1157 |
var nativeBtn = document.querySelector(sel);
|
| 1158 |
if (!nativeBtn) {
|
| 1159 |
// Native draw control not yet mounted -- retry briefly
|
|
@@ -1170,7 +1179,7 @@ LUCIDE_JS <- tags$head(
|
|
| 1170 |
|
| 1171 |
function syncDrawButtonStates() {
|
| 1172 |
['polygon', 'rectangle', 'radius', 'line_string'].forEach(function(mode) {
|
| 1173 |
-
var native = document.querySelector(
|
| 1174 |
var custom = document.querySelector('[data-draw=\"' + mode + '\"]');
|
| 1175 |
if (native && custom) {
|
| 1176 |
custom.classList.toggle('active', native.classList.contains('active'));
|
|
@@ -1178,12 +1187,9 @@ LUCIDE_JS <- tags$head(
|
|
| 1178 |
});
|
| 1179 |
}
|
| 1180 |
|
| 1181 |
-
// Watch native draw buttons so our custom state mirrors mode changes
|
| 1182 |
-
// triggered outside our buttons (e.g. when mapgl reverts to simple_select
|
| 1183 |
-
// after a polygon is completed).
|
| 1184 |
function watchNativeDrawButtons() {
|
| 1185 |
var natives = document.querySelectorAll(
|
| 1186 |
-
'.mapbox-gl-draw_polygon, .mapbox-gl-draw_rectangle, .mapbox-gl-draw_radius, .mapbox-gl-
|
| 1187 |
);
|
| 1188 |
if (!natives.length) return false;
|
| 1189 |
var obs = new MutationObserver(syncDrawButtonStates);
|
|
@@ -1671,16 +1677,17 @@ render_panel_card <- function(card) {
|
|
| 1671 |
stat = tagList(
|
| 1672 |
lapply(card$stats, function(s)
|
| 1673 |
dp_stat(s$id, s$label, s$color %||% "#1e293b"))),
|
| 1674 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1675 |
)
|
| 1676 |
-
# Donut cards are taller (the chart IS the body); other cards keep the
|
| 1677 |
-
# compact 55px sparkline. Half cards have slightly less chrome.
|
| 1678 |
-
chart_h <- if (identical(card$kind, "donut")) "180px" else "55px"
|
| 1679 |
ind_card(card$title, body,
|
| 1680 |
chart_id = card$chart,
|
| 1681 |
note = card$note,
|
| 1682 |
half = isTRUE(card$half),
|
| 1683 |
-
chart_height =
|
| 1684 |
}
|
| 1685 |
|
| 1686 |
render_panel_groups <- function() {
|
|
@@ -2178,9 +2185,29 @@ server <- function(input, output, session) {
|
|
| 2178 |
mun_df = NULL,
|
| 2179 |
dl_list = NULL,
|
| 2180 |
is_national = TRUE,
|
| 2181 |
-
temporal_filter_wkt = NULL
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2182 |
)
|
| 2183 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2184 |
# Quantile-break cache: pre-populated from the app-level cache (loaded at startup).
|
| 2185 |
# Falls back to an empty list if the cache failed; compute_qbreaks() fills gaps lazily.
|
| 2186 |
qb <- .APP_CACHE$qb
|
|
@@ -2411,6 +2438,12 @@ server <- function(input, output, session) {
|
|
| 2411 |
|
| 2412 |
rv$mun_df <- tryCatch(DBI::dbGetQuery(con, web_sql("mun_table")),
|
| 2413 |
error = function(e) data.frame())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2414 |
}
|
| 2415 |
|
| 2416 |
load_pts <- function(tbl, limit = 80000) {
|
|
@@ -2496,6 +2529,25 @@ server <- function(input, output, session) {
|
|
| 2496 |
rv$n_fosas <- fmt_n(fosas_sum$fosas_n)
|
| 2497 |
rv$n_cuerpos <- fmt_n(fosas_sum$cuerpos_n)
|
| 2498 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2499 |
rv$dl_list <- list(
|
| 2500 |
migrants = migrants_df, graves = graves_df,
|
| 2501 |
infra = infra_df, ocved = ocved_df,
|
|
@@ -3340,51 +3392,69 @@ server <- function(input, output, session) {
|
|
| 3340 |
)
|
| 3341 |
}, bg = "transparent", res = 96)
|
| 3342 |
|
| 3343 |
-
# ---
|
| 3344 |
-
# Renders a
|
| 3345 |
-
#
|
| 3346 |
-
#
|
| 3347 |
-
|
| 3348 |
-
|
| 3349 |
-
df
|
| 3350 |
-
|
| 3351 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3352 |
total <- sum(df$n)
|
| 3353 |
-
|
| 3354 |
-
|
| 3355 |
-
|
| 3356 |
-
|
| 3357 |
-
|
| 3358 |
-
|
| 3359 |
-
|
| 3360 |
-
|
| 3361 |
-
|
| 3362 |
-
|
| 3363 |
-
|
| 3364 |
-
|
| 3365 |
-
|
| 3366 |
-
|
| 3367 |
-
|
| 3368 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3369 |
}
|
| 3370 |
|
| 3371 |
-
|
| 3372 |
-
|
| 3373 |
-
|
| 3374 |
-
|
| 3375 |
-
error = function(e) NULL)
|
| 3376 |
-
donut_plot(df, GIIM_COLORS,
|
| 3377 |
-
center_text = if (!is.null(df)) format(sum(df$n), big.mark = ",") else NULL)
|
| 3378 |
-
}, bg = "transparent", res = 96)
|
| 3379 |
|
| 3380 |
-
|
| 3381 |
-
|
| 3382 |
-
|
| 3383 |
-
|
| 3384 |
-
|
| 3385 |
-
|
| 3386 |
-
|
| 3387 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3388 |
|
| 3389 |
output$dl_xlsx <- downloadHandler(
|
| 3390 |
filename = function()
|
|
|
|
| 847 |
list(id = "exposure", label = "Exposure", open = FALSE, cards = list(
|
| 848 |
list(title = "Irregular Migration Events (PAM)", kind = "stat", chart = "temporal_pam",
|
| 849 |
stats = list(list(id = "n_pam", label = "Recorded irregular migration events", color = COL_COUNT))),
|
| 850 |
+
list(title = "Migration Intensity (GIIM)", kind = "breakdown",
|
| 851 |
+
ui_id = "breakdown_giim",
|
| 852 |
+
note = "Number of municipalities by intensity of migration to the USA. Filters apply.")
|
| 853 |
)),
|
| 854 |
+
list(id = "vulnerability", label = "Vulnerability", open = TRUE, cards = list(
|
| 855 |
+
list(title = "Migrant Support Facilities", kind = "breakdown",
|
| 856 |
+
ui_id = "breakdown_infra",
|
| 857 |
stats = list(list(id = "n_facilities", label = "Active support facilities", color = COL_GREEN)),
|
| 858 |
+
note = "Composition of support facilities by type. Filters apply.")
|
| 859 |
))
|
| 860 |
)
|
| 861 |
)
|
|
|
|
| 1154 |
// We hide them via CSS but keep them in the DOM so .click() still triggers
|
| 1155 |
// the underlying draw.changeMode() / draw mode behaviour. Our Lucide-icon
|
| 1156 |
// buttons in #nav-tools-select call drawMode() which forwards the click.
|
| 1157 |
+
// The mode key on our custom buttons matches the maplibre draw mode name
|
| 1158 |
+
// ('polygon', 'rectangle', 'radius', 'line_string'), but the native button
|
| 1159 |
+
// class for line_string is just '.mapbox-gl-draw_line' (no '_string').
|
| 1160 |
+
function nativeDrawClass(modeKey) {
|
| 1161 |
+
return '.mapbox-gl-draw_' + (modeKey === 'line_string' ? 'line' : modeKey);
|
| 1162 |
+
}
|
| 1163 |
+
|
| 1164 |
function drawMode(modeKey) {
|
| 1165 |
+
var sel = nativeDrawClass(modeKey);
|
| 1166 |
var nativeBtn = document.querySelector(sel);
|
| 1167 |
if (!nativeBtn) {
|
| 1168 |
// Native draw control not yet mounted -- retry briefly
|
|
|
|
| 1179 |
|
| 1180 |
function syncDrawButtonStates() {
|
| 1181 |
['polygon', 'rectangle', 'radius', 'line_string'].forEach(function(mode) {
|
| 1182 |
+
var native = document.querySelector(nativeDrawClass(mode));
|
| 1183 |
var custom = document.querySelector('[data-draw=\"' + mode + '\"]');
|
| 1184 |
if (native && custom) {
|
| 1185 |
custom.classList.toggle('active', native.classList.contains('active'));
|
|
|
|
| 1187 |
});
|
| 1188 |
}
|
| 1189 |
|
|
|
|
|
|
|
|
|
|
| 1190 |
function watchNativeDrawButtons() {
|
| 1191 |
var natives = document.querySelectorAll(
|
| 1192 |
+
'.mapbox-gl-draw_polygon, .mapbox-gl-draw_rectangle, .mapbox-gl-draw_radius, .mapbox-gl-draw_line'
|
| 1193 |
);
|
| 1194 |
if (!natives.length) return false;
|
| 1195 |
var obs = new MutationObserver(syncDrawButtonStates);
|
|
|
|
| 1677 |
stat = tagList(
|
| 1678 |
lapply(card$stats, function(s)
|
| 1679 |
dp_stat(s$id, s$label, s$color %||% "#1e293b"))),
|
| 1680 |
+
breakdown = tagList(
|
| 1681 |
+
# Optional stat-row(s) above the list (e.g. total active facilities)
|
| 1682 |
+
if (!is.null(card$stats)) lapply(card$stats, function(s)
|
| 1683 |
+
dp_stat(s$id, s$label, s$color %||% "#1e293b")),
|
| 1684 |
+
uiOutput(card$ui_id %||% paste0("breakdown_", card$ui_id)))
|
| 1685 |
)
|
|
|
|
|
|
|
|
|
|
| 1686 |
ind_card(card$title, body,
|
| 1687 |
chart_id = card$chart,
|
| 1688 |
note = card$note,
|
| 1689 |
half = isTRUE(card$half),
|
| 1690 |
+
chart_height = "55px")
|
| 1691 |
}
|
| 1692 |
|
| 1693 |
render_panel_groups <- function() {
|
|
|
|
| 2185 |
mun_df = NULL,
|
| 2186 |
dl_list = NULL,
|
| 2187 |
is_national = TRUE,
|
| 2188 |
+
temporal_filter_wkt = NULL,
|
| 2189 |
+
# Category distributions for the breakdown cards (GIIM + infra). Both are
|
| 2190 |
+
# data.frame(cat, n) -- repopulated by load_national_totals + run_analysis.
|
| 2191 |
+
dist_giim = NULL,
|
| 2192 |
+
dist_infra = NULL
|
| 2193 |
)
|
| 2194 |
|
| 2195 |
+
# Normalise the raw 13 `category` strings into 7 user-meaningful buckets
|
| 2196 |
+
# (mirrors the SQL CASE in WEB_SQL_TEMPLATES$load_infra_pts).
|
| 2197 |
+
normalize_infra_cat <- function(category) {
|
| 2198 |
+
if (length(category) == 0) return(character(0))
|
| 2199 |
+
c_low <- tolower(category)
|
| 2200 |
+
out <- ifelse(category == "Shelter", "Shelter",
|
| 2201 |
+
ifelse(c_low %in% c("food","restaurant","lodging"), "Food & Lodging",
|
| 2202 |
+
ifelse(c_low %in% c("government","embassy"), "Government",
|
| 2203 |
+
ifelse(category == "Legal/NGO" | c_low == "charity", "Legal & Aid",
|
| 2204 |
+
ifelse(c_low == "police", "Police",
|
| 2205 |
+
ifelse(c_low %in% c("hospital","medical"), "Health",
|
| 2206 |
+
ifelse(c_low %in% c("religious","place-of-worship"), "Religious",
|
| 2207 |
+
"Other")))))))
|
| 2208 |
+
out
|
| 2209 |
+
}
|
| 2210 |
+
|
| 2211 |
# Quantile-break cache: pre-populated from the app-level cache (loaded at startup).
|
| 2212 |
# Falls back to an empty list if the cache failed; compute_qbreaks() fills gaps lazily.
|
| 2213 |
qb <- .APP_CACHE$qb
|
|
|
|
| 2438 |
|
| 2439 |
rv$mun_df <- tryCatch(DBI::dbGetQuery(con, web_sql("mun_table")),
|
| 2440 |
error = function(e) data.frame())
|
| 2441 |
+
|
| 2442 |
+
# National GIIM and infra distributions for the breakdown cards.
|
| 2443 |
+
rv$dist_giim <- tryCatch(DBI::dbGetQuery(con, web_sql("donut_giim")),
|
| 2444 |
+
error = function(e) data.frame(cat = character(0), n = integer(0)))
|
| 2445 |
+
rv$dist_infra <- tryCatch(DBI::dbGetQuery(con, web_sql("donut_infra")),
|
| 2446 |
+
error = function(e) data.frame(cat = character(0), n = integer(0)))
|
| 2447 |
}
|
| 2448 |
|
| 2449 |
load_pts <- function(tbl, limit = 80000) {
|
|
|
|
| 2529 |
rv$n_fosas <- fmt_n(fosas_sum$fosas_n)
|
| 2530 |
rv$n_cuerpos <- fmt_n(fosas_sum$cuerpos_n)
|
| 2531 |
|
| 2532 |
+
# GIIM distribution among the selected municipalities (uses giim_mun from
|
| 2533 |
+
# mun_df, which we already pulled via mun_in_poly).
|
| 2534 |
+
rv$dist_giim <- if (nrow(mun_df) > 0 && "giim_mun" %in% names(mun_df)) {
|
| 2535 |
+
tab <- as.data.frame(table(mun_df$giim_mun, useNA = "no"),
|
| 2536 |
+
stringsAsFactors = FALSE)
|
| 2537 |
+
names(tab) <- c("cat", "n")
|
| 2538 |
+
tab[order(-tab$n), , drop = FALSE]
|
| 2539 |
+
} else data.frame(cat = character(0), n = integer(0))
|
| 2540 |
+
|
| 2541 |
+
# Infra distribution: count facility points inside the drawn polygon by
|
| 2542 |
+
# normalised category bucket.
|
| 2543 |
+
rv$dist_infra <- if (!is.null(infra_df) && nrow(infra_df) > 0 &&
|
| 2544 |
+
"category" %in% names(infra_df)) {
|
| 2545 |
+
cats <- normalize_infra_cat(infra_df$category)
|
| 2546 |
+
tab <- as.data.frame(table(cats, useNA = "no"), stringsAsFactors = FALSE)
|
| 2547 |
+
names(tab) <- c("cat", "n")
|
| 2548 |
+
tab[order(-tab$n), , drop = FALSE]
|
| 2549 |
+
} else data.frame(cat = character(0), n = integer(0))
|
| 2550 |
+
|
| 2551 |
rv$dl_list <- list(
|
| 2552 |
migrants = migrants_df, graves = graves_df,
|
| 2553 |
infra = infra_df, ocved = ocved_df,
|
|
|
|
| 3392 |
)
|
| 3393 |
}, bg = "transparent", res = 96)
|
| 3394 |
|
| 3395 |
+
# --- Breakdown card builder ------------------------------------------------
|
| 3396 |
+
# Renders a coloured-text list: swatch | label | tabular value | proportion
|
| 3397 |
+
# bar. Inspired by GitHub's language-breakdown widget. Each row uses the
|
| 3398 |
+
# palette colour for the swatch + bar so the card is visually paired with
|
| 3399 |
+
# the matching map layer.
|
| 3400 |
+
build_breakdown <- function(df, palette, total_label = "Total") {
|
| 3401 |
+
if (is.null(df) || nrow(df) == 0) {
|
| 3402 |
+
return(div(class = "breakdown-empty", "No data in the current selection"))
|
| 3403 |
+
}
|
| 3404 |
+
df <- df[!is.na(df$cat) & df$n > 0, , drop = FALSE]
|
| 3405 |
+
if (nrow(df) == 0)
|
| 3406 |
+
return(div(class = "breakdown-empty", "No data in the current selection"))
|
| 3407 |
+
# Order: keep the palette order so the visual matches the layer legend.
|
| 3408 |
+
ord <- match(df$cat, names(palette))
|
| 3409 |
+
df <- df[order(ord, na.last = TRUE), , drop = FALSE]
|
| 3410 |
total <- sum(df$n)
|
| 3411 |
+
max_n <- max(df$n)
|
| 3412 |
+
rows <- lapply(seq_len(nrow(df)), function(i) {
|
| 3413 |
+
nm <- df$cat[i]
|
| 3414 |
+
n <- df$n[i]
|
| 3415 |
+
pct <- if (total > 0) n / total else 0
|
| 3416 |
+
bar <- if (max_n > 0) n / max_n else 0
|
| 3417 |
+
col <- palette[nm] %||% "#94a3b8"
|
| 3418 |
+
div(class = "breakdown-row",
|
| 3419 |
+
span(class = "breakdown-swatch", style = paste0("background:", col)),
|
| 3420 |
+
span(class = "breakdown-label", nm),
|
| 3421 |
+
span(class = "breakdown-val",
|
| 3422 |
+
format(n, big.mark = ",", scientific = FALSE)),
|
| 3423 |
+
span(class = "breakdown-pct",
|
| 3424 |
+
formatC(pct * 100, format = "f", digits = 1), "%"),
|
| 3425 |
+
div(class = "breakdown-bar",
|
| 3426 |
+
div(class = "breakdown-fill",
|
| 3427 |
+
style = paste0("width:", bar * 100, "%; background:", col)))
|
| 3428 |
+
)
|
| 3429 |
+
})
|
| 3430 |
+
tagList(
|
| 3431 |
+
div(class = "breakdown-list", rows),
|
| 3432 |
+
div(class = "breakdown-total",
|
| 3433 |
+
span(class = "breakdown-total-label", total_label),
|
| 3434 |
+
span(class = "breakdown-total-val",
|
| 3435 |
+
format(total, big.mark = ",", scientific = FALSE)))
|
| 3436 |
+
)
|
| 3437 |
}
|
| 3438 |
|
| 3439 |
+
output$breakdown_giim <- renderUI({
|
| 3440 |
+
build_breakdown(rv$dist_giim, GIIM_COLORS,
|
| 3441 |
+
total_label = "Total municipalities")
|
| 3442 |
+
})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3443 |
|
| 3444 |
+
output$breakdown_infra <- renderUI({
|
| 3445 |
+
build_breakdown(rv$dist_infra, INFRA_CATS,
|
| 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()
|
styles.css
CHANGED
|
@@ -1194,7 +1194,7 @@
|
|
| 1194 |
button.mapbox-gl-draw_polygon,
|
| 1195 |
button.mapbox-gl-draw_rectangle,
|
| 1196 |
button.mapbox-gl-draw_radius,
|
| 1197 |
-
button.mapbox-gl-
|
| 1198 |
button.mapbox-gl-draw_point,
|
| 1199 |
button.mapbox-gl-draw_trash,
|
| 1200 |
button.mapbox-gl-draw_combine,
|
|
@@ -2264,3 +2264,128 @@ button.mapbox-gl-draw_uncombine { display: none !important; }
|
|
| 2264 |
box-shadow: none !important;
|
| 2265 |
border: none !important;
|
| 2266 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1194 |
button.mapbox-gl-draw_polygon,
|
| 1195 |
button.mapbox-gl-draw_rectangle,
|
| 1196 |
button.mapbox-gl-draw_radius,
|
| 1197 |
+
button.mapbox-gl-draw_line,
|
| 1198 |
button.mapbox-gl-draw_point,
|
| 1199 |
button.mapbox-gl-draw_trash,
|
| 1200 |
button.mapbox-gl-draw_combine,
|
|
|
|
| 2264 |
box-shadow: none !important;
|
| 2265 |
border: none !important;
|
| 2266 |
}
|
| 2267 |
+
|
| 2268 |
+
/* =========================================================================
|
| 2269 |
+
Breakdown card -- coloured-text list, replaces donut plots.
|
| 2270 |
+
Each row: swatch | label | tabular value | percentage | proportion bar.
|
| 2271 |
+
========================================================================= */
|
| 2272 |
+
.breakdown-list {
|
| 2273 |
+
display: flex;
|
| 2274 |
+
flex-direction: column;
|
| 2275 |
+
gap: 6px;
|
| 2276 |
+
padding: var(--space-1) 0 var(--space-2) 0;
|
| 2277 |
+
}
|
| 2278 |
+
|
| 2279 |
+
.breakdown-row {
|
| 2280 |
+
display: grid;
|
| 2281 |
+
grid-template-columns: 12px 1fr auto auto;
|
| 2282 |
+
grid-template-rows: auto auto;
|
| 2283 |
+
column-gap: 8px;
|
| 2284 |
+
row-gap: 4px;
|
| 2285 |
+
align-items: center;
|
| 2286 |
+
padding: 4px 2px;
|
| 2287 |
+
border-radius: var(--radius-sm);
|
| 2288 |
+
transition: background var(--t-fast) var(--ease-out);
|
| 2289 |
+
}
|
| 2290 |
+
|
| 2291 |
+
.breakdown-row:hover {
|
| 2292 |
+
background: var(--c-glass-hover);
|
| 2293 |
+
}
|
| 2294 |
+
|
| 2295 |
+
.breakdown-swatch {
|
| 2296 |
+
width: 10px;
|
| 2297 |
+
height: 10px;
|
| 2298 |
+
border-radius: 3px;
|
| 2299 |
+
flex-shrink: 0;
|
| 2300 |
+
border: 1px solid oklch(20% 0.01 250 / 0.10);
|
| 2301 |
+
grid-row: 1;
|
| 2302 |
+
grid-column: 1;
|
| 2303 |
+
margin-top: 2px;
|
| 2304 |
+
}
|
| 2305 |
+
|
| 2306 |
+
.breakdown-label {
|
| 2307 |
+
grid-row: 1;
|
| 2308 |
+
grid-column: 2;
|
| 2309 |
+
font-family: var(--font-ui);
|
| 2310 |
+
font-size: 12px;
|
| 2311 |
+
color: var(--c-text);
|
| 2312 |
+
line-height: 1.35;
|
| 2313 |
+
white-space: nowrap;
|
| 2314 |
+
overflow: hidden;
|
| 2315 |
+
text-overflow: ellipsis;
|
| 2316 |
+
}
|
| 2317 |
+
|
| 2318 |
+
.breakdown-val {
|
| 2319 |
+
grid-row: 1;
|
| 2320 |
+
grid-column: 3;
|
| 2321 |
+
font-family: var(--font-mono);
|
| 2322 |
+
font-variant-numeric: tabular-nums;
|
| 2323 |
+
font-size: 12px;
|
| 2324 |
+
font-weight: 600;
|
| 2325 |
+
color: var(--c-text);
|
| 2326 |
+
letter-spacing: -0.01em;
|
| 2327 |
+
text-align: right;
|
| 2328 |
+
}
|
| 2329 |
+
|
| 2330 |
+
.breakdown-pct {
|
| 2331 |
+
grid-row: 1;
|
| 2332 |
+
grid-column: 4;
|
| 2333 |
+
font-family: var(--font-ui);
|
| 2334 |
+
font-size: 10.5px;
|
| 2335 |
+
color: var(--c-text-mid);
|
| 2336 |
+
font-variant-numeric: tabular-nums;
|
| 2337 |
+
text-align: right;
|
| 2338 |
+
min-width: 38px;
|
| 2339 |
+
}
|
| 2340 |
+
|
| 2341 |
+
.breakdown-bar {
|
| 2342 |
+
grid-row: 2;
|
| 2343 |
+
grid-column: 2 / -1;
|
| 2344 |
+
height: 4px;
|
| 2345 |
+
background: oklch(94% 0.004 250);
|
| 2346 |
+
border-radius: 999px;
|
| 2347 |
+
overflow: hidden;
|
| 2348 |
+
}
|
| 2349 |
+
|
| 2350 |
+
.breakdown-fill {
|
| 2351 |
+
height: 100%;
|
| 2352 |
+
border-radius: 999px;
|
| 2353 |
+
transition: width var(--t-base) var(--ease-out);
|
| 2354 |
+
opacity: 0.85;
|
| 2355 |
+
}
|
| 2356 |
+
|
| 2357 |
+
.breakdown-empty {
|
| 2358 |
+
padding: var(--space-3) var(--space-2);
|
| 2359 |
+
font-family: var(--font-ui);
|
| 2360 |
+
font-size: 11.5px;
|
| 2361 |
+
color: var(--c-text-dim);
|
| 2362 |
+
text-align: center;
|
| 2363 |
+
font-style: italic;
|
| 2364 |
+
}
|
| 2365 |
+
|
| 2366 |
+
.breakdown-total {
|
| 2367 |
+
display: flex;
|
| 2368 |
+
align-items: baseline;
|
| 2369 |
+
justify-content: space-between;
|
| 2370 |
+
padding: 8px 4px 2px;
|
| 2371 |
+
margin-top: 6px;
|
| 2372 |
+
border-top: 1px solid var(--c-glass-stroke);
|
| 2373 |
+
}
|
| 2374 |
+
|
| 2375 |
+
.breakdown-total-label {
|
| 2376 |
+
font-family: var(--font-ui);
|
| 2377 |
+
font-size: 10px;
|
| 2378 |
+
font-weight: 600;
|
| 2379 |
+
letter-spacing: 0.08em;
|
| 2380 |
+
text-transform: uppercase;
|
| 2381 |
+
color: var(--c-text-mid);
|
| 2382 |
+
}
|
| 2383 |
+
|
| 2384 |
+
.breakdown-total-val {
|
| 2385 |
+
font-family: var(--font-mono);
|
| 2386 |
+
font-variant-numeric: tabular-nums;
|
| 2387 |
+
font-size: 14px;
|
| 2388 |
+
font-weight: 600;
|
| 2389 |
+
color: var(--c-text);
|
| 2390 |
+
letter-spacing: -0.01em;
|
| 2391 |
+
}
|