| Layout | Description | Use case |
|---|---|---|
circular |
Nodes on a circle | General networks, equal importance |
focus |
One node centered, rest on a ring | Hub-and-spoke, dominant node |
bilayer |
Two concentric rings | Compare inner vs outer groups |
grid |
Regular grid | Sequential or matrix layouts |
manual |
User-supplied coordinates | Full custom positioning |
from netbubbles import circular, focus, bilayer, grid, manual
pos = circular(node_names, radius=3.0)
pos = focus(node_names, center="Hub")
pos = bilayer(inner_nodes, outer_nodes)from netbubbles.presets import liana
df = liana.load_results(cache_dir)
g = liana.to_graph(df["timepoint_1"], node_colors=colors)
g_merged = liana.merge_nodes(g, "Mac", "Macrophage")from netbubbles.presets import citations
entries = citations.parse_bibtex("references.bib")
g = citations.to_graph(entries, citation_map=cites, mode="paper")from netbubbles.presets import dependencies
deps = dependencies.parse_requirements("requirements.txt")
g = dependencies.to_graph(deps, root="my-app")from netbubbles.presets import pipeline
steps = [
{"name": "Extract", "type": "extract", "inputs": []},
{"name": "Transform", "type": "transform", "inputs": ["Extract"]},
{"name": "Load", "type": "store", "inputs": ["Transform"]},
]
g = pipeline.to_graph(steps)from netbubbles.presets import webgraph
links = {"home": ["about", "blog"], "blog": ["home", "post-1"]}
g = webgraph.from_links(links, root="home")from netbubbles.presets import social
edges = [("Alice", "Bob", 12), ("Bob", "Alice", 10), ("Alice", "Carol", 5)]
g = social.from_edge_list(edges)
clusters = social.detect_clusters(g)
g_colored = social.from_edge_list(edges, clusters=clusters)from netbubbles import Style
from netbubbles.style import EdgeTier
my_style = Style(
node_edgecolor="black",
node_edgewidth=2.0,
shadow_offset=0.02,
background_color="#F5F5F5",
curve_strength=0.25,
edge_tiers=[
EdgeTier("#D62728", 4.5, 0.95),
EdgeTier("#FF7F0E", 3.2, 0.80),
EdgeTier("#2CA02C", 2.0, 0.60),
EdgeTier("#AAAAAA", 1.2, 0.40),
],
label_fontsize=14,
title_fontsize=28,
)
ax = nb.draw(graph, style=my_style, title="Custom Styled")Style(high_density=...) controls how edges are drawn when nodes are tightly packed.
| Value | Behavior |
|---|---|
"auto" |
Default. Automatically detects layout density and picks "on" or "off". |
"off" |
Classic rendering: simple midpoint Bézier offset, angles toward the partner node, relaxed for separation. Use for sparse, well-spaced graphs. |
"on" |
Dense rendering: inward-pulling Bézier curves, angles fanning from the inward direction grouped by weight tier, node avoidance pass, and overlap separation pass. Use when nodes are crowded or overlapping. |
Auto detection compares inter-node gaps to node radii. If any pair of nodes has a gap smaller than 30% of the smaller radius, dense mode activates.
Angle spreading in dense mode works per weight tier: thick edges (high weight) are spread independently from thin edges — each tier fans out from the node's inward angle as if the other tiers don't exist. This keeps heavy connections visually dominant and avoids them being pushed aside by a large number of lighter edges.
# Explicit off — same output as classic rendering
Style(high_density="off")
# Explicit on — full dense pipeline
Style(high_density="on")
# Auto — let netbubbles decide (default)
Style(high_density="auto")sub = g.subgraph(["A", "B", "C"])
heavy = g.filter_edges(lambda e: e.weight >= 5)
agg = g.aggregate_edges()nb.add_legend(fig, node_names, color_dict)
fig, axes = plt.subplots(1, 3, figsize=(21, 7))
for ax, data in zip(axes, datasets):
nb.draw(graph, ax=ax, title=data["label"])