If you’ve built a LlamaIndex knowledge graph visualization pyvis tutorial before, you know the graph lives inside the index — but getting it out and into a browser takes a few deliberate steps. This guide walks through extracting nodes and edges from a KnowledgeGraphIndex, mapping them into a pyvis Network, and producing an interactive HTML file you can share with teammates or embed in a dashboard. You’ll end up with a runnable script and a clear verification step at each stage.
Step 1: Install dependencies and prepare data
Start with a clean virtual environment. You need LlamaIndex’s graph modules, pyvis for rendering, and a local LLM or API key for entity extraction. If you’re using OpenAI-compatible endpoints (like the one n4n.ai exposes across 240+ models), set OPENAI_API_BASE and OPENAI_API_KEY accordingly.
python -m venv .venv && source .venv/bin/activate
pip install "llama-index[knowledge-graph]" pyvis networkx
Create a small corpus so you can verify extraction works end to end. Save this as data/sample.txt:
Acme Corp acquired Beta Labs in March 2024. Beta Labs develops the open-source
vector database VegaDB. Acme's CEO, Priya Sharma, announced the deal at the
annual developer conference. VegaDB competes with Pinecone and Weaviate.
Verify: ls data/sample.txt shows the file exists.
Step 2: Build the KnowledgeGraphIndex
LlamaIndex’s KnowledgeGraphIndex constructs a property graph by prompting an LLM to extract triplets (subject, predicate, object) from each chunk. Use SimpleDirectoryReader to load the sample, then build the index with a local or remote LLM.
# build_graph.py
import os
from llama_index.core import (
SimpleDirectoryReader,
KnowledgeGraphIndex,
Settings,
)
from llama_index.llms.openai import OpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
# Configure LLM and embedding model — swap base_url for your gateway
Settings.llm = OpenAI(
model="gpt-4o-mini",
temperature=0.0,
)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
documents = SimpleDirectoryReader("data").load_data()
index = KnowledgeGraphIndex.from_documents(
documents,
max_triplets_per_chunk=10,
include_embeddings=True,
)
# Persist so we can inspect without re-running extraction
index.storage_context.persist(persist_dir="storage_graph")
print("Index built and persisted to storage_graph/")
Run it:
python build_graph.py
Verify: storage_graph/ contains docstore.json, graph_store.json, and vector_store.json. Open graph_store.json — you should see a triplets array with entries like ["Acme Corp", "acquired", "Beta Labs"].
Step 3: Extract graph data from the index
The KnowledgeGraphIndex stores triplets in its graph_store. LlamaIndex uses SimplePropertyGraphStore by default, which implements get_triplets(). Pull those triplets and convert them into nodes and edges for pyvis.
# extract_graph.py
import json
from llama_index.core import StorageContext, load_index_from_storage
from llama_index.core.graph_stores import SimplePropertyGraphStore
storage_context = StorageContext.from_defaults(persist_dir="storage_graph")
index = load_index_from_storage(storage_context)
graph_store: SimplePropertyGraphStore = index.property_graph_store
triplets = graph_store.get_triplets()
print(f"Extracted {len(triplets)} triplets")
for t in triplets[:5]:
print(t)
# Convert to node/edge lists for pyvis
nodes = set()
edges = [] # (source, target, label)
for subj, pred, obj in triplets:
nodes.add(subj)
nodes.add(obj)
edges.append((subj, obj, pred))
# Save intermediate JSON for inspection
with open("graph_data.json", "w") as f:
json.dump({"nodes": list(nodes), "edges": edges}, f, indent=2)
print("Wrote graph_data.json")
Run it:
python extract_graph.py
Verify: graph_data.json exists and contains nodes (unique entities) and edges (triplets with predicates as labels). Count should match the printed triplet count.
Step 4: Build the pyvis network
Pyvis wraps vis.js. Create a Network, add nodes and edges from the extracted data, and tune physics so the layout stabilizes quickly.
# visualize.py
from pyvis.network import Network
import json
with open("graph_data.json") as f:
data = json.load(f)
net = Network(
height="750px",
width="100%",
bgcolor="#1a1a2e",
font_color="#eaeaea",
directed=True,
notebook=False,
cdn_resources="remote",
)
# Add nodes
for node_id in data["nodes"]:
net.add_node(
node_id,
label=node_id,
title=node_id, # tooltip
shape="dot",
size=18,
color={"background": "#00b4d8", "border": "#90e0ef", "highlight": "#caf0f8"},
)
# Add edges
for src, tgt, label in data["edges"]:
net.add_edge(
src,
tgt,
label=label,
title=label,
arrows="to",
color={"color": "#ffb703", "highlight": "#fb8500"},
font={"size": 11, "color": "#ffb703", "align": "middle"},
smooth={"type": "curvedCW", "roundness": 0.2},
)
# Physics tuned for small-to-medium graphs
net.set_options("""
{
"physics": {
"forceAtlas2Based": {
"gravitationalConstant": -50,
"centralGravity": 0.01,
"springLength": 100,
"springConstant": 0.08
},
"minVelocity": 0.75,
"solver": "forceAtlas2Based"
},
"interaction": {
"hover": true,
"navigationButtons": true,
"keyboard": true
},
"edges": {
"scaling": { "min": 1, "max": 3 }
}
}
""")
output_path = "knowledge_graph.html"
net.write_html(output_path, open_browser=False)
print(f"Wrote {output_path}")
Run it:
python visualize.py
Verify: Open knowledge_graph.html in a browser. You should see a force-directed graph with labeled nodes and directed edges. Hover a node to see its tooltip; drag to rearrange; scroll to zoom.
Step 5: Customize appearance and add metadata
Real graphs need more than raw triplets. You likely want: node types (Person, Organization, Product), edge weights, community coloring, and clickable detail panels. LlamaIndex’s property graph stores Node and Relation objects with properties dicts — use them.
Update extract_graph.py to pull typed nodes and relations:
# extract_graph_enriched.py
import json
from llama_index.core import StorageContext, load_index_from_storage
from llama_index.core.graph_stores import SimplePropertyGraphStore
storage_context = StorageContext.from_defaults(persist_dir="storage_graph")
index = load_index_from_storage(storage_context)
graph_store: SimplePropertyGraphStore = index.property_graph_store
# Nodes with types and properties
nodes = []
for node_id, node_data in graph_store.graph.nodes(data=True):
props = node_data.get("properties", {})
nodes.append({
"id": node_id,
"label": node_id,
"type": props.get("type", "Entity"),
"properties": props,
})
# Edges with relation metadata
edges = []
for src, tgt, rel_data in graph_store.graph.edges(data=True):
rel = rel_data.get("relation", {})
edges.append({
"source": src,
"target": tgt,
"label": rel.get("label", "related_to"),
"properties": rel.get("properties", {}),
})
with open("graph_data_enriched.json", "w") as f:
json.dump({"nodes": nodes, "edges": edges}, f, indent=2)
print(f"Nodes: {len(nodes)}, Edges: {len(edges)}")
Then update visualize.py to consume the enriched data:
# visualize_enriched.py
from pyvis.network import Network
import json
with open("graph_data_enriched.json") as f:
data = json.load(f)
net = Network(
height="750px",
width="100%",
bgcolor="#1a1a2e",
font_color="#eaeaea",
directed=True,
notebook=False,
cdn_resources="remote",
)
# Color map by node type
type_colors = {
"Organization": {"bg": "#00b4d8", "border": "#90e0ef"},
"Person": {"bg": "#ffb703", "border": "#fb8500"},
"Product": {"bg": "#90be6d", "border": "#27ae60"},
"Event": {"bg": "#f94144", "border": "#f3722c"},
"Entity": {"bg": "#6c757d", "border": "#adb5bd"},
}
for node in data["nodes"]:
ntype = node.get("type", "Entity")
colors = type_colors.get(ntype, type_colors["Entity"])
# Build tooltip from properties
tooltip_lines = [f"<b>{node['id']}</b>", f"Type: {ntype}"]
for k, v in node.get("properties", {}).items():
if k != "type":
tooltip_lines.append(f"{k}: {v}")
tooltip = "<br>".join(tooltip_lines)
net.add_node(
node["id"],
label=node["id"],
title=tooltip,
shape="dot",
size=20,
color=colors,
group=ntype,
)
for edge in data["edges"]:
props = edge.get("properties", {})
weight = props.get("weight", 1.0)
tooltip = f"<b>{edge['label']}</b>"
for k, v in props.items():
tooltip += f"<br>{k}: {v}"
net.add_edge(
edge["source"],
edge["target"],
label=edge["label"],
title=tooltip,
arrows="to",
value=weight * 2, # edge thickness
color={"color": "#ced4da", "highlight": "#adb5bd"},
font={"size": 11, "color": "#ced4da", "align": "middle"},
smooth={"type": "curvedCW", "roundness": 0.2},
)
# Legend via groups
net.set_options("""
{
"physics": {
"forceAtlas2Based": {
"gravitationalConstant": -50,
"centralGravity": 0.01,
"springLength": 120,
"springConstant": 0.08
},
"minVelocity": 0.75,
"solver": "forceAtlas2Based"
},
"interaction": {
"hover": true,
"navigationButtons": true,
"keyboard": true,
"tooltipDelay": 150
},
"groups": {
"Organization": { "color": { "background": "#00b4d8", "border": "#90e0ef" } },
"Person": { "color": { "background": "#ffb703", "border": "#fb8500" } },
"Product": { "color": { "background": "#90be6d", "border": "#27ae60" } },
"Event": { "color": { "background": "#f94144", "border": "#f3722c" } },
"Entity": { "color": { "background": "#6c757d", "border": "#adb5bd" } }
},
"legend": { "enabled": true, "position": "right" }
}
""")
output_path = "knowledge_graph_enriched.html"
net.write_html(output_path, open_browser=False)
print(f"Wrote {output_path}")
Run both:
python extract_graph_enriched.py
python visualize_enriched.py
Verify: Open knowledge_graph_enriched.html. The legend on the right shows node types. Hover edges to see relation metadata. Drag nodes — physics should settle in a few seconds.
Step 6: Handle larger graphs and performance
Pyvis renders in the browser via vis.js, which slows down above ~2,000 nodes. For production graphs, apply these strategies before rendering:
- Filter by degree — drop nodes with fewer than N connections.
- Community detection — run NetworkX’s Louvain on the extracted graph, color by community, and optionally collapse communities into meta-nodes.
- Pagination — render subgraphs per document or per entity type, linking between HTML files.
Example filter snippet to add in visualize_enriched.py before adding nodes:
import networkx as nx
# Build a temporary NetworkX graph for analysis
G = nx.DiGraph()
for node in data["nodes"]:
G.add_node(node["id"], **node)
for edge in data["edges"]:
G.add_edge(edge["source"], edge["target"], **edge)
# Keep only nodes with degree >= 2
min_degree = 2
core_nodes = [n for n, d in G.degree() if d >= min_degree]
data["nodes"] = [n for n in data["nodes"] if n["id"] in core_nodes]
data["edges"] = [e for e in data["edges"] if e["source"] in core_nodes and e["target"] in core_nodes]
print(f"After degree filter: {len(data['nodes'])} nodes, {len(data['edges'])} edges")
Verify: Re-run and confirm the node count drops but the graph remains connected.
Step 7: Embed in a web app or notebook
The HTML file is standalone — no server required. To embed in a FastAPI route, Flask template, or Jupyter notebook:
# FastAPI example
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
app = FastAPI()
@app.get("/graph", response_class=HTMLResponse)
async def serve_graph():
with open("knowledge_graph_enriched.html") as f:
return f.read()
In Jupyter, use IPython.display.HTML:
from IPython.display import HTML
HTML(filename="knowledge_graph_enriched.html")
Verify: Visit http://localhost:8000/graph or run the notebook cell — the interactive graph appears inline.
Common pitfalls and fixes
| Symptom | Cause | Fix |
|---|---|---|
| Empty graph / no triplets | LLM failed to extract or max_triplets_per_chunk too low |
Increase max_triplets_per_chunk to 20; check LLM logs; ensure prompt template matches your domain |
| Duplicate nodes with slight spelling differences | Entity resolution not applied | Run a post-processing pass: cluster node labels with fuzzy matching (e.g., rapidfuzz) and merge |
| Browser freezes on load | Too many nodes/edges for vis.js | Apply degree filter; enable physics.stabilization.iterations: 0 and call net.stabilize() in JS after load |
| Edge labels overlap | Long predicate names | Truncate labels to 20 chars in tooltip; keep full text in title |
| Colors not showing in legend | Group names don’t match groups config |
Ensure node.group exactly matches keys in groups option |
Next steps
- Incremental updates: Use
index.insert(doc)then re-extract only changed triplets — diff against the previousgraph_data_enriched.jsonto avoid full rebuilds. - Multi-document graphs: Build one index per corpus, then merge
graph_store.graphobjects withnx.compose()before visualization. - Export for Gephi/GraphML:
nx.write_graphml(G, "graph.graphml")— useful for offline analysis. - Add search: Inject a small JS filter that hides nodes not matching a text input; vis.js supports
network.setData({nodes: filteredNodes, edges: filteredEdges}).
You now have a complete llamaindex knowledge graph visualization pyvis tutorial pipeline: extract → enrich → render → embed. The same pattern scales to production graphs — just add filtering, community detection, and incremental updates at the extraction layer.