Skip to content
D
Documentation

Extend rendered geometry

how-to
5 min readUpdated

Use a geometry extension when the shipped figures, arrowheads or line styles cannot express your notation. Register the geometry by name, then use that name in the specs you pass to your mounted diagram. The renderer owns the geometry as nodes move; your extension supplies an outline, a path or the SVG elements that paint it.

Start with the built-ins: figures include diamond, cylinder, document and actor; markers include open-arrow, crow-foot and hollow-diamond. For routing and corner treatment, see Route and label edges. Go below the component's props only to define geometry the renderer does not already provide.

1. Register a silhouette, pipe and marker

This example draws a built-in rectangle connected to a custom chevron by a two-stroke pipe, then connects the chevron to a sink with a feather marker. The pipe follows the routed path rather than a fixed picture.

Use registerPathShape for an SVG-path silhouette. It derives boundary points and port anchors from the outline and reuses the outline for the body, selection and shadow. Use registerShape only when you need to implement the full ShapeDefinition contract yourself: outline, boundaryPoint and portAnchor, with an optional innerRect for the label.

registerLinkTemplate receives the current routed points, pathData and selection state. Its output replaces the visible edge rendering; the renderer retains the link wrapper and hit area. registerMarker defines an endpoint glyph. Put the supplied transform on its root element and declare tipOffset so its visual tip meets the endpoint.

Create this shared file in your browser application. Its nodes use the library's NodeSpec type and its edges use EdgeSpec.

ts
import {
  registerPathShape,
  registerLinkTemplate,
  registerMarker,
  type NodeSpec,
  type EdgeSpec,
} from '@grafloria/renderer';

export function registerGeometry(): void {
  registerPathShape(
    'notation-chevron',
    'M2,4 L14,4 L22,12 L14,20 L2,20 L10,12 Z',
    {
      viewBox: { x: 0, y: 0, w: 24, h: 24 },
      innerRect: (w, h) => ({ x: w * 0.42, y: h * 0.35, w: w * 0.2, h: h * 0.3 }),
    },
  );

  registerLinkTemplate('notation-pipe', (ctx) => {
    const stroke = ctx.selected ? '#2563eb' : '#0ea5e9';
    return [
      {
        type: 'path',
        props: {
          d: ctx.pathData, fill: 'none', stroke,
          'stroke-width': 10, 'stroke-opacity': 0.35,
          'stroke-linecap': 'round',
        },
      },
      {
        type: 'path',
        props: { d: ctx.pathData, fill: 'none', stroke, 'stroke-width': 2.5 },
      },
    ];
  });

  registerMarker('notation-feather', {
    tipOffset: (style) => style.size,
    render: (ctx) => ({
      type: 'path',
      props: {
        d: `M0,0 L${ctx.size},0 M${ctx.size * 0.4},-4 L${ctx.size},0 L${ctx.size * 0.4},4`,
        stroke: ctx.color, fill: 'none', 'stroke-width': 1.5,
        transform: ctx.transform,
      },
    }),
  });
}

export const nodes: NodeSpec[] = [
  {
    id: 'source', position: { x: 40, y: 80 },
    size: { width: 120, height: 64 }, label: 'Source',
    shape: { type: 'rect', fill: '#dbeafe', stroke: '#2563eb' },
  },
  {
    id: 'gate', position: { x: 260, y: 160 },
    size: { width: 140, height: 100 }, label: 'Go',
    shape: { type: 'notation-chevron', fill: '#dbeafe', stroke: '#2563eb' },
  },
  {
    id: 'sink', position: { x: 500, y: 80 },
    size: { width: 120, height: 64 }, label: 'Sink',
    shape: { type: 'rect', fill: '#dbeafe', stroke: '#2563eb' },
  },
];

export const edges: EdgeSpec[] = [
  {
    id: 'pipe', source: 'source', target: 'gate', type: 'smooth',
    style: { template: 'notation-pipe' },
  },
  {
    id: 'feather', source: 'gate', target: 'sink',
    style: { arrowHead: { type: 'notation-feather', size: 14, filled: false } },
  },
];

The registration function returns no instance. The next step mounts real nodes and edges that consume all three names.

2. Mount the diagram in your framework

Install the shared packages and the binding you use:

bash
npm install @grafloria/element @grafloria/engine @grafloria/renderer
# Add one binding for a framework application:
npm install @grafloria/react
npm install @grafloria/vue
npm install @grafloria/angular
npm install @grafloria/qwik

This task adds geometry registration to the existing mounting pattern; see Edit nodes for the component and instance entry points.

The browser entry point calls render after registration.

ts
import { render } from '@grafloria/element';
import { nodes, edges, registerGeometry } from './geometry';

registerGeometry();
const wrapper = document.createElement('section');
const host = document.createElement('div');
host.style.height = '400px';
const remove = document.createElement('button');
remove.textContent = 'Remove diagram';
wrapper.append(host, remove);
document.body.append(wrapper);
const instance = render({ nodes, edges }, host);
remove.addEventListener('click', () => {
  instance.dispose();
  wrapper.remove();
}, { once: true });

For React, register before rendering GrafloriaFlow; see Route and label edges for the default-spec mounting pattern.

tsx
import { GrafloriaFlow } from '@grafloria/react';
import { nodes, edges, registerGeometry } from './geometry';

registerGeometry();

export default function GeometryDiagram() {
  return <div style={{ height: '400px' }}>
    <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} />
  </div>;
}

