n4nAI

Visualize a LlamaIndex knowledge graph with pyvis

A step-by-step tutorial for extracting a LlamaIndex knowledge graph and rendering it interactively with pyvis, including code you can run today.

n4n Team3 min read759 words

Audio narration

Coming soon — every post will get a voice note here.

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:

  1. Filter by degree — drop nodes with fewer than N connections.
  2. Community detection — run NetworkX’s Louvain on the extracted graph, color by community, and optionally collapse communities into meta-nodes.
  3. 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 previous graph_data_enriched.json to avoid full rebuilds.
  • Multi-document graphs: Build one index per corpus, then merge graph_store.graph objects with nx.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.

Tagsllamaindexknowledge-graphvisualizationpyvis

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All llamaindex knowledge graphs & multi-doc indexes posts →