Network Graphs¶
ggplotly provides specialized geoms for network visualization, including force-directed edge bundling for reducing visual clutter in dense graphs.
import numpy as np
import pandas as pd
from ggplotly import *
# Create nodes in a circular layout
n_nodes = 20
angles = np.linspace(0, 2 * np.pi, n_nodes, endpoint=False)
radius = 10
node_x = radius * np.cos(angles)
node_y = radius * np.sin(angles)
# Create edges across the circle
edges = []
for i in range(n_nodes):
for offset in [5, 7, 10]:
j = (i + offset) % n_nodes
edges.append({
'x': node_x[i], 'y': node_y[i],
'xend': node_x[j], 'yend': node_y[j]
})
edges_df = pd.DataFrame(edges)
nodes_df = pd.DataFrame({'x': node_x, 'y': node_y})
(ggplot(edges_df, aes(x='x', y='y', xend='xend', yend='yend'))
+ geom_edgebundle(compatibility_threshold=0.6)
+ geom_point(data=nodes_df, mapping=aes(x='x', y='y'), color='white', size=4)
+ theme_dark()
+ labs(title='Circular Network with Edge Bundling'))
Bundling 60 edges... Initial edge division (P=1)... Computing angle compatibility... Computing scale compatibility... Computing position compatibility... Filtering candidate pairs... Computing visibility for 800 candidate pairs (out of 1,770 total) Filtered out 970 pairs (54.8%) Computing final compatibility scores... Compatibility matrix: 440 compatible pairs Cycle 1/6: I=50, P=1, S=0.0400 Updating subdivisions (new P=2)... Cycle 2/6: I=33, P=2, S=0.0200
Updating subdivisions (new P=4)... Cycle 3/6: I=22, P=4, S=0.0100 Updating subdivisions (new P=8)... Cycle 4/6: I=14, P=8, S=0.0050
Updating subdivisions (new P=16)... Cycle 5/6: I=9, P=16, S=0.0025 Updating subdivisions (new P=32)... Cycle 6/6: I=6, P=32, S=0.0013
Assembling output... Done!
Random Network¶
np.random.seed(42)
n_nodes = 30
n_edges = 80
node_x = np.random.uniform(0, 100, n_nodes)
node_y = np.random.uniform(0, 100, n_nodes)
edges = []
for _ in range(n_edges):
i, j = np.random.choice(n_nodes, 2, replace=False)
edges.append({
'x': node_x[i], 'y': node_y[i],
'xend': node_x[j], 'yend': node_y[j]
})
edges_df = pd.DataFrame(edges)
nodes_df = pd.DataFrame({'x': node_x, 'y': node_y})
(ggplot(edges_df, aes(x='x', y='y', xend='xend', yend='yend'))
+ geom_edgebundle(C=5, compatibility_threshold=0.5)
+ geom_point(data=nodes_df, mapping=aes(x='x', y='y'), color='#00ff00', size=5)
+ theme_dark()
+ labs(title='Random Network with Edge Bundling'))
Bundling 80 edges... Initial edge division (P=1)... Computing angle compatibility... Computing scale compatibility... Computing position compatibility... Filtering candidate pairs... Computing visibility for 1,519 candidate pairs (out of 3,160 total) Filtered out 1,641 pairs (51.9%) Computing final compatibility scores... Compatibility matrix: 368 compatible pairs Cycle 1/5: I=50, P=1, S=0.0400 Updating subdivisions (new P=2)... Cycle 2/5: I=33, P=2, S=0.0200
Updating subdivisions (new P=4)... Cycle 3/5: I=22, P=4, S=0.0100 Updating subdivisions (new P=8)... Cycle 4/5: I=14, P=8, S=0.0050
Updating subdivisions (new P=16)... Cycle 5/5: I=9, P=16, S=0.0025 Assembling output... Done!
Edge Bundling Parameters¶
geom_edgebundle has several parameters to control the bundling behavior:
| Parameter | Default | Description |
|---|---|---|
K |
1.0 | Spring constant - higher values resist bundling |
E |
1.0 | Electrostatic constant - higher values increase bundling |
C |
6 | Number of cycles - more cycles = smoother curves |
P |
1 | Initial edge subdivisions |
S |
0.04 | Initial step size |
compatibility_threshold |
0.6 | Minimum edge compatibility (0-1) |
color |
'#9d0191' | Edge color |
alpha |
0.8 | Edge transparency |
linewidth |
0.5 | Edge line width |
show_highlight |
True | Add highlight effect on edges |
highlight_color |
'white' | Highlight line color |
verbose |
True | Print progress messages |
Weighted Edge Bundling¶
Edges can have weights that affect how strongly they attract other edges during bundling:
# Edges with weights - heavier edges attract lighter ones
edges_df = pd.DataFrame({
'x': [0, 0, 0],
'y': [0, 1, 2],
'xend': [10, 10, 10],
'yend': [0, 1, 2],
'traffic': [100, 10, 10] # First edge is 10x heavier
})
(ggplot(edges_df, aes(x='x', y='y', xend='xend', yend='yend', weight='traffic'))
+ geom_edgebundle(C=4, compatibility_threshold=0.5)
+ theme_dark())
Using edge weights (range: 10.00 - 100.00) Bundling 3 edges... Initial edge division (P=1)... Computing angle compatibility... Computing scale compatibility... Computing position compatibility... Filtering candidate pairs... Computing visibility for 3 candidate pairs (out of 3 total) Filtered out 0 pairs (0.0%) Computing final compatibility scores... Compatibility matrix: 6 compatible pairs Cycle 1/4: I=50, P=1, S=0.0400 Updating subdivisions (new P=2)... Cycle 2/4: I=33, P=2, S=0.0200 Updating subdivisions (new P=4)... Cycle 3/4: I=22, P=4, S=0.0100 Updating subdivisions (new P=8)... Cycle 4/4: I=14, P=8, S=0.0050 Assembling output... Done!
Using igraph Graphs¶
geom_edgebundle can directly accept igraph Graph objects:
import igraph as ig
from ggplotly import data
# Load built-in US flights network
g = data('us_flights')
(ggplot()
+ geom_map(map_type='usa')
+ geom_edgebundle(graph=g, show_nodes=True, node_color='white', node_size=3)
+ theme_dark()
+ labs(title='US Flight Network'))
Bundling 2682 edges... Initial edge division (P=1)... Computing angle compatibility... Computing scale compatibility...
Computing position compatibility...
Filtering candidate pairs... Computing visibility for 505,344 candidate pairs (out of 3,595,221 total) Filtered out 3,089,877 pairs (85.9%) Computing final compatibility scores...
Compatibility matrix: 82084 compatible pairs Cycle 1/6: I=50, P=1, S=0.0400
Updating subdivisions (new P=2)... Cycle 2/6: I=33, P=2, S=0.0200
Updating subdivisions (new P=4)... Cycle 3/6: I=22, P=4, S=0.0100
Updating subdivisions (new P=8)... Cycle 4/6: I=14, P=8, S=0.0050
Updating subdivisions (new P=16)... Cycle 5/6: I=9, P=16, S=0.0025
Updating subdivisions (new P=32)...
Cycle 6/6: I=6, P=32, S=0.0013
Assembling output... Done!
The graph vertices must have coordinate attributes (longitude/lon/x and latitude/lat/y). Edge weights are automatically detected from the weight attribute.
Edge Bundling on Maps¶
When geom_map is present in the plot, geom_edgebundle automatically uses geographic coordinates:
airports = pd.DataFrame({
'lon': [-122.4, -73.8, -87.6, -118.4, -95.3, -84.4],
'lat': [37.8, 40.6, 41.9, 34.0, 29.8, 33.6],
'name': ['SFO', 'JFK', 'ORD', 'LAX', 'IAH', 'ATL']
})
flights = pd.DataFrame({
'src_lon': [-122.4, -73.8, -87.6, -118.4, -95.3, -84.4, -122.4, -73.8],
'src_lat': [37.8, 40.6, 41.9, 34.0, 29.8, 33.6, 37.8, 40.6],
'dst_lon': [-73.8, -87.6, -118.4, -95.3, -84.4, -122.4, -84.4, -118.4],
'dst_lat': [40.6, 41.9, 34.0, 29.8, 33.6, 37.8, 33.6, 34.0]
})
(ggplot(flights, aes(x='src_lon', y='src_lat', xend='dst_lon', yend='dst_lat'))
+ geom_map(map_type='usa')
+ geom_point(data=airports, mapping=aes(x='lon', y='lat'), color='white', size=8)
+ geom_edgebundle(C=4, compatibility_threshold=0.5, verbose=False)
+ theme_dark()
+ labs(title='US Flights with Edge Bundling'))
shipping_routes = pd.DataFrame({
'origin': ['Rotterdam', 'Shanghai', 'Los Angeles'],
'x': [4.48, 121.47, -118.24], # origin longitude
'y': [51.92, 31.23, 33.73], # origin latitude
'xend': [121.47, -74.01, 4.48], # destination longitude
'yend': [31.23, 40.71, 51.92] # destination latitude
})
(ggplot(shipping_routes, aes(x='x', y='y', xend='xend', yend='yend'))
+ geom_map(map_type='world')
+ geom_searoute(color='steelblue', linewidth=1.0)
+ theme_dark()
+ labs(title='Maritime Shipping Routes'))
Routes with Restrictions¶
Force routes to avoid certain passages (e.g., for geopolitical or capacity reasons):
(ggplot(shipping_routes, aes(x='x', y='y', xend='xend', yend='yend'))
+ geom_map(map_type='world')
+ geom_searoute(
restrictions=['suez'],
color='#ff6b35',
show_highlight=True,
show_ports=True,
port_color='#00ff88',
verbose=True # Shows route distances
)
+ theme_dark()
+ labs(title='Shipping Routes Avoiding Suez Canal'))
Computing 3 sea routes... Successfully computed 3 routes Total distance: 46022.2 km
Sea Route Parameters¶
| Parameter | Default | Description |
|---|---|---|
restrictions |
[] | Passages to avoid: 'suez', 'panama', 'northwest', 'northeast' |
color |
'#9d0191' | Route line color |
linewidth |
0.5 | Line width |
show_highlight |
True | Add glow effect |
show_ports |
False | Show origin/destination markers |
port_color |
'white' | Port marker color |
port_size |
5 | Port marker size |
verbose |
False | Print route distances |
Caching¶
Edge bundling is computationally expensive. Results are automatically cached at the module level, so repeated plots with the same data and parameters are instant:
from ggplotly.stats.stat_edgebundle import clear_bundling_cache
# Clear cache if needed (e.g., memory constraints)
clear_bundling_cache()
The cache key includes:
- Edge coordinates (x, y, xend, yend)
- Edge weights (if provided)
- All algorithm parameters (K, E, C, P, S, etc.)
# Simple flow from sources to targets
flow_df = pd.DataFrame({
'source': ['A', 'A', 'B', 'B', 'C'],
'target': ['X', 'Y', 'X', 'Y', 'Y'],
'value': [10, 20, 15, 25, 5]
})
(ggplot(flow_df, aes(source='source', target='target', value='value'))
+ geom_sankey()
+ labs(title='Basic Sankey Diagram'))
Multi-Stage Sankey¶
For multi-stage flows (e.g., budget allocation):
# Multi-stage budget flow
budget_df = pd.DataFrame({
'source': ['Budget', 'Budget', 'Sales', 'Sales', 'Marketing', 'Marketing', 'R&D'],
'target': ['Sales', 'Marketing', 'Revenue', 'Costs', 'Revenue', 'Costs', 'Revenue'],
'value': [100, 80, 90, 10, 60, 20, 50]
})
(ggplot(budget_df, aes(source='source', target='target', value='value'))
+ geom_sankey(node_pad=20, node_thickness=25)
+ labs(title='Budget Allocation Flow'))
Custom Colors¶
# Custom node colors
(ggplot(flow_df, aes(source='source', target='target', value='value'))
+ geom_sankey(
node_color=['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd'],
link_alpha=0.5
)
+ labs(title='Sankey with Custom Colors'))
Sankey Parameters¶
| Parameter | Default | Description |
|---|---|---|
node_pad |
15 | Padding between nodes |
node_thickness |
20 | Thickness of nodes |
node_color |
auto | Color(s) for nodes |
link_color |
auto | Color for links (uses source node color) |
link_alpha |
0.4 | Transparency of links |
orientation |
'h' | 'h' (horizontal) or 'v' (vertical) |
arrangement |
'snap' | Node arrangement: 'snap', 'perpendicular', 'freeform', 'fixed' |