Skip to contents

Most of vellumplot maps columns of a data frame to x and y. Two important data shapes do not fit that mould: spatial geometries, where the coordinates live in a geometry column and the aspect ratio matters, and graphs, where the positions have to be computed by a layout algorithm first. vellumplot handles both with the same grammar, through entry points suited to each data type.

Maps from sf

mark_sf() draws the geometry column of an sf object. Points, lines, and polygons each render appropriately, and there are no x/y encodings: the coordinates come from the geometry. Other aesthetics map feature attributes as usual, so fill = AREA gives you a choropleth. Pair it with coord_sf(), which reprojects every layer to a common CRS before training and locks the map aspect ratio so nothing is stretched.

nc <- sf::st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE)
vplot(nc) |>
  mark_sf(fill = BIR74) |>
  scale_fill_binned(style = "quantile", n = 6, palette = "Batlow") |>
  coord_sf()
A map. Based on 100 observations.34.034.535.035.536.036.5-84-82-80-78-76yxBIR74[248, 767.5)[767.5, 1324)[1324, 2180)[2180, 3350)[3350, 4769)[4769, 2.159e+04]

Because mark_sf() is an ordinary layer, you can stack it: draw a base layer of boundaries, then a second mark_sf() for highlighted features or point locations on top, and every layer reprojects together.

Labelling features

mark_sf_label() names each feature at its interior point (sf::st_point_on_surface(), guaranteed to fall inside the polygon rather than in a bay or a hole). The label positions reproject through the same coord_sf() CRS as the geometry, so they always land where the feature is drawn, and crowded labels repel apart.

vplot(nc) |>
  mark_sf(fill = AREA) |>
  mark_sf_label(label = NAME, size = 5) |>
  coord_sf()
A plot combining map and sf_label plot. Based on 100 observations.AsheAlleghanySurryCurrituckNorthamptonHertfordCamdenGatesWarrenStokesCaswellRockinghamGranvillePersonVanceHalifaxPasquotankWilkesWataugaPerquimansChowanAveryYadkinFranklinForsythGuilfordAlamanceBertieOrangeDurhamNashMitchellEdgecombeCaldwellYanceyMartinWakeMadisonIredellDavieAlexanderDavidsonBurkeWashingtonTyrrellMcDowellRandolphChathamWilsonRowanPittCatawbaBuncombeJohnstonHaywoodDareBeaufortSwainGreeneLeeRutherfordWayneHarnettClevelandLincolnJacksonMooreMecklenburgCabarrusMontgomeryStanlyHendersonGrahamLenoirTransylvaniaGastonPolkMaconSampsonPamlicoCherokeeCumberlandJonesUnionAnsonHokeHydeDuplinRichmondClayCravenScotlandOnslowRobesonCarteretBladenPenderColumbusNew HanoverBrunswick34.034.535.035.536.036.5-84-82-80-78-76yxAREA0.050.100.150.20

Merging regions

When several small features share a value — counties grouped into a region, or tracts into a district — the borders between them are usually noise: you want one clean outline per group, not a mesh of internal seams. merge = TRUE dissolves each same-fill group into a single region with real boolean geometry (vellum::vl_path_op()), so the shared edges vanish and each region is one crisp path that stays exact in PDF (no double-stroked seams, no hairline gaps).

nc$region <- cut(sf::st_coordinates(sf::st_centroid(sf::st_geometry(nc)))[, 1],
  4,
  labels = c("west", "midwest", "mideast", "east")
)
vplot(nc) |>
  mark_sf(fill = region, merge = TRUE, color = "white") |>
  coord_sf()
A map. Based on 100 observations.34.034.535.035.536.036.5-84-82-80-78-76yxregionwestmidwestmideasteast

Merging is for static choropleths; on an interactive layer it is ignored, since dissolving the features would drop the per-feature keys a tooltip needs.

Cartographic furniture

