Character Relationship Graph Filters
This example builds a read-only people-relationship viewer with avatar nodes, relation labels, and an external toolbar for gender, alignment, and relation-type filtering. It loads one mocked async dataset into relation-graph, then uses updateNode(...) and updateLine(...) to dim unmatched items so the full network stays visible as context.
Filter a Character Relationship Graph While Preserving Context
What This Example Builds
This example builds a read-only people-relationship viewer with a force-directed canvas, portrait-style nodes, labeled relation lines, a full-width filter bar, and a floating helper window. The graph fills the page height, keeps the full cast visible, and presents relationships such as teacher-student, relative, collusion, and report directly on the link paths.
Users can narrow the view by gender, by character alignment, and by relation type. The important behavior is that unmatched items are faded to low opacity instead of being removed, so the graph still shows surrounding context while the active subset stands out.
How the Data Is Organized
The example loads one RGJsonData object from mock-data-api.ts. It uses a flat nodes array and a flat lines array, with rootId: "N13" as the anchor id. Each node includes id, text, color, borderColor, and a data object that carries the filterable fields: gender, isGoodMan, and icon. Each line includes from, to, text, color fields, and data.type, which is the relation category used by the toolbar chips.
There is no structural preprocessing before setJsonData(...). fetchJsonData() simply wraps the static dataset in a short async delay and returns it unchanged. In real applications, the same shape could represent stakeholder maps, investigation boards, family or cast diagrams, partner ecosystems, or customer relationship graphs, as long as node metadata and relation categories are available.
How relation-graph Is Used
index.tsx wraps the example in RGProvider, which lets both MyGraph and the shared floating utility panel resolve the same graph instance through hooks. MyGraph uses RGHooks.useGraphInstance() to load the dataset, show and clear the loading state, center the viewport, fit the graph after mount, clear checked state on blank-canvas clicks, and update node or line opacity during filtering.
The graph options define the viewer style. The demo uses the built-in force layout with force_node_repulsion: 0.5 and maxLayoutTimes: 50, straight lines, circular nodes, line text rendered on the path, and multiLineDistance: 20 so parallel relations stay readable. The toolbar is positioned vertically at the left-top corner, and a fallback node color plus node border width are supplied at the graph level even though the dataset also provides explicit colors.
The main visual customization comes from RGSlotOnNode, which replaces the default node body with a 48x48 portrait block and a text label underneath. The SCSS file then restyles checked nodes and checked lines through relation-graph’s internal peel classes, turning the line label into a colored chip and adding a colored ring treatment around the active node shell.
The shared DraggableWindow and CanvasSettingsPanel show how auxiliary tools can sit above the graph without changing the core filtering pattern. Through useGraphStore() and graphInstance.setOptions(...), the panel switches wheel and drag behavior at runtime, and it uses prepareForImageGeneration() plus restoreAfterImageGeneration() to download an image of the canvas. Those controls are secondary scaffolding; the example’s main lesson is metadata-driven filtering on a mounted graph instance.
Key Interactions
The primary interaction is the external filter toolbar. Two segmented selectors choose gender and character alignment, while a set of clickable relation chips toggles which relation categories remain fully visible. Each toolbar change updates React state and reruns the graph-instance filtering pass.
The filtering behavior is deliberately focus-plus-context rather than hide-or-rebuild. Node opacity depends on gender and isGoodMan, while line opacity depends only on the selected relation-type list. That separation means the example dims nodes and lines independently instead of trying to infer line visibility from node matches.
Clicking the canvas background calls clearChecked(), which resets relation-graph’s checked styling but does not clear the active filters. The floating helper window can also be dragged, minimized, opened into a settings panel, and used to download an image, but those actions affect the workspace around the graph rather than the data itself.
Key Code Fragments
This dataset fragment proves that the graph data carries filterable node metadata such as gender, isGoodMan, and icon.
{
"id": "N1",
"text": "Liangping.Hou",
"color": "#ec6941",
"borderColor": "#ff875e",
"data": {
"isGoodMan": "1",
"gender": "male",
"icon": "https://dss2.baidu.com/6ONYsjip0QIZ8tyhnq/it/u=2308340537,462224207&fm=58&app=83&f=JPEG?w=250&h=250&s=EC708F46DA96B89CB69D5DDA0300D014&n=侯亮平"
}
}
This line fragment shows that relation filtering is driven by data.type, not by special edge ids or a second derived table.
{
"from": "N20",
"to": "N18",
"text": "report",
"color": "#ed724d",
"fontColor": "#ed724d",
"data": {
"type": "report"
}
}
This initialization path shows the example’s async loading wrapper and the viewport-fitting sequence after the dataset arrives.
const initializeGraph = async () => {
const myJsonData: RGJsonData = await fetchJsonData();
graphInstance.loading();
await graphInstance.setJsonData(myJsonData);
graphInstance.clearLoading();
graphInstance.moveToCenter();
graphInstance.zoomToFit();
};
This options block proves that the example is configured as a force-directed avatar viewer with straight labeled links and visible multi-edge spacing.
const graphOptions: RGOptions = {
debug: false,
defaultLineShape: RGLineShape.StandardStraight,
defaultNodeShape: RGNodeShape.circle,
defaultLineTextOnPath: true,
multiLineDistance: 20,
layout: {
layoutName: 'force',
force_node_repulsion: 0.5,
maxLayoutTimes: 50
},
defaultNodeBorderWidth: 2,
defaultNodeColor: '#e85f84'
};
This filtering pass is the core technique: it scans the mounted graph and changes node and line opacity in place instead of rebuilding the dataset.
graphInstance.getNodes().forEach(thisNode => {
let _isHideThisLine = false;
if (checkecItems.checkedGender !== '') {
if (thisNode.data!['gender'] !== checkecItems.checkedGender) {
_isHideThisLine = true;
}
}
if (checkecItems.checkedCharacterAlignment !== '') {
if (thisNode.data!['isGoodMan'] !== checkecItems.checkedCharacterAlignment) {
_isHideThisLine = true;
}
}
graphInstance.updateNode(thisNode, {
opacity: _isHideThisLine ? 0.1 : 1
});
});
graphInstance.getLines().forEach(thisLine => {
let hideLine = false;
if (checkecItems.checkedRelationType.indexOf(thisLine.data!['type']) === -1) {
hideLine = true;
}
graphInstance.updateLine(thisLine, {
opacity: hideLine ? 0.1 : 1
});
});
This slot renderer shows how the example turns default nodes into portrait markers with the label placed below the image.
<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>
What Makes This Example Distinct
The comparison data shows that this example stands out most when a team needs category-based narrowing of a fixed relationship map. Compared with search-and-focus, it does not jump the viewport to one chosen node or build a locator workflow. It keeps the whole network in view and applies broad filtering by gender, alignment, and relation type through runtime opacity updates.
Compared with scene-relationship-op, this demo stays structurally fixed and read-only. It does not open node-centered edit overlays, add children, duplicate nodes, or create edges. That makes it a tighter reference for toolbar-driven exploration on a prepared people graph than for contextual graph editing.
Compared with line-hightlight-pro, it shifts the emphasis from one clicked edge to multi-axis metadata filtering across many elements. Its rare combination is a force-directed avatar relationship map, straight on-path label chips, a top filter toolbar, and opacity-based focus plus context, all layered on top of a mounted graph instance rather than a rebuilt dataset.
Where Else This Pattern Applies
This pattern transfers well to investigation dashboards where analysts need to dim a people network by role, affiliation, risk label, or relation category without losing the broader context. It also fits stakeholder maps, customer relationship views, cast diagrams, and partnership graphs where the same network needs to be explored from several metadata angles.
The same approach can be extended to fraud networks, compliance reviews, and internal org or influence maps. In those scenarios, the important reusable idea is not the specific person dataset but the combination of external controls, stable graph structure, and runtime opacity updates that keep the full network readable while one subset is emphasized.