gg2d3 provides a composable pipe-based API for adding interactivity
to D3 widgets. Each function (d3_tooltip(),
d3_hover(), d3_zoom(),
d3_brush()) takes a widget and returns a widget, so they
chain naturally and every verb is optional.
The pipe API
Call gg2d3() on any ggplot, then pipe the result through
whichever interactivity verbs you want. Order within the chain does not
matter — the functions are additive and non-destructive.
library(ggplot2)
library(gg2d3)
p <- ggplot(iris, aes(Sepal.Length, Sepal.Width, color = Species)) +
geom_point(size = 3)
# Full interactive stack — tooltip + hover + zoom + brush
gg2d3(p) |>
d3_tooltip() |>
d3_hover() |>
d3_zoom() |>
d3_brush()You can use any subset:
# Tooltip and zoom only
gg2d3(p) |> d3_tooltip() |> d3_zoom()Tooltips (d3_tooltip)
d3_tooltip() shows a floating tooltip containing data
values when the user hovers over a visual element (point, bar, line
segment, etc.).
Signature:
d3_tooltip(widget, fields = NULL, formatter = NULL)
p <- ggplot(mtcars, aes(wt, mpg, color = factor(cyl))) + geom_point()
# Default: show all mapped aesthetics
gg2d3(p) |> d3_tooltip()
# Show only a subset of fields
gg2d3(p) |> d3_tooltip(fields = c("wt", "mpg"))
# Custom JavaScript formatter
# formatter receives (field, value) and returns a display string
gg2d3(p) |> d3_tooltip(
formatter = "function(field, value) {
if (field === 'mpg') return field + ': ' + value + ' mpg';
return field + ': ' + value;
}"
)Tooltip behavior:
- When
fields = NULL(default), all aesthetics except internal fields (those starting with underscore, plusPANEL,group, etc.) are shown. - Date and datetime values are automatically formatted using the browser’s locale.
- The tooltip follows the cursor and disappears on mouse-out.
Hover highlighting (d3_hover)
d3_hover() dims non-hovered elements so the hovered
point or group stands out visually.
Signature:
d3_hover(widget, opacity = 0.3, stroke = NULL, stroke_width = NULL)
p <- ggplot(iris, aes(Sepal.Length, Sepal.Width, color = Species)) +
geom_point(size = 3)
# Default: dim others to 30% opacity; no highlight stroke
gg2d3(p) |> d3_hover()
# Add a visible outline on the hovered element
gg2d3(p) |> d3_hover(opacity = 0.3, stroke = "black", stroke_width = 2)
# Combine with tooltip
gg2d3(p) |> d3_tooltip() |> d3_hover(opacity = 0.5)Hover behavior:
- The hovered element always has opacity 1.0;
opacitycontrols the non-hovered elements only. -
strokeandstroke_widthare only applied to the hovered element; ifstroke = NULL(default), no outline is added. - When a brush selection is active, hover dimming is automatically disabled to avoid visual conflicts. Hover resumes when the brush is cleared.
Zoom and pan (d3_zoom)
d3_zoom() enables scroll-wheel zoom and drag-to-pan.
Double-click resets to the original view.
Signature:
d3_zoom(widget, scale_extent = c(1, 8), direction = c("both", "x", "y"))
p <- ggplot(economics, aes(date, unemploy)) + geom_line()
# Default: zoom both axes, 1x minimum to 8x maximum
gg2d3(p) |> d3_zoom()Zoom behavior:
-
scale_extent[1]must be >= 1 (no zoom out beyond the original view). - Axes — including temporal axes and secondary axes — update dynamically during zoom. Date formatting is preserved.
- Drag-to-pan is constrained to the panel background so that clicking data elements does not trigger accidental panning.
- Double-click resets zoom and pan to the original view.
Brush selection (d3_brush)
d3_brush() lets users drag to define a rectangular
selection. Selected elements remain at full opacity; unselected elements
are dimmed. Double-click clears the selection.
Signature:
d3_brush(widget, direction = c("xy", "x", "y"), on_brush = NULL, fill = "#3b82f6", opacity = 0.15)
p <- ggplot(iris, aes(Sepal.Length, Petal.Length, color = Species)) +
geom_point(size = 2)
# Default: 2D rectangular brush, blue overlay, non-selected dims to 0.15
gg2d3(p) |> d3_brush()
# Custom overlay color and non-selected opacity
gg2d3(p) |> d3_brush(fill = "#ef4444", opacity = 0.3)
# JavaScript callback receiving selected data array
gg2d3(p) |> d3_brush(
on_brush = "function(selectedData) {
console.log(selectedData.length + ' points selected');
}"
)
# Combine with tooltip
gg2d3(p) |> d3_tooltip() |> d3_brush()Brush behavior:
-
fillis the color of the translucent brush overlay rectangle (default"#3b82f6", a light blue). -
opacitycontrols unselected elements; selected elements are always at opacity 1.0. -
on_brushreceives the selected data rows as a JavaScript array; each element is the row object as passed to D3. - Double-click anywhere in the panel clears the brush and restores all elements to full opacity.
Linked views with Crosstalk
gg2d3 integrates with the crosstalk package for
linked brushing across multiple widgets. Wrap your data frame in
crosstalk::SharedData$new() before building the ggplot,
then give that shared data to each widget. Brushing in one widget
automatically cross-highlights the corresponding observations in all
linked widgets.
has_crosstalk <- requireNamespace("crosstalk", quietly = TRUE)
has_htmltools <- requireNamespace("htmltools", quietly = TRUE)
if (!has_crosstalk || !has_htmltools) {
missing_crosstalk_packages <- c(
if (!has_crosstalk) "crosstalk",
if (!has_htmltools) "htmltools"
)
cat(
"PKGDOWN_CROSSTALK_OPTIONAL_SKIP: linked-view example not rendered; missing ",
paste(missing_crosstalk_packages, collapse = ", "),
".\n",
sep = ""
)
} else {
# Wrap dataset in SharedData; both plots share explicit stable keys/group.
shared_iris <- crosstalk::SharedData$new(
iris,
key = rownames(iris),
group = "pkgdown_crosstalk_iris"
)
p1 <- ggplot(iris, aes(Sepal.Length, Sepal.Width, color = Species)) +
geom_point(size = 2)
p1$data <- shared_iris
p2 <- ggplot(iris, aes(Petal.Length, Petal.Width, color = Species)) +
geom_point(size = 2)
p2$data <- shared_iris
w1 <- gg2d3(p1) |> d3_tooltip() |> d3_brush()
w2 <- gg2d3(p2) |> d3_tooltip() |> d3_brush()
htmltools::tagList(
htmltools::div(
style = paste(
"display: grid;",
"grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));",
"gap: 1rem;"
),
w1,
w2
)
)
}Crosstalk behavior:
-
No Shiny required — linked views work in static
HTML documents (e.g., knitted R Markdown or
htmltools::save_html()). - The
SharedDatagroup name defaults to a random UUID; passgroup = "my_group"toSharedData$new()if you need to link widgets across separate code chunks. - Each widget can independently carry any combination of
d3_tooltip(),d3_hover(),d3_zoom(), andd3_brush()— Crosstalk operates at the data key level, not at the interactivity verb level.
Interaction caveats
- Brush overrides hover — when a brush selection is active, hover dimming is suspended. Hover resumes as soon as the brush is cleared (double-click).
- Zoom and brush coexist — zooming does not clear an existing brush selection; the selection rectangle scales with the view.
-
Large datasets — for datasets above ~10 k rows,
prefer
d3_tooltip()overd3_hover(). Tooltip hit-testing is O(1) per element; hover dimming iterates all elements on every mouse event. - Faceted plots — brush selection is per-panel. Selecting in one facet does not dim elements in other facets; use Crosstalk for cross-panel linking.
-
geom_polygonlayers — tooltip, hover, custom handlers, Shiny-style click handlers, brush selection, and Crosstalk targetpath.geom-polygonmarks. Public callback payloads are sanitized representative source-row objects. Brush selection uses the SVG path bounds, not sf-style projected anchor points or topology-aware polygon overlap. -
geom_sflayers — tooltip, hover, custom handlers, Shiny-style click handlers, and brush callbacks target.geom-sfpolygon, point, and line marks. Public callback payloads are sanitized source-row objects with renderer-only fields removed. Brush selection uses rendered representative anchors fromdata-cxanddata-cyrather than true geometry-overlap brushing; these representative anchors are stable panel-local hit-test points.d3_zoom()warns withd3_zoom() does not support geom_sf layers yet; zoom has been suppressed.and returns the unzoomed widget. Optional browser validation for these sf interactions is R/testthat/chromote based and may skip cleanly when optional local tooling is unavailable. -
geom_sf_textandgeom_sf_labellayers — sf annotations share the.geom-sfinteraction path.geom_sf_text()targetstext.geom-sf.geom-sf-text;geom_sf_label()targetsg.geom-sf.geom-sf-label. Tooltip, hover, handlers, and Shiny-style clicks receive sanitized public payloads. Brush selection uses projected anchor coordinates, so label bounds are not treated as topology-aware geometry intersections. -
Geometry caveats — see
vignettes/d3-drawing-diagnostics.mdfor detailed polygon, rect/tile, sf annotation, and map-feature limitations. -
Zoom extent —
scale_extent[1]must be >= 1. Setting it below 1 would allow zooming further out than the original view, which distorts axis labels; the function raises an error instead.