A map usually wants more than the polygons: a graticule to place it on the globe, a scale bar to read distances, and a north arrow. coord_sf(graticule = TRUE) draws meridians and parallels behind the marks — and because they are generated in longitude/latitude and then reprojected, they curve correctly under a projected CRS rather than being drawn as a naive straight grid. mark_scalebar() and mark_compass() are fixed-position decorations pinned to a panel corner (position = "bottomleft", "topright", and so on); the scale bar reads the map’s CRS to size itself, defaulting to a round distance near a quarter of the panel width.

vplot(nc) |>
  mark_sf(fill = BIR74, color = "white", linewidth = 0.2) |>
  scale_fill_binned(style = "quantile", n = 6, palette = "Batlow") |>
  coord_sf(crs = 5070, graticule = TRUE) |>
  mark_scalebar(unit = "km") |>
  mark_compass()
A plot combining map, scalebar plot, and compass plot. Based on 100 observations.84°W82°W80°W78°W76°W35°N36°N37°N0200 kmN1400000150000016000001200000140000016000001800000yxBIR74[248, 767.5)[767.5, 1324)[1324, 2180)[2180, 3350)[3350, 4769)[4769, 2.159e+04]

Networks from igraph

vgraph() starts a node-link diagram from an igraph graph. It runs a layout (stress majorization by default, via graphlayouts) and produces a spec whose node and edge tables already carry the x, y, xend, yend, and name columns the graph marks need. Then mark_edges() and mark_nodes() draw it. Draw order is fixed no matter how you pipe them: edges below nodes below labels.

g <- igraph::make_graph("Zachary")
g <- igraph::set_vertex_attr(
  g, "grp",
  value = as.factor(igraph::cluster_louvain(g)$membership)
)
g <- igraph::set_vertex_attr(g, "deg", value = igraph::degree(g))

vgraph(g, layout = "stress") |>
  mark_edges(alpha = 0.4) |>
  mark_nodes(size = deg, fill = grp) |>
  scale_size(range = c(0.5, 3))
A network graph. It has 34 nodes and 78 edges, where colour shows grp and size shows deg.grp1234deg481216

The node and edge aesthetics are the same ones you already know. mark_nodes() takes size, shape, fill, and color; mark_edges() takes color, linewidth, linetype, and alpha. mark_node_text() adds vertex labels.

vgraph(g, layout = "stress") |>
  mark_edges(color = "grey70") |>
  mark_nodes(fill = grp, size = 2) |>
  mark_node_text(label = name, size = 8)
A plot combining network graph and node labels. It has 34 nodes and 78 edges, where colour shows grp.12345678910111213141516171819202122232425262728293031323334grp1234

Independent edge scales

Edge colour, opacity, line type, and width train on their own scales – scale_edge_color(), scale_edge_alpha(), scale_edge_linetype(), and scale_edge_width() – separate from the node colour/alpha/linetype scales. So a figure can map node fill to a discrete community and edge colour to a continuous edge weight, and each gets its own legend instead of the two collapsing into one. Map an edge attribute to color and it is trained as an edge scale automatically; call scale_edge_color() to choose the palette.

g <- igraph::set_edge_attr(g, "w", value = runif(igraph::ecount(g)))

vgraph(g, layout = "stress") |>
  mark_edges(color = w, linewidth = w) |>
  mark_nodes(fill = grp, size = deg) |>
  scale_edge_color(palette = "Grays") |>
  scale_edge_width(range = c(0.3, 2.5)) |>
  scale_size(range = c(0.5, 3))
A network graph. It has 34 nodes and 78 edges, where colour shows grp and size shows deg.grp1234deg481216w0.250.500.75w0.250.500.75

Directed edges, arrows, and edge labels

For directed graphs, arrow = TRUE draws a closed arrowhead at each edge’s target end; edges are capped at the node boundary so the head is never buried under the marker. Pass a vellum::vl_arrow() for full control over the head (ends, type, length, angle). mark_edge_text() labels the edges at their midpoints, and angle = "along" rotates each label to follow its edge.

el <- matrix(c(1, 2, 2, 3, 3, 1, 1, 4, 4, 2), ncol = 2, byrow = TRUE)
d <- igraph::graph_from_edgelist(el, directed = TRUE)
d <- igraph::set_edge_attr(d, "flow", value = c(3, 1, 4, 1, 5))

