Skip to content

Commit debc79b

Browse files
committed
implement draggable legend
1 parent 1742c36 commit debc79b

File tree

8 files changed

+256
-13
lines changed

8 files changed

+256
-13
lines changed

DESCRIPTION

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Package: mapgl
22
Title: Interactive Maps with 'Mapbox GL JS' and 'MapLibre GL JS'
3-
Version: 0.4.4
4-
Date: 2026-01-12
3+
Version: 0.4.4.9000
4+
Date: 2026-01-13
55
Authors@R:
66
person(given = "Kyle", family = "Walker", email = "kyle@walker-data.com", role = c("aut", "cre"))
77
Description: Provides an interface to the 'Mapbox GL JS' (<https://docs.mapbox.com/mapbox-gl-js/guides>)

R/legends.R

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@
186186
#' @param filter_values For interactive legends, the actual data values to filter on. For categorical legends, use this when your display labels differ from the data values (e.g., values = c("Music", "Bar") for display, filter_values = c("music", "bar") for filtering). For continuous legends, provide numeric break values when using formatted display labels (e.g., values = get_legend_labels(scale), filter_values = get_breaks(scale)). If NULL (default), uses values.
187187
#' @param classification A mapgl_classification object (from step_quantile, step_equal_interval, etc.) to use for the legend. When provided, values and colors will be automatically extracted. For interactive legends, range-based filtering will be used based on the classification breaks.
188188
#' @param breaks Numeric vector of break points for filtering with classification-based legends. Typically extracted automatically from the classification object. Only needed if you want to override the default breaks.
189+
#' @param draggable Logical, whether the legend can be dragged to a new position by the user. Default is FALSE.
189190
#' @export
190191
add_legend <- function(
191192
map,
@@ -211,7 +212,8 @@ add_legend <- function(
211212
filter_column = NULL,
212213
filter_values = NULL,
213214
classification = NULL,
214-
breaks = NULL
215+
breaks = NULL,
216+
draggable = FALSE
215217
) {
216218
type <- match.arg(type)
217219
if (is.null(unique_id)) {
@@ -263,7 +265,8 @@ if (is.null(values) || is.null(colors)) {
263265
style,
264266
interactive,
265267
filter_column,
266-
filter_values
268+
filter_values,
269+
draggable
267270
)
268271
} else {
269272
add_categorical_legend(
@@ -287,7 +290,8 @@ if (is.null(values) || is.null(colors)) {
287290
interactive,
288291
filter_column,
289292
filter_values,
290-
breaks
293+
breaks,
294+
draggable
291295
)
292296
}
293297
}
@@ -317,7 +321,8 @@ add_categorical_legend <- function(
317321
interactive = FALSE,
318322
filter_column = NULL,
319323
filter_values = NULL,
320-
breaks = NULL
324+
breaks = NULL,
325+
draggable = FALSE
321326
) {
322327
# Handle deprecation of circular_patches
323328
if (!missing(circular_patches) && circular_patches) {
@@ -632,6 +637,9 @@ add_categorical_legend <- function(
632637
""
633638
}
634639

640+
# Add draggable attribute if draggable is TRUE
641+
draggable_attr <- if (draggable) ' data-draggable="true"' else ""
642+
635643
legend_html <- paste0(
636644
'<div id="',
637645
unique_id,
@@ -640,6 +648,7 @@ add_categorical_legend <- function(
640648
'"',
641649
layer_attr,
642650
interactive_attr,
651+
draggable_attr,
643652
">",
644653
"<h2>",
645654
legend_title,
@@ -887,7 +896,8 @@ add_continuous_legend <- function(
887896
style = NULL,
888897
interactive = FALSE,
889898
filter_column = NULL,
890-
filter_values = NULL
899+
filter_values = NULL,
900+
draggable = FALSE
891901
) {
892902
if (is.null(unique_id)) {
893903
unique_id <- paste0("legend-", as.hexmode(sample(1:1000000, 1)))
@@ -972,6 +982,9 @@ add_continuous_legend <- function(
972982
""
973983
}
974984

985+
# Add draggable attribute if draggable is TRUE
986+
draggable_attr <- if (draggable) ' data-draggable="true"' else ""
987+
975988
legend_html <- paste0(
976989
'<div id="',
977990
unique_id,
@@ -980,6 +993,7 @@ add_continuous_legend <- function(
980993
'"',
981994
layer_attr,
982995
interactive_attr,
996+
draggable_attr,
983997
">",
984998
"<h2>",
985999
legend_title,

R/legends_compare.R

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ add_legend.mapboxgl_compare <- function(
2424
filter_column = NULL,
2525
filter_values = NULL,
2626
classification = NULL,
27-
breaks = NULL
27+
breaks = NULL,
28+
draggable = FALSE
2829
) {
2930

3031
# Warn if interactive features are requested (not yet supported for compare maps)

inst/htmlwidgets/lib/legend-interactivity/legend-interactivity.css

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,45 @@
203203
background: #444;
204204
border-color: #666;
205205
}
206+
207+
/* ===========================================
208+
Draggable Legends
209+
=========================================== */
210+
211+
/* Draggable legend styling */
212+
.mapboxgl-legend.legend-draggable {
213+
cursor: grab;
214+
}
215+
216+
.mapboxgl-legend.legend-draggable h2 {
217+
cursor: grab;
218+
}
219+
220+
/* While dragging */
221+
.mapboxgl-legend.legend-dragging {
222+
cursor: grabbing;
223+
opacity: 0.9;
224+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
225+
z-index: 2000 !important;
226+
}
227+
228+
.mapboxgl-legend.legend-dragging h2 {
229+
cursor: grabbing;
230+
}
231+
232+
/* Allow clicking on interactive elements without starting drag */
233+
.mapboxgl-legend.legend-draggable .legend-item {
234+
cursor: pointer;
235+
}
236+
237+
.mapboxgl-legend.legend-draggable .legend-gradient-handle {
238+
cursor: ew-resize;
239+
}
240+
241+
.mapboxgl-legend.legend-draggable .legend-gradient-middle {
242+
cursor: grab;
243+
}
244+
245+
.mapboxgl-legend.legend-dragging .legend-gradient-middle {
246+
cursor: grabbing;
247+
}

inst/htmlwidgets/lib/legend-interactivity/legend-interactivity.js

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -891,3 +891,172 @@ function formatValue(value) {
891891
return value.toFixed(2);
892892
}
893893
}
894+
895+
/**
896+
* Initialize draggable functionality for legends
897+
* Called automatically when legends are rendered
898+
* @param {HTMLElement} container - The map container element
899+
*/
900+
function initializeDraggableLegends(container) {
901+
var legends = container.querySelectorAll('.mapboxgl-legend[data-draggable="true"]');
902+
legends.forEach(function(legend) {
903+
// Skip if already initialized
904+
if (legend._draggableInitialized) return;
905+
legend._draggableInitialized = true;
906+
907+
makeLegendDraggable(legend);
908+
});
909+
}
910+
911+
/**
912+
* Make a single legend element draggable
913+
* @param {HTMLElement} legend - The legend element
914+
*/
915+
function makeLegendDraggable(legend) {
916+
var isDragging = false;
917+
var startX, startY;
918+
var startLeft, startTop;
919+
920+
// Add draggable class for styling
921+
legend.classList.add('legend-draggable');
922+
923+
// Get the map container (parent of legend)
924+
var mapContainer = legend.closest('.mapboxgl-map, .maplibregl-map') || legend.parentElement;
925+
926+
function onMouseDown(e) {
927+
// Don't start drag if clicking on interactive elements (including slider handles)
928+
if (e.target.closest('.legend-item, .legend-reset-btn, .continuous-slider-container, .legend-gradient-handle, .legend-gradient-middle, .legend-gradient-overlay-container, input, button')) {
929+
return;
930+
}
931+
932+
isDragging = true;
933+
legend.classList.add('legend-dragging');
934+
935+
startX = e.clientX;
936+
startY = e.clientY;
937+
938+
// Get current position
939+
var rect = legend.getBoundingClientRect();
940+
var containerRect = mapContainer.getBoundingClientRect();
941+
942+
// Calculate position relative to container
943+
startLeft = rect.left - containerRect.left;
944+
startTop = rect.top - containerRect.top;
945+
946+
// Switch to absolute positioning with explicit coordinates
947+
legend.style.position = 'absolute';
948+
legend.style.left = startLeft + 'px';
949+
legend.style.top = startTop + 'px';
950+
legend.style.right = 'auto';
951+
legend.style.bottom = 'auto';
952+
953+
e.preventDefault();
954+
e.stopPropagation();
955+
956+
document.addEventListener('mousemove', onMouseMove);
957+
document.addEventListener('mouseup', onMouseUp);
958+
}
959+
960+
function onMouseMove(e) {
961+
if (!isDragging) return;
962+
963+
var deltaX = e.clientX - startX;
964+
var deltaY = e.clientY - startY;
965+
966+
var newLeft = startLeft + deltaX;
967+
var newTop = startTop + deltaY;
968+
969+
// Constrain to container bounds
970+
var containerRect = mapContainer.getBoundingClientRect();
971+
var legendRect = legend.getBoundingClientRect();
972+
973+
var maxLeft = containerRect.width - legendRect.width;
974+
var maxTop = containerRect.height - legendRect.height;
975+
976+
newLeft = Math.max(0, Math.min(newLeft, maxLeft));
977+
newTop = Math.max(0, Math.min(newTop, maxTop));
978+
979+
legend.style.left = newLeft + 'px';
980+
legend.style.top = newTop + 'px';
981+
982+
e.preventDefault();
983+
}
984+
985+
function onMouseUp(e) {
986+
if (!isDragging) return;
987+
988+
isDragging = false;
989+
legend.classList.remove('legend-dragging');
990+
991+
document.removeEventListener('mousemove', onMouseMove);
992+
document.removeEventListener('mouseup', onMouseUp);
993+
}
994+
995+
// Add touch support
996+
function onTouchStart(e) {
997+
if (e.touches.length !== 1) return;
998+
999+
var touch = e.touches[0];
1000+
// Don't start drag if touching interactive elements (including slider handles)
1001+
if (e.target.closest('.legend-item, .legend-reset-btn, .continuous-slider-container, .legend-gradient-handle, .legend-gradient-middle, .legend-gradient-overlay-container, input, button')) {
1002+
return;
1003+
}
1004+
1005+
isDragging = true;
1006+
legend.classList.add('legend-dragging');
1007+
1008+
startX = touch.clientX;
1009+
startY = touch.clientY;
1010+
1011+
var rect = legend.getBoundingClientRect();
1012+
var containerRect = mapContainer.getBoundingClientRect();
1013+
1014+
startLeft = rect.left - containerRect.left;
1015+
startTop = rect.top - containerRect.top;
1016+
1017+
legend.style.position = 'absolute';
1018+
legend.style.left = startLeft + 'px';
1019+
legend.style.top = startTop + 'px';
1020+
legend.style.right = 'auto';
1021+
legend.style.bottom = 'auto';
1022+
1023+
e.preventDefault();
1024+
}
1025+
1026+
function onTouchMove(e) {
1027+
if (!isDragging || e.touches.length !== 1) return;
1028+
1029+
var touch = e.touches[0];
1030+
var deltaX = touch.clientX - startX;
1031+
var deltaY = touch.clientY - startY;
1032+
1033+
var newLeft = startLeft + deltaX;
1034+
var newTop = startTop + deltaY;
1035+
1036+
var containerRect = mapContainer.getBoundingClientRect();
1037+
var legendRect = legend.getBoundingClientRect();
1038+
1039+
var maxLeft = containerRect.width - legendRect.width;
1040+
var maxTop = containerRect.height - legendRect.height;
1041+
1042+
newLeft = Math.max(0, Math.min(newLeft, maxLeft));
1043+
newTop = Math.max(0, Math.min(newTop, maxTop));
1044+
1045+
legend.style.left = newLeft + 'px';
1046+
legend.style.top = newTop + 'px';
1047+
1048+
e.preventDefault();
1049+
}
1050+
1051+
function onTouchEnd(e) {
1052+
if (!isDragging) return;
1053+
1054+
isDragging = false;
1055+
legend.classList.remove('legend-dragging');
1056+
}
1057+
1058+
legend.addEventListener('mousedown', onMouseDown);
1059+
legend.addEventListener('touchstart', onTouchStart, { passive: false });
1060+
legend.addEventListener('touchmove', onTouchMove, { passive: false });
1061+
legend.addEventListener('touchend', onTouchEnd);
1062+
}

inst/htmlwidgets/mapboxgl.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1940,6 +1940,11 @@ HTMLWidgets.widget({
19401940
});
19411941
}
19421942

1943+
// Initialize draggable legends
1944+
if (typeof initializeDraggableLegends === "function") {
1945+
initializeDraggableLegends(el);
1946+
}
1947+
19431948
// Add fullscreen control if enabled
19441949
if (x.fullscreen_control && x.fullscreen_control.enabled) {
19451950
const position = x.fullscreen_control.position || "top-right";

inst/htmlwidgets/maplibregl.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2032,6 +2032,11 @@ HTMLWidgets.widget({
20322032
});
20332033
}
20342034

2035+
// Initialize draggable legends
2036+
if (typeof initializeDraggableLegends === "function") {
2037+
initializeDraggableLegends(el);
2038+
}
2039+
20352040
// Add fullscreen control if enabled
20362041
if (x.fullscreen_control && x.fullscreen_control.enabled) {
20372042
const position = x.fullscreen_control.position || "top-right";

0 commit comments

Comments
 (0)