JavaScript is required

Metric Rollup Graph Editor

This example builds a compact graph editor for value and summary nodes where users can create nodes and links, edit numbers inline, and recalculate rollup totals from incoming relationships. It combines slot-based node rendering, guided authoring tools, optional tree relayout, and a shared settings or export window on one dotted full-screen canvas.

Building a Graph-Based Metric Rollup Editor

What This Example Builds

This example builds a compact relation-graph editor for metric or cost rollups. The screen starts with a dotted full-screen canvas, a purple summary node, two blue value nodes, an orange authoring toolbar, and a floating helper window above the graph.

Users can drag new value or summary nodes onto the canvas, click a line tool to connect inputs into a summary node, double-click values and names to edit them, delete the currently selected node or line, switch to a tree layout, and open a shared settings or export panel. The main point is that the graph is not just a diagram: the incoming links define how summary totals are recalculated.

How the Data Is Organized

The initial dataset is declared inline as one RGJsonData object with nodes and lines. Each starter node already carries fixed x and y coordinates plus custom metadata in data, especially myNodeType and numberValue. That makes the graph both visual and semantic: node type controls behavior, while numberValue is the field used for aggregation.

Before setJsonData(...) runs, the code normalizes every starter line by guaranteeing an id, switching the line shape to Curve7, and setting explicit junction points. Runtime creation uses a second layer of data definitions: the toolbar contains reusable node templates for value-node and sum-node, plus one line template for summarization links. In a real system, the same structure could represent KPI rollups, budget breakdowns, project cost models, staffing totals, or any dependency graph where upstream numeric inputs feed derived summaries.

How relation-graph Is Used

RGProvider wraps the demo so relation-graph hooks can resolve the active graph instance across the graph component, the toolbar, and the floating settings panel. In MyGraph, RGHooks.useGraphInstance() loads the starter dataset with setJsonData(...), recalculates summary nodes, then recenters and fits the scene with moveToCenter() and zoomToFit().

The graph options keep the base scene simple and editor-oriented. The graph starts in fixed layout, uses border-based junction points, draws lines with StandardCurve, and moves the built-in toolbar to the right. A separate toolbar action later replaces that with a top-down tree layout through updateOptions(...) and doLayout().

Two relation-graph slots define the custom UI. RGSlotOnNode replaces the default node body so value nodes render an inline numeric editor, summary nodes render the computed total, and every node shows an editable name badge below the card. RGSlotOnView injects the orange SummarizerToolbar directly into the graph viewport instead of relying on an external page sidebar.

The authoring flow uses graph-instance APIs rather than external stateful layout code. The toolbar starts node creation with startCreatingNodePlot(...), creates ids with generateNewUUID(), inserts new items with addNodes(...), starts line creation with startCreatingLinePlot(...), validates the target node before addLines(...), and removes the current selection with removeNode(...) or removeLink(...). RGHooks.useCreatingNode() and RGHooks.useCreatingLine() expose creation mode so the toolbar can visibly highlight the active tool.

The floating DraggableWindow is a shared helper rather than logic unique to this example, but it still matters to the finished workflow. Its settings panel reads graph store state with RGHooks.useGraphStore(), changes wheel and drag behavior through setOptions(...), and exports the current canvas by calling prepareForImageGeneration(), domToImageByModernScreenshot(...), and restoreAfterImageGeneration(). Local SCSS finishes the editor look with a dot-grid background, an externally positioned node-name badge, and a stronger checked-state style for line labels.

Key Interactions

Dragging either toolbar template starts relation-graph’s node-plot flow and inserts a new typed node where the drop finishes. New nodes inherit the template color, shape, and myNodeType, so the same toolbar controls both editable value nodes and computed summary nodes.

Clicking the line tool starts guided line creation. The code blocks value nodes from being used as the target, so new links are treated as inputs into a summary node rather than arbitrary connections. After a valid link is added, the example reruns the aggregation logic immediately.

Double-clicking the number inside a value node opens an inline input. Committing the edit writes the new numberValue back with updateNodeData(...) and recalculates all summary nodes. Double-clicking the name badge uses the same inline editor pattern, but it updates node.text through updateNode(...) instead of recalculating values.

Node and line deletion is selection-aware. Clicking a node or line stores it as the current selection, the matching delete button appears in the toolbar, and clicking the canvas background clears both the local selection state and relation-graph’s checked state. Separate from authoring, the floating helper window can be dragged, toggled into a settings panel, and used to export the current graph as an image.

Key Code Fragments

This fragment shows that the starter graph is fixed-positioned and that node metadata already carries the type and numeric value used later by the editor.