vgraph(d, layout = "stress") |>
  mark_edges(linewidth = flow, arrow = TRUE) |>
  mark_nodes(size = 2, fill = "steelblue") |>
  mark_edge_text(label = flow, size = 7) |>
  mark_node_text(label = name, size = 8, color = "white") |>
  scale_edge_width(range = c(0.4, 2.5))
A plot combining network graph, edge_text plot, and node labels. It has 4 nodes and 5 edges.314151234flow12345

Readable labels

Labelling every vertex of a real network is unreadable, and labels that sit on the markers or vanish into the edges are worse. mark_node_text() has four tools for this:

  • top_n / by label only the most important vertices (e.g. the highest-degree hubs) – the common “label a handful, not all 34” move as a one-liner.
  • dist pushes each label radially outward from the layout centre, so it clears its node marker.
  • repel = TRUE nudges any labels that still overlap apart, ggrepel-style, with a thin leader line back to each vertex.
  • effects = list(shadow()) drops a shadow behind each label so it stays legible where it crosses an edge. mark_edge_text() takes the same effects.
vgraph(g, layout = "stress") |>
  mark_edges(alpha = 0.3) |>
  mark_nodes(size = deg, fill = grp) |>
  mark_node_text(
    label = name,
    top_n = 8,
    by = deg,
    dist = 3,
    color = "white",
    effects = list(shadow(x = 0.4, y = -0.4))
  ) |>
  scale_size(range = c(0.5, 3))
A plot combining network graph and node labels. It has 34 nodes and 78 edges, where colour shows grp and size shows deg.1234932333412349323334grp1234deg481216

Edge routing

Edges are straight by default. routing = "elbow" draws orthogonal right-angle steps instead – still straight segments (curved edges are a deliberate non-goal), stepping along whichever axis the endpoints are farther apart on, so a top-down tree bends downward. It is the natural routing for "tree", "sugiyama", and dendrogram layouts, and keeps node-boundary caps and arrowheads.

tr <- igraph::make_tree(15, children = 2, mode = "out")
vgraph(tr, layout = "tree") |>
  mark_edges(routing = "elbow", arrow = TRUE) |>
  mark_nodes(size = 2, fill = "steelblue")
A network graph. It has 15 nodes and 14 edges.

For directed graphs, gradient = TRUE fades each edge from faint at its source to opaque at its target – a direction cue that reads without arrowheads (and without the clutter they add on a dense graph).

dg <- igraph::sample_gnp(15, 0.2, directed = TRUE)
vgraph(dg) |>
  mark_edges(gradient = TRUE) |>
  mark_nodes(size = 2, fill = "grey30")
A network graph. It has 15 nodes and 48 edges.

Edge bundling

On a dense graph, straight edges pile into an unreadable hairball. mark_edge_bundle() routes them as bundled curves instead, so edges that run roughly together merge into a few trunks and the backbone of the graph shows through. It is a drop-in swap for mark_edges() – the same edge aesthetics apply – and delegates the geometry to the edgebundle package. type picks the algorithm; bundled edges are faint by default so overlapping trunks read as density.

gb <- igraph::sample_gnp(60, 0.08)
vgraph(gb, layout = "stress") |>
  mark_edge_bundle(type = "hammer", color = "firebrick") |>
  mark_nodes(size = 1.5, fill = "grey20")
A plot combining edge_bundle plot and network graph. It has 60 nodes and 135 edges.

The other algorithms trade off speed against how aggressively they merge: "force" (the default, force-directed) and "path" bend edges gently, "stub" only tufts each endpoint, and "mingle" merges hierarchically. For a directed graph, type = "divided" splits each trunk by direction. Pass algorithm-specific tuning through params, e.g. params = list(compatibility_threshold = 0.5).

Flow maps

A flow map shows one source fanning out to many destinations, the branches merging into trunks whose width grows with the combined flow – the shape Minard used for Napoleon’s march and cartographers use for migration. Give mark_flow_map() a one-to-many (star) graph laid out at fixed coordinates, name the root, and map the per-edge flow to weight.

