JavaScript is required

Toys - Clock

This example builds a self-running clock-like radial graph from one center node, sixty generated second nodes, and a separate moving marker node. It is a compact reference for timer-driven playback, checked-state synchronization, and custom node-slot rendering in a viewer-only relation-graph scene.

Clock-Style Radial Playback with a Moving Marker Node

What This Example Builds

This example turns a relation graph into a clock-like radial display. A large center node works as the hub, sixty numbered circle nodes form the outer ring, and a separate semi-transparent marker node moves from spoke to spoke every 500 ms.

The viewer does not expose local controls or editing tools. Its main behavior is autoplay: the graph loads itself on mount, centers the canvas, fits the viewport, and then advances through the sixty positions while the active spoke stays visually checked.

The most notable detail is that the animation is not produced by rebuilding the graph on every step. Instead, the code keeps the numbered nodes in place, moves one dedicated marker node, and updates the center node’s custom data as playback progresses.

How the Data Is Organized

The graph data is assembled inline inside initializeGraph() before setJsonData(...) runs. The initial dataset contains one root node with width: 100, height: 100, and data.percent = 0. A loop then creates nodes 1 through 60 and one line from root to each numbered node, which gives the graph its spoke structure. After that, the code appends a separate current node at an off-canvas position so it can be moved independently during playback.

This means the example has a clear separation between stable structure and changing runtime state. The spoke nodes and lines represent fixed positions in a cycle, while the current node and the root node’s percent field represent the live state layered on top of that structure.

In a real application, the same shape could represent time slots, shift rotations, machine-cycle checkpoints, guided walkthrough steps, or any other fixed circular sequence that needs a visible “current position” indicator.

How relation-graph Is Used

index.tsx wraps the demo in RGProvider, and MyGraph.tsx uses RGHooks.useGraphInstance() to drive the graph entirely through the provider-scoped instance API. The example starts from relation-graph’s center layout, with low elastic and repulsion values plus maxLayoutTimes: 3000, which is enough to keep the radial arrangement readable while still allowing a short auto-layout phase later in the cycle.

The graph options also define the visual baseline: straight lines, line text on path, 40x40 default nodes, no built-in node border, a white node background, and translucent gray links. The visible styling is then supplied by RGSlotOnNode, which renders three different node bodies: a gradient current marker, a larger root node with a fill gauge driven by node.data.percent, and the ordinary numbered second nodes with a thin gray border.

At runtime, the instance APIs do the real work. setJsonData(...), moveToCenter(), and zoomToFit() establish the initial view. startAutoLayout() begins a short layout phase at second 15, stopAutoLayout() and doLayout() reset the scene at second 60, updateNodeData(...) changes the root gauge, updateNodePosition(...) moves the marker node, and updateOptions(...) stores the checked node and checked line that should be highlighted for the current second. dataUpdated() flushes each playback step.

The imported SCSS file adds checked-line and node-text overrides under a .my-graph wrapper. The reviewed JSX does not render that wrapper itself, so those stylesheet overrides are only effective when an outer host provides the matching container.

Key Interactions

This is a passive viewing demo rather than a manual interaction recipe. The graph initializes automatically on mount, so the main experience is watching prepared states advance without user input.

The important runtime interaction is the 500 ms playback tick. On each tick, the code looks up the active numbered node, moves the current marker node onto that node’s coordinates, finds the matching line id through getLinks(), and updates checkedNodeId plus checkedLineId together so the visual emphasis follows the moving marker.

There are also two stage changes inside the same cycle. At second 15 the demo enables auto layout, at second 18 it fits the viewport again, and at second 60 it resets the center gauge, stops auto layout, runs a fresh layout pass, recenters the canvas, and starts the next cycle.

Key Code Fragments

This fragment shows that the clock face is generated in memory before the graph is loaded.

const myJsonData: RGJsonData = {
    rootId: 'root',
    nodes: [{ id: 'root', text: '', width: 100, height: 100, data: { percent: 0 }, nodeShape: RGNodeShape.circle }],
    lines: []
};

for (let i = 1; i < 61; i++) {
    myJsonData.nodes.push({ id: i.toString(), text: i.toString(), nodeShape: RGNodeShape.circle });
    myJsonData.lines.push({ id: `line-${i}`, from: 'root', to: i.toString(), text: '' });
}
myJsonData.nodes.push({ id: 'current', text: '', x: -20, y: -20, nodeShape: RGNodeShape.circle });

This fragment shows that the demo fits the graph once and immediately starts autoplay.

await graphInstance.setJsonData(myJsonData);
graphInstance.moveToCenter();
graphInstance.zoomToFit();
timer.current = setInterval(() => {
    play();
}, 500);

This fragment proves that playback is driven by instance updates rather than full data reloads.

if (currentSecond.current <= 20 && rootNode) {
    graphInstance.updateNodeData(rootNode, {
        percent: currentSecond.current / 15
    });
}

// ...

graphInstance.updateNodePosition(focusNode, targetNode.x, targetNode.y);
const targetLinkId = graphInstance.getLinks().find((l) => l.toNode.id === targetNode.id)?.line.id;
graphInstance.updateOptions({
    checkedNodeId: 'current',
    checkedLineId: targetLinkId
});

This fragment shows that one node slot renders three different visual roles.

{node.id === 'current' && (
    <div style={{ opacity: 0.5, borderRadius: '50%', background: 'linear-gradient(to right, #00FFFF, #FF00FF)' }}>
        {node.text}
    </div>
)}
{node.id === 'root' && (
    <div style={{ borderRadius: '50%', overflow: 'hidden', border: '#000000 solid 1px' }}>
        <div style={{ height: `${percent * 100}%`, marginTop: `${(1 - percent) * 100}%` }} />
    </div>
)}

What Makes This Example Distinct

The comparison data shows that this example is unusual not because it simply auto-starts or uses center layout, but because it combines several rarer techniques in one small viewer. It generates sixty spoke nodes in code, appends a dedicated current marker node before load, moves that marker with updateNodePosition(...), and keeps the visual focus synchronized through checkedNodeId and checkedLineId.

Compared with multi-group and multi-group-3, this example uses the center layout as an animated instrument surface rather than as a way to present disconnected groups or transition into a more general layout change. Compared with force-sea-anemone, its motion is deterministic and step-based: one marker advances through a fixed 60-step cycle instead of letting many nodes keep reforming under changing force settings. Compared with expand-holder-slot, the custom rendering supports passive playback and the center gauge rather than a click-driven expand or collapse control.

Its strongest distinctive combination is the clock-like spoke dataset, the slot-rendered circular node family, the gradient center fill driven by updateNodeData(...), and the brief second-gated auto-layout phase inside the same playback loop. That makes it a stronger starting point for scripted focus tracking than for general graph exploration.

Where Else This Pattern Applies

The same pattern can be reused for unattended dashboards that need to rotate through fixed checkpoints, such as manufacturing cycle displays, kiosk countdowns, patrol or shift rotations, or periodic monitoring views that should highlight one slot at a time.

It also adapts well to guided presentations of graph state. Instead of rebuilding the dataset for every frame, a product can keep the structural graph stable, move one marker node to the current target, and update a small amount of node metadata to show progress, capacity, or completion.