Graph Hand-Drawn Style Switcher
This example keeps a small centered hierarchy fixed and switches it between plain and hand-drawn presentation modes at runtime. It combines custom node-slot wrappers, selectable border and texture variants, SVG filter styling on built-in graph surfaces, graph-wide node and line updates, a minimap, and shared canvas export controls.
Switching a Relation Graph Into a Hand-Drawn Presentation Mode
What This Example Builds
This example builds a centered hierarchy viewer that can switch between a plain card presentation and a sketch-like hand-drawn skin at runtime. The canvas shows seven KPI-style nodes, six labeled links, a floating control window, and a built-in minimap.
Users can turn the hand-drawn mode on or off, change the node border style, and choose a texture overlay such as paper, hatch, grid, or dots. The graph structure does not change. Instead, the example keeps one loaded hierarchy and rethemes its nodes, lines, labels, and arrowheads in place.
The main point of interest is that the sketch effect comes from layered relation-graph features rather than a separate renderer. Custom node slots, SVG filters, SCSS overrides, and graph-instance update APIs work together so one existing graph can adopt a distinct illustrated look.
How the Data Is Organized
The data comes from getJsonData() and returns one RGJsonData object with rootId: "a", a flat nodes array, and a flat lines array. Each node record carries data.name and data.myicon, while the six lines initially only define from and to.
There is a preprocessing step before setJsonData(). During initialization, the code iterates over every line and injects a shared label (Line Text), random junction-point offsets, a random junctionOffset, and the custom my-hand-drawn-arrow-end marker. After the data is loaded, later style changes do not rebuild the JSON. They update the already rendered nodes and lines through updateNodeData() and updateLine().
In a real product, the same shape could represent a small org chart, a team dashboard, a capability map, or a feature hierarchy. data.name can map to people, departments, products, or services, and the style fields written at runtime can stand for theme presets, presentation modes, or brand-specific illustration variants.
How relation-graph Is Used
index.tsx wraps the example with RGProvider, and MyGraph.tsx consumes the shared graph context through RGHooks.useGraphInstance(). The graph is configured with a center layout, explicit levelGaps, centered alignment on both axes, and defaultLineWidth: 2. Built-in node fill and border rendering are intentionally reduced with defaultNodeColor: 'transparent' and defaultNodeBorderWidth: 0 so the visible card body comes from the custom slot content instead.
The main extension point is RGSlotOnNode. Each node renders a large icon, a name, and three fixed KPI rows, then wraps that content in either MyNodeContentBox or HandDrawnBox depending on enableHandDrawn. HandDrawnBox applies asymmetric border-radius values and optional SVG-based textures, while IconSwitcher maps node.data.myicon to Lucide icons and falls back to HelpCircle for unmatched values.
The example also uses RGSlotOnView to mount RGMiniView, so the viewer gains a built-in overview panel without custom viewport code. Graph-instance APIs drive the runtime behavior: setJsonData() loads the graph, getNodes() plus updateNodeData() propagate border and texture state, getLines() plus updateLine() switch line shapes, and moveToCenter(), zoomToFit(), and zoomToFitWithAnimation() keep the viewport aligned after the visual changes.
The hand-drawn effect extends beyond the node slot. MySvgFilters injects the static-hand-drawn SVG filter and the custom arrow marker, while my-relation-graph.scss targets built-in relation-graph node, line, and label layers under the .node-filter-hand-drawn wrapper. That SCSS adds filter distortion, dashed note-like labels, and a checked-state override around the custom content instead of the default graph shadow.
The floating control surface comes from the shared DraggableWindow component. It hosts the style selectors directly in this example, and its settings overlay uses RGHooks.useGraphStore() plus setOptions() to change wheel and drag behavior. The same shared helper also exposes screenshot export through prepareForImageGeneration(), domToImageByModernScreenshot(), and restoreAfterImageGeneration().
Key Interactions
The primary interaction is the hand-drawn mode switch. Toggling it changes the root wrapper class, swaps the node wrapper component, rewrites every line between RGLineShape.Curve8 and RGLineShape.StandardStraight, and then refits the viewport.
When hand-drawn mode is active, two additional selectors appear for border style and background texture. Changing either one triggers a graph-wide pass that writes nodeBorderStyle and nodeBackgroundStyle into each node’s data, so the entire hierarchy changes appearance together.
The floating window itself is interactive. Users can drag it by the title bar, minimize it, open a settings overlay, and keep it above the graph while exploring the current presentation.
The settings overlay adds viewer-level controls rather than content editing. It can switch wheel behavior, switch canvas dragging behavior, and export the current graph DOM as an image.
Key Code Fragments
This options block shows that the example keeps relation-graph’s native center layout and strips the default node body down so slot content can define the visible card style.
const graphOptions: RGOptions = {
debug: true,
defaultJunctionPoint: RGJunctionPoint.border,
defaultNodeColor: 'transparent',
defaultNodeShape: RGNodeShape.rect,
defaultNodeBorderWidth: 0,
defaultLineWidth: 2,
layout: {
layoutName: 'center',
levelGaps: [500, 400, 400],
alignItemsX: 'center',
alignItemsY: 'center'
}
};
This initialization step proves that the line labels, offsets, and custom sketch arrow marker are injected before the graph is loaded.
const myJsonData: RGJsonData = await getJsonData();
myJsonData.lines.forEach(line => {
line.text = 'Line Text';
line.fromJunctionPointOffsetX = Math.random() * 10;
line.fromJunctionPointOffsetY = Math.random() * 10;
line.toJunctionPointOffsetX = Math.random() * 10;
line.toJunctionPointOffsetY = Math.random() * 10;
line.junctionOffset = Math.random() * 20 - 10;
line.endMarkerId = 'my-hand-drawn-arrow-end';
});
await graphInstance.setJsonData(myJsonData);
This runtime update function is the core of the demo: it propagates the selected node styles and line geometry to the already rendered graph.
graphInstance.getNodes().forEach(node => {
graphInstance.updateNodeData(node, {
nodeBorderStyle,
nodeBackgroundStyle
});
});
graphInstance.getLines().forEach(line => {
graphInstance.updateLine(line, {
lineShape: enableHandDrawn ? RGLineShape.Curve8 : RGLineShape.StandardStraight,
});
});
This control fragment shows that the hand-drawn toggle is the entry point, and that the border and texture selectors only appear when the sketch mode is enabled.
<SimpleUISelect data={[
{ value: false, text: 'None' },
{ value: true, text: 'Hand Drawn Style' }
]} currentValue={enableHandDrawn} onChange={setEnableHandDrawn} />
{
enableHandDrawn && <>
<SimpleUISelect data={[
{ value: 'rough', text: 'Rough' },
{ value: 'pencil', text: 'Pencil' },
{ value: 'thick', text: 'Thick' }
]} currentValue={nodeBorderStyle} onChange={setNodeBorderStyle} />
</>
}
This node slot fragment shows how one KPI-card template is wrapped in either the plain box or the hand-drawn box without changing the graph data structure.
<RGSlotOnNode>
{({ node }) => {
const nodeContent = <div>{/* icon, name, KPI rows */}</div>;
if (enableHandDrawn) {
return <HandDrawnBox variant={node.data.nodeBorderStyle} texture={node.data.nodeBackgroundStyle}>
{nodeContent}
</HandDrawnBox>
} else {
return <MyNodeContentBox>{nodeContent}</MyNodeContentBox>
}
}}
</RGSlotOnNode>
This wrapper component proves that the hand-drawn card is not just a color theme. The border geometry and stroke weight change by variant.
const getWobbleStyle = () => {
switch (variant) {
case "pencil":
return {
borderRadius: "255px 15px 225px 15px/15px 225px 15px 255px",
borderWidth: "1px",
borderStyle: "solid",
borderColor: color,
};
case "thick":
return {
borderRadius: "4px 6px 4px 10px / 8px 4px 10px 5px",
borderWidth: "4px",
borderStyle: "solid",
borderColor: color,
};
This SCSS fragment shows how the example pushes the hand-drawn mode onto relation-graph’s built-in node, line, and label DOM layers.
.node-filter-hand-drawn {
.relation-graph {
.rg-node-peel {
.rg-node {
filter: url(#static-hand-drawn);
}
}
.rg-line-peel {
.rg-line {
filter: url(#static-hand-drawn);
}
.rg-line-label {
filter: url(#static-hand-drawn);
background: #fff;
border: 2px dashed #666;
}
}
}
}
This shared helper code shows that screenshot export is handled through graph-instance preparation and restoration rather than by capturing arbitrary page DOM.
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
});
if (imageBlob) {
downloadBlob(imageBlob, 'my-image-name');
}
What Makes This Example Distinct
The comparison data places this example near custom-line-animation, custom-line-style, node, and deep-each, but its emphasis is different from each of those neighbors. The strongest distinguishing point is that it turns one fixed centered hierarchy into a full graph-skin switcher. A single control window changes node wrappers, border variants, texture overlays, line shapes, line jitter, label treatment, and arrowheads together.
Compared with custom-line-animation, this example is less about maintaining a catalog of motion or line presets and more about one cohesive sketchbook presentation mode that spans nodes, lines, labels, and wrappers. Compared with custom-line-style, it goes beyond swapping CSS line classes by also propagating node-level border and texture state through updateNodeData().
Compared with node, which mainly compares node-rendering techniques side by side, this example keeps one card template and rethemes the whole graph at runtime. Compared with deep-each, its graph-instance updates are about whole-scene presentation switching rather than subtree emphasis or focus behavior.
The rarity data also supports a more specific claim: the combination of HandDrawnBox, injected SVG filters, per-node texture selectors, custom arrow markers, and Curve8 versus straight-line switching is unusually rich for a styling-oriented demo. It is a stronger starting point for teams that need an illustrated or branded presentation layer without replacing relation-graph’s renderer.
Where Else This Pattern Applies
This pattern applies to dashboards and presentation views where the graph data stays stable but the visual language needs to change. Examples include investor storytelling screens, product strategy diagrams, education content, workshop boards, white-label tenant themes, and marketing-oriented org or capability maps.
It also applies when teams need a temporary presentation mode rather than a permanent custom renderer. A product can keep its normal relation-graph data model and viewer behavior, then layer on a stylized theme for export, live demos, stakeholder reviews, or a special reporting mode.