set.seed(1)
dest <- 10
fg <- igraph::make_star(dest + 1, mode = "undirected")
igraph::V(fg)$name <- c("hub", paste0("d", seq_len(dest)))
igraph::E(fg)$weight <- sample(1:20, dest, replace = TRUE)
coords <- rbind(
  c(0, 0),
  cbind(runif(dest, 1, 6), runif(dest, -3, 3))
)

vgraph(fg, layout = coords) |>
  mark_flow_map(root = "hub", weight = weight) |>
  mark_nodes(size = 1.5, fill = "grey30")
A plot combining flow_map plot and network graph. It has 11 nodes and 10 edges.

type = "spiral" (the default) is the recommended layout: a planar, angle-restricted spiral tree that needs only the edgebundle package. type = "steiner" builds an approximate Steiner tree instead (it additionally needs the package). The computed flow is mapped onto width_range, so the widest trunk near the root stays legible however lopsided the weights are.

Dendrograms

vgraph() also accepts a base hclust/dendrogram, coercing it to a tree that carries each merge’s height. The "dendrogram" layout places leaves on a line and every merge at its height, and elbow_at = "start" turns the elbow into the classic bracket (siblings share a bar at the parent’s level):

hc <- hclust(dist(USArrests))
vgraph(hc, layout = "dendrogram") |>
  mark_edges(routing = "elbow", elbow_at = "start", elbow_axis = "v")
A network graph. It has 99 nodes and 98 edges.

Add labels with mark_text(), which controls angle and justification – for a top-down tree the leaf labels read vertically, hanging below their leaves (merge nodes carry a blank label, so only the leaves show):

vgraph(hc, layout = "dendrogram") |>
  mark_edges(routing = "elbow", elbow_at = "start", elbow_axis = "v") |>
  mark_text(
    x = x, y = y, label = label, size = 1.6,
    angle = 90, hjust = "right", nudge_y = -1.5
  )
A plot combining network graph and text-label plot. It has 99 nodes and 98 edges.AlabamaAlaskaArizonaArkansasCaliforniaColoradoConnecticutDelawareFloridaGeorgiaHawaiiIdahoIllinoisIndianaIowaKansasKentuckyLouisianaMaineMarylandMassachusettsMichiganMinnesotaMississippiMissouriMontanaNebraskaNevadaNew HampshireNew JerseyNew MexicoNew YorkNorth CarolinaNorth DakotaOhioOklahomaOregonPennsylvaniaRhode IslandSouth CarolinaSouth DakotaTennesseeTexasUtahVermontVirginiaWashingtonWest VirginiaWisconsinWyoming

vdendrogram() is the one-line preset for all of that – bracket edges and placed leaf labels, direction to orient it, and k to cut the tree and colour the clusters (branches above the cut stay neutral):

vdendrogram(hc, k = 3)
A plot combining network graph and text-label plot. It has 99 nodes and 98 edges, where colour shows cluster.AlabamaAlaskaArizonaCaliforniaDelawareFloridaIllinoisLouisianaMarylandMichiganMississippiNevadaNew MexicoNew YorkNorth CarolinaSouth CarolinaArkansasColoradoGeorgiaMassachusettsMissouriNew JerseyOklahomaOregonRhode IslandTennesseeTexasVirginiaWashingtonWyomingConnecticutHawaiiIdahoIndianaIowaKansasKentuckyMaineMinnesotaMontanaNebraskaNew HampshireNorth DakotaOhioPennsylvaniaSouth DakotaUtahVermontWest VirginiaWisconsincluster123

For unrooted trees (phylogeny-style), layout = "unrooted" uses graphlayouts’ layout_as_tree_unrooted()mode picks "equalangle", "equaldaylight", or "stress":

vgraph(igraph::make_tree(31, 3, "undirected"), layout = "unrooted", mode = "equaldaylight") |>
  mark_edges(alpha = 0.6) |>
  mark_nodes(size = 1.5, fill = "grey30")
A network graph. It has 31 nodes and 30 edges.

Augmenting and filtering

