Use lightgraph in R#
This vignette is the R twin of the Python vignette: a feature-by-feature tour where every option group gets a code example and every figure is a live lightgraph htmlwidget — pan, zoom, hover, click, box-select, and double-click them.
The lightgraph() function takes the same arguments as Python’s
net_vis() (same names, same defaults), and every analytics helper
exists with an lg_ prefix, returning named vectors instead of dicts.
We work with the same real network throughout: the OpenFlights world flight network — 3,257 airports and 18,930 routes. The hero figure below renders all of it; later sections zoom into its European and competitive-route slices, so a dozen live figures share one page and stay smooth.
Overview#
library(lightgraph)
# the visualization
lightgraph(nodes, edges, ...)
# the analytics
lg_summary(); lg_degree(); lg_betweenness(); lg_closeness()
lg_eigenvector(); lg_pagerank(); lg_communities(); lg_components()
lg_neighbors(); lg_top_nodes()
# Shiny bindings
lightgraphOutput(); renderLightgraph()
Installation#
# install.packages("remotes")
remotes::install_github("haozhu233/lightgraph", subdir = "R")
Widgets display in the RStudio Viewer, knit into R Markdown and Quarto documents, drop into Shiny, and save to fully standalone HTML.
Getting Started#
Edges are enough; nodes are derived automatically:
friends <- data.frame(
source = c("Amy", "Amy", "Ben", "Cat", "Dan", "Dan", "Eve"),
target = c("Ben", "Cat", "Cat", "Dan", "Eve", "Fay", "Fay"),
weight = c(3, 1, 2, 1, 2, 1, 3)
)
lightgraph(edges = friends)
A nodes data frame adds per-node attributes (id required; group,
color, size optional), and adjacency_to_lightgraph() converts
adjacency matrices:
nodes <- data.frame(id = c("Amy", "Ben"), group = c("g1", "g2"))
lightgraph(nodes, edges)
parts <- adjacency_to_lightgraph(adj_matrix, node_names)
lightgraph(parts$nodes, parts$edges)
The Flight Network#
Every scheduled airline route in the
OpenFlights database. Each row of
routes.dat is one airline serving one leg, so aggregating duplicates
gives natural edge weights:
base <- "https://raw.githubusercontent.com/jpatokal/openflights/master/data/"
airports <- read.csv(paste0(base, "airports.dat"), header = FALSE,
na.strings = "\\N")[, c(2, 4, 5, 12)]
names(airports) <- c("name", "country", "id", "tz")
airports <- airports[!is.na(airports$id), ]
# continents from the tz name ("Europe/Paris" -> "Europe")
airports$group <- sub("/.*", "", airports$tz)
routes <- read.csv(paste0(base, "routes.dat"), header = FALSE,
na.strings = "\\N")[, c(3, 5)]
names(routes) <- c("source", "target")
routes <- routes[complete.cases(routes), ]
# one row per airline serving a leg -> weighted undirected edges
und <- data.frame(source = pmin(routes$source, routes$target),
target = pmax(routes$source, routes$target))
edges <- aggregate(list(weight = rep(1, nrow(und))), by = und, FUN = sum)
edges <- edges[edges$source %in% airports$id &
edges$target %in% airports$id, ]
nodes <- airports[, c("id", "group")]
What are we looking at?
str(lg_summary(edges))
List of 8
$ nodes : int 3257
$ edges : num 18930
$ density : num 0.00357
$ average_degree : num 11.6
$ max_degree : int 248
$ components : int 7
$ largest_component: int 3231
$ transitivity : num 0.249
Mild density, high clustering, seven components (the giant one holds 3,231 airports) — and the busiest hubs are exactly who you’d guess:
strength <- lg_degree(edges, weighted = TRUE) # route listings per airport
lg_top_nodes(strength, 8)
ATL ORD LHR PEK CDG FRA LAX DFW
1826 1108 1051 1050 1041 990 986 936
And the zero-configuration picture — node size and edge opacity adapt to density automatically, and the layout is warmed up before the first paint:
lightgraph(edges = edges)
Drag to pan, scroll to zoom, hover an airport to light up its neighborhood, shift-drag to box-select, and double-click a hub to isolate its ego network (Escape restores).
Node Styling#
Coloring by group#
A group column on the nodes data frame colors nodes and builds the
interactive legend. The airports table lists thousands of fields with no
routes, so we drop them with remove_unconnected = TRUE:
lightgraph(nodes, edges, remove_unconnected = TRUE,
show_ellipses = FALSE)
No coordinates anywhere in the data — the force layout reconstructs a
recognizable world map from route topology alone. show_ellipses
(default TRUE) adds a covariance ellipse per group; for groups as
interwoven as continents it reads better off.
Stable colors across figures#
Palette colors are assigned to groups in sorted-name order, so the same
data always renders the same colors. In a series of figures that
filters the data, though, a group that drops out of one figure would
shift the palette for every group after it. Compute group_order
once from the full dataset and pass it to every figure: listed groups
keep their palette slot even when absent, so each group’s color is
identical in every subset. group_colors pins exact colors on top of
that; names matching no group in a given figure are ignored, so one
vector serves the whole series:
continents <- sort(unique(nodes$group[!is.na(nodes$group)]))
lightgraph(nodes, edges, remove_unconnected = TRUE,
show_ellipses = FALSE, group_order = continents)
# routes flown by 5+ airlines: the Arctic drops out — colors hold anyway
busy <- edges[edges$weight >= 5, ]
lightgraph(nodes, busy, remove_unconnected = TRUE,
show_ellipses = FALSE, group_order = continents)
# or pin colors outright
lightgraph(nodes, edges, remove_unconnected = TRUE,
group_colors = c(Europe = "#1f77b4", Asia = "#ff7f0e"))
Both figures share one group_order. The busy slice has no Arctic
routes left — without group_order every continent sorting after
Arctic would slide up one palette slot in the second figure; here each
keeps its color. Note continents also lists Antarctica, which has
no scheduled routes and appears in neither figure — absent names simply
hold their slot, so one list serves every figure in the series.
Metric-driven size#
Any named numeric vector drives node size via node_metric; values
are min-max normalized into metric_size_range. For the styling
sections we zoom into one slice with plenty of structure: the European
subnetwork (561 airports, 5,088 routes). Size encodes route volume
within Europe — and the intra-European ranking holds a surprise worth
plotting, with Barcelona and holiday-island Palma de Mallorca outranking
Frankfurt and Paris once intercontinental routes are excluded:
europe <- airports$id[airports$group == "Europe"]
eu <- edges[edges$source %in% europe & edges$target %in% europe, ]
strength_eu <- lg_degree(eu, weighted = TRUE)
lg_top_nodes(strength_eu, 8)
# BCN AMS PMI FRA MUC VIE CDG LGW
# 615 549 544 518 512 493 478 454
lightgraph(edges = eu, node_metric = strength_eu,
metric_size_range = c(2, 26),
metric_label = "Route volume")
Metric-driven color#
metric_map chooses the channel: "size" (default), "color",
or "both", interpolating between the metric_colors endpoints.
When the legend is on, a metric section shows the size dots and color
gradient with the metric’s min/max values — title it with
metric_label (e.g. metric_label = "PageRank"). Here a PageRank
heat map of the European network:
lightgraph(edges = eu, node_metric = lg_pagerank(eu),
metric_map = "both",
metric_colors = c("#dbe9f6", "#08306b"),
metric_label = "PageRank")
Per-node overrides#
Explicit size / color columns on the nodes data frame always win
over metric-derived values, and group colors win over node colors.
node_size and node_color set the defaults for everyone else.
Edge Styling#
Weights to width and opacity#
edge_weight_to_width / edge_weight_to_opacity map weights into
weight_width_range / weight_opacity_range. On the European
subnetwork, busy corridors turn into thick, solid strokes while
single-airline routes fade back:
lightgraph(edges = eu,
edge_weight_to_width = TRUE,
edge_weight_to_opacity = TRUE,
weight_width_range = c(0.3, 5),
node_metric = strength_eu,
metric_label = "Route volume")
By default edge opacity adapts continuously to on-screen density and
zoom — zoom in and watch edges solidify. edge_opacity = 0.4 pins a
fixed value; edge_width and edge_color set the base stroke.
Direction arrows#
For directed data, aggregate ordered pairs instead. Routes between the 25 busiest airports:
top25 <- names(lg_top_nodes(strength, 25))
dir25 <- routes[routes$source %in% top25 & routes$target %in% top25, ]
directed <- aggregate(list(weight = rep(1, nrow(dir25))),
by = dir25, FUN = sum)
lightgraph(edges = directed, show_arrows = TRUE,
edge_weight_to_width = TRUE, node_size = 12,
label_font_size = 8, link_distance = 140,
edge_opacity = 0.5)
Layouts#
Force layout tuning#
lightgraph(edges = edges,
simulation_strength = 4000, # node repulsion
link_distance = 100, # preferred edge length
group_attraction = 0.3, # pull toward group centroids
warmup_ticks = "auto") # settle before first paint
Circular layout#
layout = "circular" starts nodes on a ring ordered by group, then
lets a gentle simulation relax it — a chord-diagram view of the top 40
hubs that settles into its natural clusters:
top40 <- names(lg_top_nodes(strength, 40))
t40 <- edges[edges$source %in% top40 & edges$target %in% top40, ]
lightgraph(nodes[nodes$id %in% top40, ], t40, layout = "circular",
edge_weight_to_opacity = TRUE, node_size = 10,
label_font_size = 8)
Interaction & Exploration#
Everything is modeless — no tool switching:
Hover highlights the 1-hop neighborhood; the rest fades to
neighbor_fadeopacity (highlight_neighbors = FALSEdisables).Double-click isolates the
ego_depth-hop ego network; Escape or double-clicking empty space restores.Click selects; shift-click toggles; shift-drag box-selects; search matches ids as you type; legend clicks select groups.
show_tooltipscontrols hover tooltips;show_statistics = TRUEadds a live stats panel; dragging a node pins it.
lightgraph(edges = edges,
ego_depth = 2, # double-click reveals 2 hops
neighbor_fade = 0.06, # fade non-neighbors harder
show_statistics = TRUE)
Themes#
theme = "dark" swaps canvas, panels, labels, and default colors;
background_color overrides just the canvas. This figure also carries
the interaction tuning above — double-click any airport for its two-hop
reach:
lightgraph(edges = eu, theme = "dark",
node_metric = strength_eu, metric_size_range = c(2, 24),
metric_label = "Route volume",
ego_depth = 2, neighbor_fade = 0.06)
Graph Analytics#
The lg_* functions are dependency-free (igraph is used for Louvain
communities when installed, never required), treat graphs as undirected,
and return named vectors keyed by node id.
Function |
What it tells you |
|---|---|
|
node/edge counts, density, degree stats, components, transitivity |
|
connections per node (weighted: strength) |
|
who sits on the shortest paths (Brandes) |
|
who can reach everyone fastest |
|
who is connected to the well-connected |
|
random-surfer importance |
|
Louvain / label-propagation groups (‘c1’, ‘c2’, …) |
|
connected component ids, largest first |
|
the k-hop ego set around a node |
|
the n best entries of any metric vector |
Communities#
node_groups = "auto" runs lg_communities() under the hood.
Since it returns a named vector, you can also post-process it. Here we
detect communities on the network’s competitive core — routes flown by
more than one carrier (weight ≥ 3, since each carrier lists both
directions), which sharpens the community structure — then keep the 8
largest and leave the rest uncolored. What emerges is the geography of
air travel: each community is a regional route system, sized by its
route volume:
competitive <- edges[edges$weight >= 3, ]
comm <- lg_communities(competitive) # Louvain when igraph is installed
top8 <- names(sort(table(comm), decreasing = TRUE))[1:8]
comm <- comm[comm %in% top8]
lightgraph(edges = competitive, node_groups = comm,
show_ellipses = FALSE,
node_metric = lg_degree(competitive, weighted = TRUE),
metric_size_range = c(2, 22),
metric_label = "Route volume")
Betweenness: finding the brokers#
On the 500 busiest airports (a couple of seconds in pure R):
top500 <- names(lg_top_nodes(strength, 500))
sub <- edges[edges$source %in% top500 & edges$target %in% top500, ]
bt <- lg_betweenness(sub)
lg_top_nodes(bt, 8)
# AMS FRA CDG PEK DXB LHR IST HKG
# 0.0754 0.0708 0.0642 0.0544 0.0385 0.0366 0.0349 0.0319
Raw volume and brokerage disagree: Atlanta (the #1 hub by routes) is not a top broker, while Amsterdam, Frankfurt, and Paris — the gateways between route systems — take over:
lightgraph(edges = sub, node_metric = bt, metric_map = "both",
metric_colors = c("#d9d9d9", "#c22e00"),
metric_label = "Betweenness",
edge_weight_to_opacity = TRUE)
Slicing with components and neighbors#
comp <- lg_components(edges) # named integer, 1 = largest
mainland <- names(comp)[comp == 1]
reach <- lg_neighbors(edges, "ANC", depth = 2)
sub <- edges[edges$source %in% reach & edges$target %in% reach, ]
lightgraph(edges = sub, node_groups = "auto")
# a metric table in one data frame
metrics <- data.frame(
degree = lg_degree(edges),
strength = strength,
pagerank = lg_pagerank(edges)
)
head(metrics[order(-metrics$pagerank), ])
Performance at Scale#
Zoom and pan cost is dominated by edges drawn per frame — that is why most styling figures on this page run on European and competitive-route slices of the network instead of all 19k edges: halving the edge count roughly doubles the zoom frame rate, with no loss of resolution. Filtering nodes barely helps (edges concentrate on hubs — the 1,000 busiest airports still carry 14k of the 19k edges); filter edges by weight when frame rate is the concern. Beyond that, the dials are:
lightgraph(edges = big[big$weight >= 3, ],
warmup_ticks = 100, # cap pre-paint layout work
pixel_ratio = 1, # trade sharpness for fill-rate
show_labels = FALSE, # labels are the first to drop
export_scale = 4) # ...but export PNGs at 4x
The Config Escape Hatch#
Arguments cover the common surface; config deep-merges a raw
lightGraph configuration (the JS shape) over the generated one, so any
option in the JS DEFAULT_CONFIG is reachable:
lightgraph(edges = edges,
auto_fit = TRUE,
zoom_range = c(0.05, 10),
config = list(
nodes = list(borderWidth = 2,
selectedBorderColor = "#ffd700"),
labels = list(fontFamily = "Georgia, serif"),
simulation = list(centerStrength = 0.8)
))
Saving, R Markdown, and Shiny#
w <- lightgraph(nodes, edges, remove_unconnected = TRUE)
htmlwidgets::saveWidget(w, "flights.html") # standalone file
# R Markdown / Quarto: just print the widget in a chunk
# Shiny
ui <- fluidPage(lightgraphOutput("net"))
server <- function(input, output) {
output$net <- renderLightgraph(lightgraph(nodes, edges))
}
Viewers can also export PNG, SVG, and JSON from the widget toolbar.
The Same Thing in Python#
Every argument on this page has the same name and meaning in the Python package — see the Python vignette and the API reference parity table.