JavaScript is required

Jin Yong Martial Sect Map Browser

This example overlays fixed-position martial-sect pins on a bundled China SVG and connects a selected sidebar card to one or many pins with fake lines. The same selection state also drives a detail panel, making it a reference for map-backed master-detail browsing rather than geographic analysis or graph editing.

Browse Jin Yong Martial Sects on a China Map

What This Example Builds

This example builds a themed browsing view that places martial-sect markers on top of a bundled SVG map of China. The canvas is arranged as three coordinated surfaces: the map on the left, a sidebar list of sects in the middle, and a conditional detail card on the right.

Users can click either a map pin or a sidebar card to change the current sect. That selection redraws the purple highlight connectors and swaps the detail content without rebuilding the whole scene. The main point of the example is not geographic analysis or graph editing. It is a map-backed master-detail browser built from fixed graph nodes plus ordinary DOM endpoints.

How the Data Is Organized

The source data comes from the local AllMenPai array in my-data.ts. Each record follows the GroupDoc shape and can contain:

  • a stable id
  • a display title
  • long-form detail fields such as masters, martialArts, descriptions, and optional moreInfo
  • optional x and y coordinates

Only records with coordinates are projected into relation-graph nodes. Records without coordinates still appear in the sidebar and can still drive connector logic and detail content, which is why the dataset can mix summary-like entries with map-backed entries.

Before anything is rendered, the code filters AllMenPai for entries that have x and y, scales those coordinates by 0.35, and converts them into JsonNode records. The same source ids are then reused in two places: node ids such as location-1 and DOM endpoint ids such as doc-group-1. There is no setJsonData(...) or doLayout(...) step here. The scene is assembled imperatively with addNodes(...), and the same data objects also feed the detail panel.

In a real system, this structure could represent regional offices, historical sites, retail locations, service zones, or any map-backed catalog where some list items represent categories while others represent fixed points.

How relation-graph Is Used

The graph runs in fixed layout mode with layoutName: 'fixed', so every visible map pin stays at an explicit coordinate that matches the China SVG backdrop. debug is disabled, and the component gets its runtime control surface from RGHooks.useGraphInstance().

Instead of loading a graph JSON model, the example uses instance APIs directly:

  • addNodes(...) inserts the positioned sect pins after mount
  • zoomToFit() frames the initial canvas
  • clearFakeLines() and addFakeLines(...) replace the active connector set whenever the selection changes

RGProvider supplies the graph context, and RelationGraph hosts both the graph layer and the canvas-composed UI. The example does not declare an explicit canvas slot component, but the content placed inside RelationGraph is used as canvas content: the China SVG, the sidebar card stack, and the conditional detail card all live inside the graph canvas area.

Two relation-graph extension points matter most:

  • RGSlotOnNode replaces the default node body with a clickable MapPin marker and a numeric label
  • RGConnectTarget turns each sidebar card into a DOM endpoint that fake lines can connect to

The fake lines are configured as curved left-to-right connectors with RGInnerConnectTargetType.Node, RGJunctionPoint.lr, and RGLineShape.StandardCurve. Styling is intentionally simple but important to the scene: the SCSS file adds a diagonal grid background to the canvas, makes the China SVG green, and keeps the node body transparent so the custom pin slot remains the visible marker.

The example is a viewer, not an editor. Users browse prepared records and temporary highlight states, but they do not create nodes, drag endpoints, or persist changes.

Key Interactions

  • The component initializes itself after mount by adding the fixed nodes, fitting the viewport, and selecting group 0 after a short timeout.
  • Clicking a map pin calls the same onGroupClick(...) flow used by the sidebar, so both surfaces update one shared checkedGroup state.
  • Clicking a sidebar card changes the highlighted card style, redraws the active fake lines, and swaps the detail panel content.
  • Most selections draw one connector from the sidebar card to one map node, but groups 0 and 6 switch into a fan-out mode that connects one sidebar item to every coordinate-backed node.
  • The detail panel appears only when a group is selected and reveals masters, martial arts, descriptions, and optional extra text from the selected record.

Key Code Fragments

This fragment shows that the map pins are not hard-coded as graph JSON. They are projected from local records with coordinates, scaled, and tagged with the source group id.

const nodes: JsonNode[] = AllMenPai.filter(g => g.x && g.y).map(g => ({
    id: 'location-' + g.id,
    text: '' + g.id,
    x: g.x * 0.35,
    y: g.y * 0.35,
    data: {
        myGroupId: g.id
    }
}));

