Shiny workflows

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.

Embed A Draggable Plot

The simplest Shiny pattern is to write a helper HTML file into a Shiny resource directory and show it in an iframe.

if (interactive()) {
  shiny::runApp(system.file("examples", "shiny_draggable_plot.R", package = "dragmapr"))
}

This is useful when the app only needs to let users explore or compose a layout.

Custom Labels

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().

if (interactive()) {
  shiny::runApp(system.file("examples", "shiny_custom_labels.R", package = "dragmapr"))
}

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.

Export A Report Image

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.

if (interactive()) {
  shiny::runApp(system.file("examples", "shiny_draggable_export.R", package = "dragmapr"))
}

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:

  • show or hide labels;
  • switch between short labels and info boxes;
  • show text labels with or without circular markers;
  • change info-box width and height;
  • show connector lines;
  • choose straight, elbow, curve, or squiggle connectors;
  • adjust connector thickness, line pattern, and arrow endpoints;
  • show or hide the region legend;
  • filter visible legend keys and labels while preserving offsets;
  • show origin outlines, movement connectors, and browser-only drag trails;
  • download a static PNG reconstructed from the current region and label offset state.

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.

Static Only

If offsets have already been saved, an app can skip the draggable helper and only provide static preview/export.

if (interactive()) {
  shiny::runApp(system.file("examples", "shiny_static_export.R", package = "dragmapr"))
}

Spatial Studio

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.

if (interactive()) {
  shiny::runApp(system.file("examples", "shiny_spatial_studio.R", package = "dragmapr"))
}

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:

  • PNG — static render from render_dragged_map()
  • Region CSV — current region offsets, importable by any later session
  • Label CSV — current label offsets
  • Labels table — current label geometry and edited label text
  • GeoJSON / GPKG — the adjusted sf geometry with offsets applied
  • HTML helper — the standalone D3 drag file you can share or reopen
  • Project ZIP — source geometry, offsets, labels, 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 collaborator
  • Static bundle — the project bundle plus ready-to-share PNG and PDF files

Spatial 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 bundle
  • projected_sf() — after prepare_dragmapr_sf()
  • region_col(), label_col() — chosen columns
  • label_table() — styled label data frame passed to drag_map_prototype()
  • region_state(), label_state() — current drag offsets as data frames
  • region_palette() — named colour vector
  • current_plot()ggplot2 object ready to save

Loading veil

The 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.

Reusable Shiny Helpers

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.