Skip to content

How to render a diagram ​

Run the canonical GraphIR 2.0 workflow from Coral text to laid-out SVG and standalone HTML.

Before you begin ​

Install the language and render packages with the exact external validator:

sh
npm install @coral-viz/language@0.2.5 @coral-viz/render@0.2.5 \
  @graph-ir/core@0.2.1

Published @graph-ir/core@0.2.1 is pinned exactly by all four direct Coral dependencies. Release verification uses the registry package and one deduplicated installed resolution; local paths, sibling checkouts, and archive substitutions are not release evidence.

Render the graph ​

  1. Parse Coral text with @coral-viz/language and stop on parser errors.
  2. Validate the returned GraphIR 2.0 graph with validateIR from @graph-ir/core@0.2.1.
  3. Map the graph to a LayoutRequest and call computeElkLayout from @coral-viz/render/layout.
  4. Apply the returned absolute positions, dimensions, and edge sections.
  5. Call renderSvg and renderHtml from @coral-viz/render.

The executable source is included directly from render-diagram.mjs:

mjs
import assert from 'node:assert/strict';

import { validateIR } from '@graph-ir/core';
import { parse } from '@coral-viz/language';
import { renderHtml, renderSvg } from '@coral-viz/render';
import { computeElkLayout } from '@coral-viz/render/layout';

const source = `
service "API" as api
database "Orders" as orders
api -> orders as writes
`;

const parsed = parse(source);
assert.equal(parsed.success, true, parsed.errors.map((error) => error.message).join('; '));
assert.equal(parsed.graph.version, '2.0.0');

const validation = validateIR(parsed.graph);
assert.equal(validation.valid, true, validation.errors.map((error) => error.message).join('; '));

// LayoutRequest is deliberately explicit. Applications choose their own size,
// hierarchy, port, pinning, and fallback policies.
const layout = await computeElkLayout({
  id: parsed.graph.id,
  nodes: parsed.graph.nodes.map((node) => ({
    id: node.id,
    width: node.dimensions?.width ?? 160,
    height: node.dimensions?.height ?? 80,
  })),
  edges: parsed.graph.edges.map((edge) => ({
    id: edge.id,
    source: edge.source,
    target: edge.target,
  })),
  defaults: {
    algorithm: 'layered',
    direction: 'RIGHT',
    edgeRouting: 'ORTHOGONAL',
  },
});

const rectangles = new Map(layout.nodes.map((node) => [node.id, node]));
const graph = {
  ...parsed.graph,
  nodes: parsed.graph.nodes.map((node) => {
    const rectangle = rectangles.get(node.id);
    assert.ok(rectangle, `layout omitted ${node.id}`);
    return {
      ...node,
      position: rectangle.absolutePosition,
      dimensions: { width: rectangle.width, height: rectangle.height },
    };
  }),
};
const edgeRoutes = new Map(layout.edges.map((edge) => [edge.id, edge.sections]));

const svg = renderSvg(graph, { edgeRoutes, title: 'Order service' });
const html = renderHtml(graph, { edgeRoutes, title: 'Order service' });
assert.match(svg, /^<svg/);
assert.match(svg, /API/);
assert.match(html, /^<!doctype html>/);
assert.match(html, /<svg/);

// Root rendering consumes geometry. It does not call the layout engine.
const unlaid = renderSvg(parsed.graph, { padding: 0 });
assert.notEqual(svg, unlaid);
assert.equal(
  [...unlaid.matchAll(/<g transform="translate\(0,0\)/g)].length,
  parsed.graph.nodes.length,
);

console.log(`Rendered ${graph.nodes.length} nodes to SVG and standalone HTML.`);

This example is intentionally flat. A host application decides how nested nodes, ports, pinned positions, and missing dimensions map into a layout request.

Check the boundary ​

renderSvg and renderHtml serialize supplied geometry. They don't call a layout engine. Without positions, nodes draw at the origin. Call the layout subpath explicitly as shown above.

The MCP coral_render tool is a separate convenience boundary: it lays out input whose geometry is incomplete, while preserving complete supplied geometry.