From 04d252a726fd931d7b782c15960bbe39bfc9aece Mon Sep 17 00:00:00 2001 From: Martijn Tennekes Date: Sun, 14 Jun 2026 20:49:27 +0200 Subject: [PATCH] Add patch_spacing for proportional row heights in categorical legends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adds a `patch_spacing` argument to `add_categorical_legend()` (threaded through `add_legend()`) so that legends with per-item `sizes` can size each row to its own symbol instead of forcing every row to the largest symbol's height. For graduated/proportional-symbol legends, the current fixed-height rows leave small symbols floating in oversized boxes, so the vertical gaps look uneven. The new `"proportional"` option makes each row's height track its symbol size, which matches how other mapping libraries (e.g. leaflet) render this kind of legend. ## What changed - New arg `patch_spacing = c("uniform", "proportional")` on `add_categorical_legend()` and `add_legend()`. - `"uniform"` (default): every row uses `max(sizes)` — **identical to current behaviour**. - `"proportional"`: each row's patch container height is set to that item's own size. Container *width* stays at `max(sizes)` so labels remain aligned. - Only affects categorical legends with varying `sizes`; line patches keep their existing uniform spacing. ## Backward compatibility The default is `"uniform"`, so existing output is unchanged. No other arguments or behaviour are touched. ## Example ```r library(mapgl) maplibre() |> add_categorical_legend( legend_title = "Population", values = c("Small", "Medium", "Large", "Huge"), colors = "#3182bd", patch_shape = "circle", sizes = c(8, 16, 32, 48), patch_spacing = "proportional" # rows hug each symbol ) ``` ## Notes - Found this while adding proportional-symbol (donut) legends to `tmap`'s mapgl backend, where the constant row height was the only thing keeping the size legend from matching the plot/leaflet output. - Docs regenerated with `devtools::document()`; a test for the row-height behaviour is included. To be added to NEWS.md: `add_categorical_legend()` and `add_legend()` gain a `patch_spacing` argument (`"uniform"`/`"proportional"`). With `"proportional"`, each legend row's height tracks its own symbol size, giving proportional vertical spacing for graduated-symbol legends. Default `"uniform"` preserves existing behaviour (#PR) Best, Martijn (with some help from Claude) --- R/legends.R | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/R/legends.R b/R/legends.R index ef7dab2..bbfbf69 100644 --- a/R/legends.R +++ b/R/legends.R @@ -196,6 +196,10 @@ #' @param draggable Logical, whether the legend can be dragged to a new position by the user. Default is FALSE. #' @param collapsible Logical, whether to render a toggle button that collapses the legend to a header-only view. Default is FALSE. Most useful for categorical legends with tall bodies on small viewports. #' @param collapsed Logical, whether the legend starts in the collapsed state. Only applies when \code{collapsible = TRUE}. Default is FALSE. +#' @param patch_spacing For categorical legends with per-item `sizes`, either +#' `"uniform"` (default; every row uses the largest symbol's height) or +#' `"proportional"` (each row's height tracks its own symbol size, so vertical +#' spacing scales with symbol size - useful for graduated-symbol legends). #' #' @details #' \strong{Collapsible legends.} When \code{collapsible = TRUE}, a 26x26px toggle @@ -247,7 +251,8 @@ add_legend <- function( na_color = NULL, draggable = FALSE, collapsible = FALSE, - collapsed = FALSE + collapsed = FALSE, + patch_spacing = c("uniform", "proportional") ) { type <- match.arg(type) if (is.null(unique_id)) { @@ -359,7 +364,8 @@ if (is.null(values) || is.null(colors)) { breaks, draggable, collapsible = collapsible, - collapsed = collapsed + collapsed = collapsed, + patch_spacing = patch_spacing ) } } @@ -677,7 +683,8 @@ add_categorical_legend <- function( breaks = NULL, draggable = FALSE, collapsible = FALSE, - collapsed = FALSE + collapsed = FALSE, + patch_spacing = c("uniform", "proportional") ) { # Handle deprecation of circular_patches if (!missing(circular_patches) && circular_patches) { @@ -756,7 +763,9 @@ add_categorical_legend <- function( } max_size <- max(sizes) - + patch_spacing <- match.arg(patch_spacing) + proportional <- identical(patch_spacing, "proportional") + # Function to process custom SVG shapes .process_custom_svg <- function(svg_string, color, size) { # Remove whitespace and normalize @@ -948,7 +957,7 @@ add_categorical_legend <- function( container_height <- max(sizes) # Use max line thickness for consistent spacing } else { container_width <- max_size - container_height <- max_size + container_height <- if (proportional) sizes[[i]] else max_size } # Add data-value attribute for interactive legends