Vertex metrics are the analyst’s choice, not something vgraph() computes behind your back – but augment attaches a set on request, so you can map or filter by them without a manual igraph round-trip. augment = TRUE adds degree and components; a character vector picks from degree, betweenness, closeness, eigen, coreness, components, community, and the in/out-degree variants.

filter_nodes / filter_edges take data-masked predicates over the vertex / edge attributes, and k_core keeps the k-core. Filtering happens before the layout, so you see a clean layout of the subgraph rather than a full layout with holes – the honest way to tame a dense graph (the node-link idiom breaks down past a link density of ~3).

# attach degree + community, then plot just the 2-core, sized and coloured by them
vgraph(g, augment = c("degree", "community"), k_core = 2) |>
  mark_edges(alpha = 0.3) |>
  mark_nodes(size = degree, fill = factor(community)) |>
  mark_node_text(label = name, top_n = 6, by = degree, dist = 3) |>
  scale_size(range = c(0.5, 3))
A plot combining network graph and node labels. It has 33 nodes and 77 edges, where colour shows factor(community) and size shows degree.12343233factor(community)1234degree481216

Community hulls and node glyphs

mark_node_hull() shades groups of nodes with a convex hull drawn behind the graph – the standard way to show community structure. Group by a mapped fill; expand grows each hull so it wraps the markers rather than clipping them.

g <- igraph::set_vertex_attr(
  g, "comm",
  value = factor(igraph::membership(igraph::cluster_louvain(g)))
)
vgraph(g, layout = "stress") |>
  mark_node_hull(fill = comm, expand = 0.12) |>
  mark_edges(alpha = 0.3) |>
  mark_nodes(size = deg, fill = comm) |>
  scale_size(range = c(0.5, 3))
A plot combining hull plot and network graph. It has 34 nodes and 78 edges, where colour shows comm and size shows deg.comm1234deg481216

mark_node_pie() replaces the node markers with pie (or donut, via inner) glyphs whose wedges come from a set of compositional columns – one wedge per column, sized by that column’s value at each vertex.

set.seed(1)
for (nm in c("x1", "x2", "x3")) {
  g <- igraph::set_vertex_attr(g, nm, value = runif(igraph::vcount(g)))
}
vgraph(g, layout = "stress") |>
  mark_edges(alpha = 0.3) |>
  mark_node_pie(cols = c("x1", "x2", "x3"), size = 5)
A plot combining network graph and node_pie plot. It has 34 nodes and 78 edges.

Interactive neighbour highlighting

select_neighbours() is a network-aware selection preset: in an interactive host (vellumwidget::as_widget()), pointing at a node spotlights its neighbourhood — the node, its incident edges, and its adjacent nodes — and dims the rest; pointing at an edge highlights its two endpoints. degree = 2 reaches neighbours-of-neighbours. Like every selection it is inert on a static render, so the code below draws the same picture on the page but comes alive under as_widget().

vgraph(g, layout = "stress") |>
  mark_edges(alpha = 0.3) |>
  mark_nodes(size = deg, fill = grp) |>
  mark_node_text(label = name, top_n = 8, by = deg) |>
  select_neighbours(on = "hover") |>
  scale_size(range = c(0.5, 3))

Declaring the selection also keys every node (by its vertex name) and edge, so node/edge tooltips, click-select, and pan/zoom work in the widget too.

Choosing a layout

layout accepts a name ("stress", "sparse_stress", "backbone", "fr", "kk", "circle", "tree", "sugiyama", "dendrogram", "unrooted", and more), a supplied N-by-2 coordinate matrix, or a function that returns one. Stochastic layouts take a seed so the figure is reproducible.

vgraph(g, layout = "circle") |>
  mark_edges(alpha = 0.3) |>
  mark_nodes(fill = grp, size = 2)
A network graph. It has 34 nodes and 78 edges, where colour shows grp.grp1234

Both of these are still ordinary specs. They face the same scales, themes, and composition tools as any other plot, and they render to a file with render_plot(). For per-feature interactivity (tooltips and data keys on map features or nodes), see the interactivity notes on mark_sf() and the mark reference pages.