Create a Visualizer Plugin
A visualizer plugin handles the visualise_graph action. It receives a Graph object and returns an HTML string — typically an SVG with nodes marked by the [node] attribute and edges marked by [edge]. The Nessie web UI then injects this HTML and runs a D3 force simulation on top of it, so your plugin only needs to describe the initial shape of each node — D3 will animate and position them.
[node] and line[data-source][data-target] attributes. It reads their geometry (bbox, transforms) and uses D3's force simulation to position them. You don't need to set positions.
The Contract
Your visualizer must return an SVG string where:
- Nodes are SVG elements (or
<g>groups) with anodeattribute - All nodes are inside a
<g id="nodes">group - Edges are
<line>elements inside a<g id="edges">group - Each edge line has
data-source="N"anddata-target="M"attributes (integer indices into the nodes list) - The root element is
<svg id="main_view">
Step 1 — Scaffold with the CLI
Inside your Nessie project, use the CLI to scaffold a new plugin:
nessie new my-hex-visualizerSelect visualiser as the plugin type. This creates:
my_plugins/
└── my-hex-visualizer/
├── my_hex_visualizer/
│ ├── __init__.py
│ └── plugin.py
├── setup.py
└── README.md
Step 2 — Write the Handler
Open my_hex_visualizer/plugin.py and write the visualizer logic. Here is a complete working example that renders each node as a hexagon:
import json, math
from nessie_api.models import Action, Graph, plugin
from nessie_api.protocols import Context
def hexagon_points(cx: float, cy: float, r: float) -> str:
"""Return SVG polygon points string for a hexagon."""
pts = []
for i in range(6):
angle = math.pi / 180 * (60 * i - 30)
pts.append(f"{cx + r * math.cos(angle):.1f},{cy + r * math.sin(angle):.1f}")
return " ".join(pts)
def visualise_graph_handler(action: Action, context: Context) -> str:
graph: Graph = action.payload
if not hasattr(graph, "to_dict"):
raise ValueError("Expected a Graph object")
data = graph.to_dict()
nodes = data.get("nodes", [])
edges = data.get("edges", [])
# Index nodes for edge lookup
node_index = {n["id"]: i for i, n in enumerate(nodes)}
# --- Build SVG ---
edge_svg = []
for e in edges:
si = node_index.get(e["source"])
ti = node_index.get(e["target"])
if si is None or ti is None: continue
edge_svg.append(
f'<line edge="" data-source="{si}" data-target="{ti}" '
f'stroke="#475569" stroke-width="1.5" />'
)
node_svg = []
R = 28 # hexagon radius
for node in nodes:
pts = hexagon_points(0, 0, R)
label = node["id"][:16] # truncate long IDs
node_svg.append(f"""
<g node="" data-id="{node['id']}" transform="translate(0,0)">
<polygon points="{pts}"
fill="#1e2130" stroke="#3b82f6" stroke-width="1.5" />
<text x="0" y="4" text-anchor="middle"
font-family="JetBrains Mono,monospace" font-size="9"
fill="#dde1ec">{label}</text>
</g>""")
directed = data.get("type") == "directed"
defs = ""
marker_attr = ""
if directed:
defs = """<defs>
<marker id="arrow" markerWidth="8" markerHeight="6"
refX="8" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#475569"/>
</marker>
</defs>"""
marker_attr = 'marker-end="url(#arrow)"'
edge_svg = [e.replace('stroke-width="1.5"',
f'stroke-width="1.5" {marker_attr}')
for e in edge_svg]
return f"""<svg id="main_view" xmlns="http://www.w3.org/2000/svg"
style="width:100%;height:100%;">
{defs}
<g id="edges">{''.join(edge_svg)}</g>
<g id="nodes">{''.join(node_svg)}</g>
</svg>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
"""
@plugin("My Hex Visualizer", verbose=False)
def my_hex_visualizer_plugin():
return {
"handlers": {"visualise_graph": visualise_graph_handler},
"requires": [],
"setup_requires": {},
}
Step 3 — Register in pyproject.toml
[project.entry-points."nessie_plugins"]
my_hex_visualizer = "my_hex_visualizer:my_hex_visualizer_plugin"
Step 4 — Install and Test
nessie install my-hex-visualizerThen restart the server. In the UI, click the active visualizer chip in the toolbar to switch between visualizers.
getBBox() and handles all animation. The force simulation respects your node dimensions when computing collision radii.
Node Rendering Rules
The web UI supports three types of node root elements:
| Element type | How position is read | How position is written |
|---|---|---|
<circle> or <ellipse> | From cx, cy, r/rx/ry | Sets cx, cy |
<g> (group) | From transform="translate(x,y)" | Sets transform |
Any other element (<rect>, etc.) | From x, y, width, height | Sets x, y |
Complete Reference
Your SVG output must follow this structure exactly:
<svg id="main_view" xmlns="http://www.w3.org/2000/svg"
style="width:100%;height:100%;">
<!-- Optional defs: markers, gradients, etc -->
<defs>...</defs>
<!-- Edges layer (rendered below nodes) -->
<g id="edges">
<line edge=""
data-source="0" <!-- integer index of source node -->
data-target="1" <!-- integer index of target node -->
stroke="#475569" stroke-width="1.5"
marker-end="url(#arrow)" /> <!-- optional arrowhead -->
</g>
<!-- Nodes layer -->
<g id="nodes">
<g node="" data-id="nodeId" transform="translate(0,0)">
<!-- your shape here (rect, circle, polygon, path...) -->
<rect x="0" y="0" width="120" height="40" .../>
<text>Node Label</text>
</g>
</g>
</svg>
<!-- D3 must be loaded for physics to work -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>