const myJsonData: RGJsonData = {
  rootId: 'my-root',
  nodes: [
    { id: 'my-root', text: '成本', color: 'rgba(214,103,239,0.59)', x: -50, y: -2, data: { myNodeType: 'sum-node', numberValue: '510000' }},
    { id: 'newNode-1', text: '材料成本', color: '#5da0f8', x: -167, y: -176, data: { myNodeType: 'value-node', numberValue: '10000' }},
    { id: 'newNode-3', text: '人力成本', color: '#5da0f8', x: 29, y: -176, data: { myNodeType: 'value-node', numberValue: '500000' }}
  ],
  lines: [

This fragment proves that summary values are derived from graph topology by reading incoming nodes and writing the computed total back into node data.

const incomingNodes = graphInstance.getNodeIncomingNodes(node);

let sum = 0;
incomingNodes.forEach(inNode => {
  let val = 0;
  if (inNode.data?.myNodeType === 'sum-node') {
    val = analyzeNodesForSum(inNode, newPath);
  } else {
    val = parseFloat(inNode.data?.numberValue || '0');
  }
  sum += isNaN(val) ? 0 : val;
});
graphInstance.updateNodeData(node, { numberValue: sum.toFixed(3) });

This fragment shows how RGSlotOnNode turns the node body into an editable property surface instead of a plain label.

{node.data?.myNodeType === 'value-node' ? (
  <MyEditableProperty
    dataType="number"
    currentValue={node.data.numberValue}
    onChange={(val) => {
      graphInstance.updateNodeData(node, { numberValue: String(val) });
      reCalcSumNodeValue();
    }}
  />
) : (
  <div className="font-bold px-2">{node.data?.numberValue}</div>
)}

This fragment shows the guided line-authoring rule: the toolbar starts relation-graph’s create-line flow, rejects invalid targets, and recalculates after insertion.

graphInstance.startCreatingLinePlot(e.nativeEvent, {
  template: { ...lineTemplate },
  onCreateLine: (from, to, finalTemplate) => {
    if ('id' in to) {
      if (to.data?.myNodeType === 'value-node') {
        return alert('Value node cannot be a summary target!');
      }
      graphInstance.addLines([{ ...finalTemplate, from: (from as RGNode).id, to: (to as RGNode).id, text: 'Summarizes' }]);
      onLineCreated();
    }
  }
});

This fragment shows that the floating helper window exposes runtime canvas behavior changes and image export through relation-graph instance APIs.

const canvasDom = await graphInstance.prepareForImageGeneration();
let graphBackgroundColor = graphInstance.getOptions().backgroundColor;
if (!graphBackgroundColor || graphBackgroundColor === 'transparent') {
  graphBackgroundColor = '#ffffff';
}
const imageBlob = await domToImageByModernScreenshot(canvasDom, {
  backgroundColor: graphBackgroundColor
});
await graphInstance.restoreAfterImageGeneration();

What Makes This Example Distinct

The comparison data shows that this example is not distinctive merely because it uses slots, fixed coordinates, or a floating helper window. Nearby examples such as drag-to-create-nodes-with-preset-styles, create-object-from-menu, use-d3-layout, and canvas-selection already overlap with parts of that implementation space. What is rarer here is the combination of editor features around a rollup model.

Against drag-to-create-nodes-with-preset-styles, this is not a generic style-authoring canvas. The created nodes have semantic types, the accepted connections are constrained, and successful edits feed directly into recomputed totals. Against create-object-from-menu, the workflow is persistent and guided rather than menu-driven: node creation, line creation, rename, numeric editing, and deletion stay visible in one overlay interface. Against use-d3-layout and canvas-selection, the emphasis shifts away from external layout or selection mechanics and toward topology-driven calculation.

The prepared rarity and comparison records point to the same conclusion: this example is a strong starting point when the requirement is a lightweight graph editor whose edges mean aggregation. Its most unusual combination is template-driven node creation, validated line authoring, inline slot-based property editing, recursive summary recomputation, selection-aware deletion, optional relayout, and shared canvas settings or export utilities on one screen.

Where Else This Pattern Applies

This pattern transfers well to KPI rollup designers, budget and cost breakdown planners, revenue decomposition tools, and operational scorecard editors. In those scenarios, users need to model how upstream values feed one or more summary nodes and immediately verify the resulting totals.

It also fits dependency-based planning tools where the graph itself is the calculation model, such as staffing estimates, component cost allocation, or layered target planning. The same slot-based editing pattern can be extended to more fields per node, stricter validation rules, or domain-specific node templates without changing the overall authoring structure.