For Vue, register in the setup script before GrafloriaFlow mounts; see How Grafloria works for default-spec ownership.

vue
<script setup lang="ts">
import { GrafloriaFlow } from '@grafloria/vue';
import { nodes, edges, registerGeometry } from './geometry';

registerGeometry();
</script>

<template>
  <div style="height:400px">
    <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" />
  </div>
</template>

For Angular, register in the constructor before DiagramCanvasComponent paints; see How Grafloria works for the two-way bindings.

ts
import { Component } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import type { NodeSpec, EdgeSpec } from '@grafloria/renderer';
import { nodes, edges, registerGeometry } from './geometry';

@Component({
  selector: 'app-geometry-diagram',
  standalone: true,
  imports: [DiagramCanvasComponent],
  template: `
    <grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
      style="display:block; height:400px" />
  `,
})
export class GeometryDiagramComponent {
  nodes: NodeSpec[] = nodes;
  edges: EdgeSpec[] = edges;

  constructor() {
    registerGeometry();
  }
}

For Qwik, register in GrafloriaFlow's browser initialization callback and repaint its live DiagramInstance; see Route and label edges for the default-spec mounting pattern.

tsx
import { component$, $ } from '@builder.io/qwik';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/qwik';
import { nodes, edges, registerGeometry } from './geometry';

export default component$(() => (
  <div style={{ height: '400px' }}>
    <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges}
      onInit$={$((instance: DiagramInstance) => {
        registerGeometry();
        instance.renderNow();
      })} />
  </div>
));

The chevron labelled Go connects to Source through the blue two-stroke pipe; its thinner outgoing line ends with the feather marker at Sink.

You see Source, the chevron labelled Go, and Sink. Drag the chevron to change both routes; the pipe repaints from the routed pathData, and the feather receives the endpoint's new transform. Select the pipe to switch its strokes to the selection blue.

Register before the first paint in JavaScript, React, Vue and Angular. In Qwik, register from the browser's onInit$ callback and call renderNow() to repaint with the definitions; do not rely on a server-side registration surviving resumption. The specs contain names, not the template functions.

See the live Shapes, Custom edges and Edge markers demos for the individual extensions.

Choose the remaining geometry seams

Keep the stages separate: an anchor chooses one endpoint, a connection-point strategy chooses both endpoints, a router computes the polyline, and a connector turns that polyline into an SVG path. An edge template replaces the visible rendering after that path has been computed.

ExtensionSelect it withInput and result
registerAnchormetadata.sourceAnchor or metadata.targetAnchor on the edgeReads this end, the opposite end, the default point and arguments; returns { point, side? }. Use the world-space node rectangle for a notation-specific attachment.
registerConnectionPointEdge metadata.connectionPoint, or the renderer's connectionPoint defaultReads both ends and their defaults; returns { start, end, sourceDirection?, targetDirection? }, or null to defer to the default pipeline. The context supplies a shape-boundary solver.
registerConnectorEdge connectorReads the routed world-space points, style and resolved corner radius; returns a complete SVG path string.
registerLabelTemplateA live link label's templateReads the label, link, world-space anchor, rotation and theme; returns SVG elements or a foreignObject, or null to suppress the label.
registerPortLayoutPort layout.strategy, or a group's layout under node metadata.portGroupsReads node-local width, height, side, rank, count and shape type, plus layout arguments; returns node-local { x, y }.

For floating attachment, try the shipped smart connection-point strategy before implementing a two-ended strategy. For connectors, try straight, rounded, smooth or bezier. For port layouts, the shipped choices are shape, absolute, line, sideLinear, ellipse and ellipseSpread; shape preserves the silhouette's own anchors.

To select a label template during setup, obtain the live link through instance.getModel().getLink(id) and call its addLabel() with text, position or slot, and template. The top-level edge label is the text convenience field, not a label-template selector. Repaint through the instance after setup mutations. For user-facing edits that need undo, use Commands and history.

Options that matter

The shape options belong to PathShapeOptions.

OptionTypeDefaultWhat it does
viewBox{ x: number; y: number; w: number; h: number }{ x: 0, y: 0, w: 1, h: 1 }Scales a static path's reference box into the node box. Set it to the art box you authored.
sampleStepsnumber24Sets curve subdivision when sampling the outline.
portAnchorShapeDefinition['portAnchor']Derived from the pathSupplies exact node-local port anchors instead of sampled anchors.
boundaryPointShapeDefinition['boundaryPoint']Derived from the pathSupplies exact world-space floating attachments instead of sampled boundaries.
innerRectShapeDefinition['innerRect']Padded bounding boxGives the label a box inside a slanted or concave silhouette.

Pitfalls and registration lifetime

  • An edge template owns the visible edge, including any markers and labels you want it to show. Setting arrowHead on the pipe edge does not add the built-in marker rendering to its output. The example puts the feather on a separate, non-templated edge.
  • Use a distinct custom connector name. The built-in connector names use internal rendering branches; registering one of those names does not replace that branch.
  • Global registration functions share their names across diagrams. Prefix your notation's names to avoid replacing another extension. Shape, edge-template, label-template, marker and port-layout registration functions return void; anchor, connection-point and connector registration functions return a disposer that restores the previous definition. Run disposers when the owning extension unmounts, not immediately after registration.
  • For shape, marker, template and link-pipeline definitions private to one diagram, use the corresponding methods on instance.registry. Those methods return restoring disposers and resolve local definitions before global ones. Port layouts use the global port-layout registry.

Was this page helpful?