Visualization

verve.viz computes layouts and charts in Zig and renders them as SVG on the server. Pages are complete with JavaScript off; one island makes a graph interactive when you want it.

See it live: the viz example renders every chart type below plus the interactive graph; multi-instance graphs shows two independent interactive graphs on one page; canvas render and two canvas graphs show the canvas2d path for large graphs.

Graphs

const nodes = [_]verve.viz.GraphNode{ .{ .id = "core", .label = "core" },};
const edges = [_]verve.viz.GraphEdge{ .{ .from = "core", .to = "ui" },};
const g = verve.viz.Graph{ .nodes = &nodes, .edges = &edges, .layout = .force };

const svg = verve.viz.renderGraph(ctx, g, .{
    .width = 640, .height = 420,
    .node_color = "#1f6feb", .edge_color = "#30363d", .label_color = "#f5f5f5",
});

Layouts: .force (converged force-directed), .tree, .radial, .dag (layered, with crossing-minimization sweeps and virtual-node edge routing). GraphOpts.edge_routing: .straight, .curved (Catmull-Rom), .orthogonal (Manhattan, rounded corners). dag_crossing_iterations = 0 turns the sweep off if you want to show why it exists.

graphPositions(ctx, g, opts) returns the computed positions — feed them to an island's props so client-side interaction starts exactly where SSR drew.

Charts

All server-side SVG, one call each:

barChart, stackedBarChart, groupedBarChart, lineChart, areaChart, scatterChart, pieChart (with inner_ratio for donuts), candlestickChart, boxPlotChart, heatmapChart, radarChart, violinChart, sankeyChart, treemapChart, chordChart.

const data = [_]verve.viz.Datum{ .{ .label = "Jan", .value = 12 },};
const chart = verve.viz.barChart(ctx, &data, .{ .width = 480, .height = 300 });

Scales (LinearScale, BandScale, LogScale, TimeScale) and Axis are public if you want custom scenes.

Live updates — wire deltas

Stream graph mutations to connected clients instead of re-sending the graph:

const ops = try verve.viz.diffGraphs(alloc, old_nodes, old_edges, new_nodes, new_edges);
const frame = try verve.viz.writeDeltaJson(&buf, seq, ops);
// publish on a push channel; clients apply ops in seq order

The shipped VizGraphInteractive island consumes these over SSE: a server publisher thread ticks the model and broadcasts {"seq":N,"ops":[…]} frames; the chunk applies deltas, resyncing via a pull snapshot when a gap is detected. Zoom, selection, and collapse all survive the stream.

Interactive graphs

renderGraphInteractive(ctx, g, opts) emits SVG annotated for the interaction layer — wrap it in the VizGraphInteractive island and you get wheel-zoom, drag-to-pan, node dragging with pointer capture, hover tooltips, click-select, and double-click subtree collapse. The element set is fixed at SSR; the chunk only mutates transforms.

Dragging a node live re-routes its incident edges: under .curved or .orthogonal edge_routing the chunk rebuilds each affected edge's path d from the live endpoints (not just the two <line> coordinates), so a Catmull-Rom or Manhattan run tracks the node instead of snapping back to a straight line.

Double-click subtree collapse is multi-parent-aware: a node hides only when every path to it runs through a collapsed ancestor — a shared node with one collapsed parent and one visible parent stays visible. Collapsed roots always stay visible as the re-expand handle.

Multiple graphs per page

VizGraphInteractive and VizGraphCanvas are both multi-instance: each maintains a small fixed pool of instance slots (2 concurrent islands per page) keyed by vid — the framework-assigned instance id each island's SSR marker carries. hydrate allocates a slot for its root_id; every bridge-dispatched event (pointer, wheel, drag, push-delta) re-selects the instance for that vid before running, so two independent graphs — different data, different zoom/pan/selection/ collapse state — coexist on one page without interfering. See multi-instance graphs (two VizGraphInteractive SVG graphs) and two canvas graphs (two VizGraphCanvas instances).

A single instance on a page still works exactly as before — it just takes one of the two slots.

Canvas render path

For graphs too large to render as one SVG element per node/edge, VizGraphCanvas draws to a <canvas> with canvas2d instead: the chunk synthesizes (or receives) the graph, then issues one batched draw call per frame rather than mutating a DOM element per node. It supports the same interaction model as the SVG path — pan (drag), zoom (wheel), hover and click-select via hit-testing against the current view transform — but no per-element pointer/DOM events. See canvas render for the ~1500-node procedural demo graph.

Next: Animation.