Gradient Line Graph Editor
This example turns a small relation-graph canvas into a lightweight editor with palette-driven node creation, contextual line creation, and endpoint-colored gradient edges. It also shows how custom line slots can coexist with selection, deletion, canvas settings, and image export.
Building a Palette-Driven Gradient Line Graph Editor
What This Example Builds
This example builds a compact graph editor rather than a read-only relationship view. The starting canvas shows a small workflow-like routing graph with rounded icon nodes, detached text labels, and curved lines whose color transitions from the source node color to the target node color.
Users can select nodes and lines, box-select multiple nodes, drag icon templates from a top-center palette to create new nodes, create new outgoing lines from a node-adjacent toolbar, delete selected nodes, delete selected lines, resize nodes, change canvas wheel and drag behavior from a floating settings panel, and export the current graph as an image. The most important idea is that the example keeps relation-graph’s editing model intact while replacing the visual surface of both nodes and lines.
How the Data Is Organized
The initial graph is declared inline as RGJsonData in MyGraph.tsx. Each node stores id, text, color, and a data.myIcon field that the node slot uses to choose the Lucide icon. Each line stores only id, from, to, and text, so the graph starts from a simple business-friendly shape.
There is very little preprocessing before setJsonData(...). The graph is loaded directly, then a post-load pass iterates over graphInstance.getNodes() and overwrites every node color with a random value from a fixed palette. Runtime-created nodes follow the same structure: the top palette supplies an icon name, a random color is assigned, and the callback adds data.myIcon plus a generated node id.
In a production system, this structure could represent support channels, approval steps, service dependencies, or workflow states. The node data field is the main extension point for carrying icon keys, business type metadata, or editor-only state without changing the core graph schema.
How relation-graph Is Used
The demo is wrapped in RGProvider, then MyGraph uses RGHooks.useGraphInstance() as the central control surface. The graph itself runs in a tree layout with fixed horizontal and vertical gaps, curved lines, left-right junction defaults, a default line width of 3, mouse wheel zoom, and drag-to-move canvas behavior.
Three slots define most of the custom UI. RGSlotOnNode replaces the default node body with an icon card and a title rendered below the node. RGSlotOnLine replaces default edge rendering with CustomLineContent, which still delegates routing and label geometry to generateLinePath(...) and generateLineTextStyle(...) so the custom visuals stay aligned with relation-graph internals. RGSlotOnView adds viewport-fixed UI that does not move with canvas zoom: the top creation palette, the alignment reference line, the resize handle, the node toolbar, the line controller, and the connect controller.
The example also uses relation-graph’s editing APIs directly. setJsonData(...), zoomToFit(), getNodes(), and updateNode(...) initialize and restyle the graph. setEditingNodes(...), toggleEditingNode(...), setEditingLine(...), getNodesInSelectionView(...), and clearChecked() keep selection state synchronized across node clicks, line clicks, rectangle selection, and blank-canvas clicks. startCreatingNodePlot(...) powers drag-created nodes from the top palette, while startCreatingLinePlot(...) powers contextual line creation from the selected node. removeNode(...) and removeLine(...) mutate live graph data, and the shared floating window uses setOptions(...), prepareForImageGeneration(), and restoreAfterImageGeneration() to change canvas behavior and export an image.
Styling is handled through local SCSS rather than extra graph data fields. The stylesheet gives the canvas its layered radial-gradient background, recolors the built-in toolbar, adds white borders and rounded corners to nodes, applies gradient text to line labels, and changes checked lines into a pink-highlighted delete state.
Key Interactions
- Clicking a node selects it for editing. Holding
Shift,Ctrl, orMetaturns the same action into a toggle, so the example supports modifier-assisted multi-selection. - Dragging one of the top icon tiles starts
startCreatingNodePlot(...). Releasing on the canvas inserts a new node near the drop point and immediately makes it the active editing node. - Completing a rectangle selection replaces the editing-node set with the nodes returned by
getNodesInSelectionView(...). - When exactly one node is in editing state,
MyNodeToolbarappears around it. Its side and bottom buttons start outgoing line creation, and the top button removes the node. - Clicking a line promotes it to the active editing line. In that state, the custom line label switches from text to an inline delete button.
RGEditingLineControlleris mounted withtextEditable={false}andpathEditable={false}. In this example it serves as a compact line-endpoint editing aid rather than a full path-shaping or text-editing tool.- The floating helper window includes a manual recolor button, a settings overlay for wheel and drag modes, and an image export action.
Key Code Fragments
This block defines the baseline graph behavior for the editor canvas.
const graphOptions: RGOptions = {
debug: false,
layout: {
layoutName: 'tree',
treeNodeGapH: 200,
treeNodeGapV: 40
},
defaultLineShape: RGLineShape.StandardCurve,
defaultJunctionPoint: RGJunctionPoint.lr,
defaultLineWidth: 3,
wheelEventAction: 'zoom',
dragEventAction: 'move',
};
This callback turns the top palette into a drag-and-drop node template source.
graphInstance.startCreatingNodePlot(e.nativeEvent, {
templateNode: {
text: iconName,
color: randomColor,
data: {
myIcon: iconName
}
},
onCreateNode: (x, y, nodeTemplate) => {
const newNode = {
...nodeTemplate,
id: `N-${graphInstance.generateNewNodeId()}`,
This is the part that persists a newly dropped node into the live graph and immediately hands it to the editing state.
const newNode = {
...nodeTemplate,
id: `N-${graphInstance.generateNewNodeId()}`,
x: x - 20,
y: y - 20
};
graphInstance.addNodes([newNode]);
graphInstance.setEditingNodes([graphInstance.getNodeById(newNode.id)]);
}
});
This callback makes the node-adjacent toolbar create real edges, not just temporary guides.
graphInstance.startCreatingLinePlot(e.nativeEvent, {
template: { ...lineTemplate },
fromNode: node,
onCreateLine: (from, to, finalTemplate) => {
if ('id' in to) {
graphInstance.addLines([{
...finalTemplate,
from: (from as RGNode).id,
to: (to as RGNode).id,
text: 'New Line'
}]);
}
}
});
This part of the line slot keeps relation-graph path computation but derives a custom SVG gradient from the current endpoint colors.
const linePathInfo = useMemo<RGLinePathInfo>(
() => graphInstance.generateLinePath(lineConfig),
[lineConfig]
);
const textStyle = graphInstance.generateLineTextStyle(lineConfig, linePathInfo);
const fromNodeColor = lineConfig.from.color || '#666666';
const toNodeColor = lineConfig.to.color || '#666666';
const linearGradientId = 'gradient-for-' + lineConfig.line.id;
This conditional swaps the normal line label for an inline delete action when the line is checked.
{checked ?
<button
className="cursor-pointer h-8 w-8 bg-pink-400 text-white rounded flex place-items-center justify-center"
onClick={() => {
onMyRemoveIconClick(lineConfig.line);
}}
>
<Trash2Icon size={18} />
</button>
This helper-panel code shows how the example reuses graph APIs for export and runtime canvas settings.
const downloadImage = async () => {
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
Compared with neighboring examples such as create-line-from-node and line-vertex-on-node, this demo is broader. It does not stop at contextual line creation from an existing node; it also lets users create brand-new nodes from a top palette and then continue editing them in the same canvas.
Compared with customize-line-toolbar, the selected-line experience is more embedded in the line itself. The same RGSlotOnLine implementation owns gradient drawing, label rendering, click passthrough, and inline deletion, so selected-line actions do not move into a separate floating toolbar.
Compared with change-line-vertices and gee-node-alignment-guides, the built-in editing aids are supporting pieces inside a fuller authoring surface. Alignment guides, resize handles, the line controller, and the connect controller all appear together with custom node and line rendering, which makes this example an unusually dense reference for lightweight editor composition rather than a single isolated controller demo.
The rare combination is what matters most: palette-driven node creation, contextual node-based line creation, selection-aware inline edge deletion, endpoint-color custom edge rendering, and shared helper UI for recoloring, canvas settings, and image export. That combination makes the example a strong starting point for teams that need a compact graph editor without building a full application shell first.
Where Else This Pattern Applies
This pattern transfers well to tools where users need both a global way to add new elements and contextual tools for existing elements. Examples include support-routing designers, approval-flow builders, service handoff maps, incident response playbooks, and lightweight architecture canvases.
It is also useful when a team wants graph-attached editing controls instead of side panels. The top palette can represent reusable node templates, the node toolbar can expose context-specific actions, and the custom line slot can encode status or ownership visually without giving up relation-graph’s selection model.
Finally, this is a practical reference for building branded editors. The example shows that the visual layer can be heavily customized through slots and local CSS while still relying on relation-graph for layout, selection, geometry, controller overlays, and export preparation.