This fragment proves that initialization is imperative: the example inserts nodes, fits the viewport, and preselects group 0 on startup.

graphInstance.addNodes(nodes);
setTimeout(() => {
    graphInstance.zoomToFit();
    onGroupClick('0');
}, 200);

This fragment shows the selection-dependent connector strategy. Groups 0 and 6 fan out to all positioned nodes, while every other valid selection draws one connector.

if (groupId === '0' || groupId === '6') {
    AllMenPai.filter(g => g.x && g.y).forEach((item, idx) => processItem(item.id, idx));
} else if (groupId) {
    processItem(groupId, 0);
}

graphInstance.clearFakeLines();
graphInstance.addFakeLines(myFakeLines);

This fragment shows how relation-graph nodes are turned into map pins that route clicks back into the shared selection state.

<RGSlotOnNode>
    {({ node, checked }: RGNodeSlotProps) => {
        return (
            <div
                className={`pointer-events-auto cursor-point c-i-location ${checked ? 'c-i-location-active' : ''}`}
                onClick={() => onGroupClick(node.data.myGroupId)}
            >
                <MapPin className='transform translate-y-[-25px] translate-x-[-5px]' size={24} />
                <div className="absolute left-[50%] top-1 w-6 text-center transform translate-x-[-50%]">{node.text}</div>
            </div>
        );
    }}
</RGSlotOnNode>

This fragment shows how ordinary sidebar cards become DOM endpoints that relation-graph can connect to.

<RGConnectTarget
    targetId={`doc-group-${group.id}`}
    junctionPoint={RGJunctionPoint.lr}
    disableDrag={true}
>
    <div
        className={`px-3 py-2 cursor-pointer bg-green-200 hover:bg-green-300 rounded-lg ${checkedGroup?.id === group.id ? ' bg-green-600 text-white' : ''}`}
        onClick={() => { onGroupClick(group.id); }}
    >
        {group.title}
    </div>
</RGConnectTarget>

This fragment shows that the right-hand panel is driven directly from the selected data record rather than from separate detail-fetching logic.

{checkedGroup && <div className="grow p-5 bg-white border rounded-2xl flex flex-col gap-1 whitespace-pre-wrap">
    <p className="py-1 text-sm">Masters:</p>
    <p className="py-1 text-xs bg-gray-100 rounded p-2">{checkedGroup.masters}</p>
    <p className="py-1 text-sm">Martial Arts:</p>
    <p className="py-1 text-xs bg-gray-100 rounded p-2">{checkedGroup.martialArts}</p>

    {checkedGroup.descriptions && <div className="py-1 text-xs bg-gray-100 rounded p-2">
        {
            checkedGroup.descriptions.map((text, idx) => (

What Makes This Example Distinct

The comparison data places this example near other map-backed endpoint demos, but with a different emphasis. It is distinctive because it combines five things in one compact scene: a bundled China SVG backdrop, fixed-position graph pins, sidebar RGConnectTarget endpoints, selection-driven fake-line regeneration, and a descriptive master-detail panel. That makes it read more like a curated reference board than a generic connector demo.

Compared with element-line-edit, this example goes beyond one-to-one list-to-location highlighting by adding a content-heavy detail card and a special fan-out mode for groups 0 and 6. Compared with inventory-structure-diagram, it keeps the map anchors as real relation-graph nodes rendered through RGSlotOnNode, instead of relying primarily on canvas-slot DOM anchors.

Compared with element-connect-to-node and interest-group, the main reusable lesson here is not endpoint strategy comparison or two-sided membership exploration. It is the use of one selection state to keep the sidebar emphasis, connector set, and long-form detail content synchronized around a thematic map scene.

Where Else This Pattern Applies

This pattern transfers well to interfaces where one part of the UI is ordinary document-style content and the other part is a fixed spatial graph layer.

  • A regional office browser where category cards connect to one or many branch markers on a branded map
  • A museum, campus, or exhibition guide where a selected topic highlights several prepared positions and opens long-form reference notes
  • A facility dashboard where list records connect to fixed equipment points on a floor plan or schematic background
  • A historical or cultural atlas where selections reveal descriptive content while linking summary entries to multiple annotated locations

The reusable idea is to keep the spatial anchors as graph nodes, keep the browsing UI as DOM, and redraw only the connector set that matches the current selection.