dragmapr is useful in
Shiny when the draggable plot is the interface itself. Users can move
regions, panels, or labels directly instead of typing numeric
offsets.
The simplest Shiny pattern is to write a helper HTML file into a Shiny resource directory and show it in an iframe.
This is useful when the app only needs to let users explore or compose a layout.
Apps can provide their own label table instead of using one label per
region. This is useful for annotations, review notes, callouts, or
workflow-specific metadata. Labels can be ordinary text labels,
text-only draggable labels, or annotation boxes created with
as_drag_annotations().
The user-supplied label table is created with
as_drag_labels() and can carry extra columns for
app-specific behavior. Connector columns such as connector,
connector_type, connector_start_x, and
connector_mid_x are also preserved, so apps can let users
choose straight, elbow, curved, or squiggle leader lines.
Some apps need a static image after the user has finished dragging. The helper posts offset state to its Shiny parent window. The Shiny app can then render a preview and expose a PNG download.
This pattern is useful for report builders, document workflows, or review apps where the interactive layout is the editing surface and the exported PNG is the deliverable.
The bundled shiny_draggable_export.R app intentionally
has many controls because it acts as a smoke test for the package
surface:
The important design point is that the Shiny app does not hold magic layout state. The parent app receives the same region and label offset tables that a non-Shiny workflow can copy or download from the helper.
If offsets have already been saved, an app can skip the draggable helper and only provide static preview/export.
shiny_spatial_studio.R is the most complete Shiny
example. It is a small spatial workspace rather than a fixed demo: users
upload local polygon data, or reopen a saved project ZIP; pick grouping
and label columns; edit label text and colors; undo and redo drag-state
changes; drag boundaries; switch between short labels and info boxes;
control text size, connector geometry, connector line color, line
pattern, smart connectors, arrow endpoints, legend title, visible legend
keys, visible labels, origin outlines, movement connectors, drag preview
trails, and map background; preview a static render; set static export
size and DPI; and download the current artifacts.
Supported inputs: zipped shapefiles, shapefile sidecar files
(.shp + .dbf + .shx), GeoJSON,
and GeoPackage files uploaded locally. The app reads geometry with
sf::st_read(), repairs it with
sf::st_make_valid(), and transforms longitude/latitude data
to EPSG:3857 so metre offsets work correctly. Region group names are
sorted using a natural (numeric-aware) order, so “1”, “2”, …, “10” are
displayed in the right sequence rather than lexicographic order.
The label sidebar consolidates controls so only sliders relevant to the current label type are shown: text size is always visible; marker size (width + height) appears only for rounded-box labels; circle radius only for circle labels; box dimensions only for info boxes.
Available downloads from the studio sidebar:
render_dragged_map()palette.csv, metadata.json, and
recreate-static-map.R; enough to reopen the project in
Spatial Studio, reconstruct the layout in a new session, including
legend and label selections plus movement context settings, or hand off
to a collaboratorSpatial Studio intentionally keeps its adjusted-geometry exports
focused on GeoJSON and GeoPackage. If another shape format is needed,
download either file, open it in Mapshaper, and export the required
format there. The same handoff works for an sf object
created elsewhere in R: write it to a supported spatial file, open that
file in Mapshaper, and choose the desired export format.
The export panel also includes a reproducible R script and a static
bundle. The R script calls render_dragmapr_project() and
expects dragmapr-project.zip to be in the same folder
unless project_path is edited. The static bundle includes
the project files plus ready-to-share PNG and PDF files. The project ZIP
is a one-line static rendering input:
render_dragmapr_project(
"dragmapr-project.zip",
file = "final-map.png",
width = 10,
height = 8,
dpi = 300
)render_dragmapr_project() validates the bundle before
rendering. It gives file-specific errors for missing metadata, malformed
CSVs, unknown region columns, and labels that refer to regions that are
not present in source.gpkg. When an offset row is absent,
it reports the missing rows and uses zero movement for that region or
label so users can still inspect the output.
When launched with ?debug=1, the app includes a State
tab that names its central reactive values so they are easy to find when
adapting the code:
source_sf() - raw sf from upload / demo / project
bundleprojected_sf() — after
prepare_dragmapr_sf()region_col(), label_col() — chosen
columnslabel_table() — styled label data frame passed to
drag_map_prototype()region_state(), label_state() — current
drag offsets as data framesregion_palette() — named colour vectorcurrent_plot() — ggplot2 object ready to
saveThe studio shows a loading veil while data is being read and the D3
helper is being built. The veil is dismissed when the helper iframe
signals it has finished its first render() call by posting
a dragmapr-ready message to the parent page. This avoids
the race condition where the browser fires the iframe load
event before the parent’s listener has been attached.
The spatial studio internally uses four package-level helpers that are also available directly for custom apps.
read_dragmapr_sf_upload(upload) wraps a
[shiny::fileInput()] result - including multi-file shapefile sidecar
uploads and zip archives - into a single sf::st_read()
call. Returns NULL when the upload is empty so callers can
fall back to demo data.
read_dragmapr_sf_url(url, timeout = 60)
downloads a spatial file from a URL into a temporary directory, unpacks
zip archives, and returns an sf object. Raises a
descriptive error on network or format failures.
prepare_dragmapr_sf(x, target_crs = 3857)
repairs invalid geometry, keeps only polygon types, assigns a fallback
CRS when none is present, and reprojects geographic data to the target
projected CRS so metre offsets are meaningful.
dragmapr_iframe_bridge(...) returns a
JavaScript string that installs the postMessage listener
and polling loop needed to relay drag state from the helper iframe back
to Shiny inputs. Wrap it in
tags$head(tags$script(HTML( dragmapr_iframe_bridge()))) in
your UI. The function accepts region_input,
label_input, slow_poll_ms,
fast_poll_ms, allowed_origin, and
iframe_selector arguments to customise the input names,
timing, origin check, and helper iframe selection.
library(shiny)
library(dragmapr)
ui <- fluidPage(
tags$head(tags$script(HTML(dragmapr_iframe_bridge()))),
uiOutput("helper")
)
server <- function(input, output, session) {
helper_dir <- tempfile("myapp_")
dir.create(helper_dir)
shiny::addResourcePath("myapp_static", helper_dir)
x <- prepare_dragmapr_sf(my_sf)
drag_map_prototype(x, region_col = "region",
file = file.path(helper_dir, "helper.html"))
output$helper <- renderUI(
tags$iframe(src = "myapp_static/helper.html",
style = "width:100%;height:700px;border:none;")
## Switching Grouping Columns in Spatial Studio
Spatial Studio stores drag positions per region column and propagates them
when you change the **Group / region column** dropdown. This means you can
work at multiple levels of geographic hierarchy without losing your layout.
### How inheritance works
Each column maintains its **own independent layout cache**. Switching columns
never displaces regions that you have not personally dragged in that column:
- **Coarser → finer** (e.g. HHS region → state name): the finer column resumes
its own last saved layout. If you have not visited it before, all fine units
start at their natural geographic positions — they are **not** displaced by
the parent column's drag offsets.
- **Finer → coarser** (e.g. state name → HHS region): each parent group is
placed at the **mean** of its member units' current positions, or (if you
choose "Restore parent's last position") at the position the parent had when
you last worked at that column.
### Example: HHS regions and state names
The bundled HHS demo has both an `hhs_region` column (ten groups) and a `NAME`
column (individual states). A typical workflow:
1. Set **Group column** to `hhs_region`. Drag the ten regions into an exploded
layout.
2. Switch to `NAME`. The states appear at their **natural positions** (first
visit) — ready for individual fine-tuning without any carry-over from the
HHS drag.
3. Fine-tune individual states as needed.
4. Switch back to `hhs_region`. Each region lands at the mean of its states'
current positions, reflecting any individual fine-tuning.
### What resets and what is preserved
| On column switch | Behaviour |
|---|---|
| Region offsets (coarser→finer) | Restored to that column's last saved positions, or zero if never visited |
| Region offsets (finer→coarser) | Average of children's positions, or restored to parent's last position |
| Label offsets | Reset — label IDs are derived from the new column's region names |
| Undo / redo stack | Reset — new column starts with a clean history |
| Region palette | Preserved |
| Legend and label filter selections | Preserved |
### Round-trip precision
The only step that involves averaging is **finer → coarser**. If all child
regions had identical offsets, the round-trip is lossless. Mixed individual child
moves are summarised into an average for the parent — or you can choose
**"Restore parent's last position"** to skip averaging and return the parent to
exactly where it was.
Changing only the **Label column** while keeping the region column the same
leaves region offsets completely untouched. Only the label IDs change.