Force Classifier
This example generates a synthetic set of user nodes and reclusters them with a custom force layout whenever the active grouping field changes. It demonstrates imperative graph loading, slot-based node badges, a floating legend, and shared canvas utilities for interaction-mode switching and image export.
Attribute-Switchable Force Clustering Without Relationship Lines
What This Example Builds
This example renders an edge-free field of 100 synthetic user nodes and lets the user regroup that field by one attribute at a time. The visible controls switch the active grouping between region, major, grade, and gender, and the force layout recomputes the clusters after each change. Each node is rendered as a compact person badge with a name, a major initial, a region flag, a grade color, and a gender-based shape, while a floating legend explains those encodings.
The most important idea is that the graph is behaving like a classifier playground rather than a relationship diagram. There are no visible links. The layout itself becomes the encoding, and the selected attribute controls which nodes attract each other.
How the Data Is Organized
The data is a flat runtime-generated node list, not a predefined graph dataset loaded from JSON. initializeGraph() creates 100 random user records, assigns them random coordinates, and pushes them into the graph with addNodes() and addLines() instead of setJsonData(). The line list is intentionally empty.
Each node carries its display text plus a metadata object with userRegion, userRegionIcon, userMajor, userGrade, userGender, and currentGroupValue. The first five fields describe the entity. currentGroupValue is a derived field that mirrors the currently selected grouping key and is the only attribute the custom force layout needs to decide whether two nodes should attract each other.
Before the custom layout runs, the example also maps metadata into visual properties: grade becomes node.color, gender becomes node.nodeShape, and the initial grouping is copied into currentGroupValue. In a real application, the same structure can represent employees, customers, students, devices, or cases that need to be regrouped by different categorical facets without changing the underlying entity list.
How relation-graph Is Used
The example is wrapped in RGProvider, then uses RGHooks.useGraphInstance() to control the graph imperatively. RelationGraph is mounted with force-layout defaults, transparent white node styling, a straight-line configuration, and the toolbar placed at the lower right. Even though the graph has no meaningful edges, the standard graph runtime still provides the viewport, toolbar, slots, and layout lifecycle.
The main customization point is layout replacement. After nodes are added, the code instantiates MyForceLayout, a subclass of RGLayouts.ForceLayout, and installs it with setLayoutor(myLayout, true, true). That keeps the relation-graph lifecycle intact while swapping in different force rules. The graph instance API then drives initial placement and later regrouping through stopAutoLayout(), startAutoLayout(), getNodes(), updateNodeData(), moveToCenter(), setZoom(), sleep(), and zoomToFit().
Slots do most of the rendering work. RGSlotOnNode turns each node into a multi-attribute badge instead of the default node body, and RGSlotOnView mounts DataLegendPanel directly on the canvas so the legend stays visible while the graph moves underneath it. The shared DraggableWindow component adds a floating control surface with drag, minimize, canvas settings, and image export behavior. Local SCSS styles the active grouping buttons and overrides checked node and line styles for the relation-graph shell.
Key Interactions
The primary interaction is the group-by button row in the floating window. Clicking one of those buttons updates currentGroupBy, rewrites every node’s currentGroupValue, stops the current force pass, and restarts auto layout so the cloud reforms around the newly selected category.
The floating window itself is interactive: it can be dragged to a new screen position, minimized, and switched into a settings mode. That settings panel changes wheel behavior between scroll, zoom, and none, changes canvas drag behavior between selection, move, and none, and exposes an image export action. The export flow uses relation-graph’s image-preparation hooks before capturing the canvas DOM.
The graph area is otherwise intentionally simple. There is no edge editing, no link labels to inspect, and no node click workflow. The experience is centered on regrouping the same entity set and observing how the layout responds.
Key Code Fragments
This initialization path shows that the example bypasses setJsonData() and installs a custom layout on the live graph instance.
const data: RGJsonData = {
rootId: 'a',
nodes: randomUsers,
lines: []
};
graphInstance.addNodes(data.nodes);
graphInstance.addLines(data.lines);
graphInstance.stopAutoLayout();
const myLayout = new MyForceLayout(
{ maxLayoutTimes: Number.MAX_SAFE_INTEGER, force_node_repulsion: 0.4, force_line_elastic: 0.1 },
graphInstance.getOptions(),
graphInstance
);
graphInstance.setLayoutor(myLayout, true, true);
Each generated node carries both categorical metadata and visual defaults that the layout and slots reuse later.
const node: JsonNode = {
id: 'u-' + graphInstance.generateNewNodeId(),
text: userName,
x: Math.random() * 300,
y: Math.random() * 300,
};
node.data = {
userRegion: region.code,
userRegionIcon: region.icon,
userMajor: majors[Math.floor(Math.random() * majors.length)],
userGrade: randomGrade.grade,
userGender
};
node.color = randomGrade.color;
node.nodeShape = userGender === 'Male' ? RGNodeShape.rect : RGNodeShape.circle;
Regrouping is implemented as a metadata rewrite plus a layout restart, not as a full data reload.
graphInstance.stopAutoLayout();
graphInstance.getNodes().forEach(node => {
graphInstance.updateNodeData(node, {
currentGroupValue: node.data[currentGroupBy]
});
});
setTimeout(async () => {
graphInstance.startAutoLayout();
}, 200);
The custom layout adds attraction only when two nodes share the currently selected grouping value.
this.addGravityByNode(__node1, __node2);
if (__node1.myGroupBy === __node2.myGroupBy) {
this.addElasticByLine(
__node1,
__node2,
1
);
}
The node slot turns one record into a compact badge with text, a major marker, and a region icon.
<RGSlotOnNode>
{({ node }) => (
<div className="px-6 py-1 w-full h-full flex place-items-center justify-center text-xs">
<div className="px-3 py-0.5 bg-gray-100 bg-opacity-30 rounded text-black text-sm">
{node.text}
</div>
<div className="absolute left-[-3px] top-[-3px] h-4 w-4 border border-gray-900 bg-gray-600 rounded text-white text-sm flex place-items-center justify-center">
{node.data.userMajor.substring(0, 1).toUpperCase()}
</div>
<div className="absolute right-[-3px] bottom-[-3px] text-xl">{node.data.userRegionIcon}</div>
</div>
)}
</RGSlotOnNode>
The shared settings panel shows how the example switches canvas behavior and exports the current graph image.
<SettingRow
label="Wheel Event:"
value={wheelMode}
onChange={(newValue: string) => { graphInstance.setOptions({ wheelEventAction: newValue }); }}
/>
<SettingRow
label="Canvas Drag Event:"
value={dragMode}
onChange={(newValue: string) => { graphInstance.setOptions({ dragEventAction: newValue }); }}
/>
<SimpleUIButton onClick={downloadImage}>Download Image</SimpleUIButton>
What Makes This Example Distinct
The prepared comparison file is empty for this example, so the safest distinctness claims come from doc-context. That context marks the following combination as rare, and in several cases very rare, within the example set: synthetic node generation, direct graph loading with addNodes() and addLines() instead of setJsonData(), a custom MyForceLayout extends RGLayouts.ForceLayout, runtime regrouping through updateNodeData(), and a view that stays edge-free while clustering by a selected attribute.
The views labels narrow the scene further: this example is explicitly categorized as an “attribute clustering playground” with “attribute-rich people badges”, an “edge-free clustered node field”, and a “legend-guided light canvas”. That makes it more specialized than a general force-layout demo. The emphasis is not on relationship topology. It is on how one entity set can reorganize itself around different classification dimensions.
The nearest-example list also suggests a useful positioning. force-classifier-pro is the closest structural neighbor because it shares the custom force-layout playground theme, while examples such as css-theme and node-drag-handle overlap more on the reusable floating utility window and the general RelationGraph shell. So this example is the cleaner starting point when the goal is to study attribute-driven reclustering logic itself.
Where Else This Pattern Applies
This pattern transfers well to any exploratory view where entities need to regroup by one selected facet at a time: employees by office, team, or level; students by region, major, or year; customers by market, segment, or tier; or devices by site, type, and status.
It is also useful when explicit edges would add more noise than value. A lightweight force field plus a switchable grouping key can show distribution, density, and cluster boundaries while keeping the data model as a simple flat node list.