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.

How the UI Processes Your HTML
After injecting your HTML, the web UI looks for all SVG elements with [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 a node attribute
  • 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" and data-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-visualizer

Select 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-visualizer

Then restart the server. In the UI, click the active visualizer chip in the toolbar to switch between visualizers.

Tip: the D3 physics simulation does the heavy lifting
Your plugin does not need to calculate node positions. Just describe the shape and size of each node. The web UI reads bounding boxes with 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 typeHow position is readHow position is written
<circle> or <ellipse>From cx, cy, r/rx/rySets cx, cy
<g> (group)From transform="translate(x,y)"Sets transform
Any other element (<rect>, etc.)From x, y, width, heightSets 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>