Character Relationship Graph Operations
This example loads a mocked character relationship network into a force-directed relation-graph scene and renders each person as an avatar circle with labels below. Clicking a node opens a radial action menu for renaming, copying, adding children, creating links, and opening a fixed detail card for live node property edits.
Character Relationship Graph with Node-Centered Operations
What This Example Builds
This example builds a force-directed character relationship graph that behaves like a lightweight editing workspace rather than a read-only network view. The canvas starts with a prepared cast-style relationship dataset, renders each person as a circular avatar node, and overlays a node-centered radial menu plus a fixed side editor for the selected node.
Users can click a node to open contextual actions, rename the node inline, copy it, add new child nodes, start interactive link creation, or open a detail card that edits colors and avatar URL live. The most important idea is that the overlay UI is not detached from the graph state: it is anchored to the selected node and drives real graph mutations through relation-graph instance APIs.
How the Data Is Organized
The dataset is mocked as asynchronous RGJsonData in mock-data-api.ts. It defines rootId: "N13", a list of person nodes, and labeled relationship lines such as friend, relative, lover, collusion, corruption, and report. Each node carries id, text, color, borderColor, and a data object with fields such as icon, sexType, and isGoodMan. Each line uses from, to, text, color, fontColor, and a data.type value.
There is no preprocessing before the graph is loaded. fetchJsonData() resolves the inline object after a short timeout, and initializeGraph() passes that result directly into setJsonData(...). In a real application, the same structure could represent people, customers, departments, devices, fraud entities, or case participants, while data.icon and the relationship labels could be replaced with domain-specific metadata.
The example also benefits from repeated links between the same endpoints, which is why multiLineDistance matters. For example, the mock data includes multiple relationships between the same characters, so the graph needs visible spacing between parallel lines instead of collapsing them into a single unreadable path.
How relation-graph Is Used
The demo is wrapped in RGProvider, then MyGraph reads the live graph instance through RGHooks.useGraphInstance(). The graph options configure a force layout with maxLayoutTimes: 50, straight lines, circular default nodes, line text drawn on path, multiLineDistance: 20, a 2px node border, and a fallback node color of #e85f84.
The loading flow uses relation-graph instance APIs directly. After the mock request resolves, the component calls loading(), setJsonData(...), clearLoading(), moveToCenter(), and zoomToFit(). Two seconds later it programmatically opens node N3, which makes the editing affordance visible without requiring the user to discover it first.
The rendering layer relies on two slot APIs. RGSlotOnNode replaces the default node body with an avatar circle and a label positioned below it. RGSlotOnView renders the floating radial menu and the fixed side card above the canvas, which lets the example combine graph-native layout with custom React UI overlays.
The mutation workflow also stays inside relation-graph. The menu uses generateNewNodeId(), addNodes(...), addLines(...), and startAutoLayout() to copy a node or append random children. It uses updateNode(...) and updateNodeData(...) to push live edits back into the selected node, and startCreatingLinePlot(...) to enter interactive edge authoring mode from the current node.
The shared DraggableWindow component adds a secondary workspace layer around the graph. It uses RGHooks.useGraphStore() to read current interaction modes and setOptions(...) to switch wheel behavior and canvas drag behavior at runtime. It also demonstrates prepareForImageGeneration(), getOptions(), and restoreAfterImageGeneration() to export a screenshot of the graph canvas.
Styling is heavily customized through local SCSS. The canvas gets a tiled background image, checked nodes receive a color halo and a label pill, checked lines invert their label colors, expand buttons are recolored blue, and the radial menu animates into place with custom SVG quadrants and a text input placed below the menu ring.
Key Interactions
Clicking a node opens the radial menu and marks that node as the current editing target. The menu is not fixed to the page; it is repositioned whenever zooming, node dragging, canvas dragging, or canvas drag end events fire, so it continues to track the selected node in view coordinates.
Clicking empty canvas clears relation-graph’s checked state and dismisses both overlays. That reset behavior matters because the example can otherwise leave a menu or info card floating after the user shifts attention away from the selected node.
The four radial actions drive different runtime behaviors. The info action toggles the fixed detail card. The copy action duplicates the current node, including its color and data payload. The add-children action creates between one and eight new connected nodes. The link action enters relation-graph’s interactive line-plot mode and creates a new labeled edge when the user finishes on another node.
There are two live text-edit paths. The input under the radial menu writes directly into the selected node’s text field as the user types, while the side card edits text, fill color, border color, and avatar URL through updateNode(...) and updateNodeData(...).
The draggable helper window adds graph-wide controls that are separate from node editing. Users can move or minimize the window, open a settings panel, switch wheel interaction between scroll, zoom, and none, switch drag behavior between selection, move, and none, and export the current graph as an image.
Key Code Fragments
This configuration establishes the force layout and the visual defaults that make the avatar network readable.
const graphOptions: RGOptions = {
debug: false,
defaultLineShape: RGLineShape.StandardStraight,
defaultNodeShape: RGNodeShape.circle,
defaultLineTextOnPath: true,
multiLineDistance: 20,
layout: {
layoutName: 'force',
maxLayoutTimes: 50
},
defaultNodeBorderWidth: 2,
defaultNodeColor: '#e85f84'
};
This initialization path shows that the example loads mocked RGJsonData directly into relation-graph and then focuses the graph viewport.
const initializeGraph = async () => {
const myJsonData: RGJsonData = await fetchJsonData();
graphInstance.loading();
await graphInstance.setJsonData(myJsonData);
graphInstance.clearLoading();
graphInstance.moveToCenter();
graphInstance.zoomToFit();
};
This function proves the menu is anchored in graph space, not just placed near the last click position.
const updateNodeMenuPosition = () => {
if (showNodeMenu && currentNode) {
const rgNode = graphInstance.getNodeById(currentNode.id);
if (rgNode) {
const viewCoordinate = graphInstance.getViewXyByCanvasXy({
x: rgNode.x + rgNode.el_W / 2,
y: rgNode.y + rgNode.el_H / 2
});
setNodeMenuPanel({
x: viewCoordinate.x - nodeMenuPanel.width / 2,
y: viewCoordinate.y - nodeMenuPanel.height / 2,
width: nodeMenuPanel.width,
height: nodeMenuPanel.height
});
}
}
};
This mutation block shows how the menu grows the graph in place and immediately restarts the force layout.
const randomChildrenCount = Math.ceil(Math.random() * 8);
const newNodes: JsonNode[] = [];
const newLines: JsonLine[] = [];
for (let i = 0; i < randomChildrenCount; i++) {
const newNodeId = graphInstance.generateNewNodeId();
newNodes.push({
id: newNodeId,
text: 'New Node',
x: currentNode.x + 200,
y: currentNode.y + (Math.random() * 200 - 100)
});
newLines.push({ id: 'line-to-' + newNodeId, from: currentNode.id, to: newNodeId });
}
This slot composition is the core UI pattern: custom node rendering plus view-level overlays driven by selection state.
<RGSlotOnNode>
{({ node }: RGNodeSlotProps) => (
<div className="w-12 h-12 flex place-items-center justify-center">
<div className="my-node-avatar" style={{ backgroundImage: `url(${node.data?.icon})` }} />
<div className="my-node-name absolute transform translate-y-[35px]">{node.text}</div>
</div>
)}
</RGSlotOnNode>
<RGSlotOnView>
{showNodeMenu && currentNode && (
<MyNodeMenus
What Makes This Example Distinct
Compared with nearby examples such as effect-and-control, this demo shifts the center of gravity from global graph controls to node-local operations. The reusable lesson here is not viewport management or global tuning; it is how to make a selected node the launch point for renaming, copying, graph growth, edge creation, and live property editing.
Compared with custom-node-quick-actions, it goes beyond overlay placement and turns the overlay into a real editing surface. The radial menu is tied to concrete mutations, and the fixed side card extends that same selection into a richer property editor for avatar URL and colors.
Compared with amount-summarizer, it is less of a full canvas authoring tool and more of an in-place editor for an already loaded network. That makes it a strong starting point for teams that already have relationship data and need contextual editing on top of it rather than a blank-canvas modeling workflow.
The comparison data also supports a rarer feature combination: avatar-based relationship presentation, force-layout relayout after edits, a node-following radial action menu, live node property editing, and interactive edge authoring in one example. That combination makes this demo more useful than a menu-only or style-only reference when the requirement is to attach structural graph operations directly to a selected node.
Where Else This Pattern Applies
This pattern transfers well to customer relationship exploration tools where analysts need to inspect an entity, adjust display properties, and add new links or related entities without leaving the graph canvas.
It also fits investigation and case-management graphs where a selected suspect, account, device, or document needs a local action menu for linking evidence, cloning nodes, or appending newly discovered related entities.
Another good extension is organizational or project dependency mapping. A selected team, service, or work item could open a node-centered action ring for adding dependents, editing metadata, or starting a new connection, while a side card handles richer property updates.