Compare Element-to-Node and Element-to-Element Links
This example compares two connector destinations for the same selected DOM list item. One fake line terminates on a real relation-graph node and the other terminates on a plain HTML marker, using the same coordinates and selection flow across both panels.
Comparing Element-to-Node and Element-to-Element Links
What This Example Builds
This example builds a three-panel comparison board inside a single relation-graph canvas. The left panel shows location markers as real graph nodes on top of a map, the middle panel shows selectable interest-group cards, and the right panel shows the same locations as ordinary HTML markers. When a group is selected, the example redraws two animated connectors from the same list item: one goes to the graph node on the left and the other goes to the HTML marker on the right.
The visible result is less about graph topology and more about endpoint strategy. It lets developers compare, under the same coordinates and styling, what changes when a destination stays inside the graph model versus when it becomes a plain DOM target. A floating helper window adds runtime canvas settings and image export, but the main highlight is the synchronized side-by-side connector comparison.
How the Data Is Organized
The example uses a small inline dataset in MyGraph.tsx. Each record contains a groupId, a groupName, and a location object with explicit x and y coordinates. There is no setJsonData() call here. Instead, initialization takes the inline array, stores it in React state as interestGroups, and projects it into graph nodes through addNodes(...).
That same interestGroups state is reused three ways:
- to create fixed-position graph nodes with ids like
node-location-a - to render the middle-column list items with connect-target ids like
group-a - to render right-panel HTML markers with connect-target ids like
el-location-a
This is the important data lesson in the example: one source record drives both graph-native endpoints and plain DOM endpoints. In a production scenario, the records could represent stores on a campus map, warehouse stations, maintenance points, event booths, or service desks where one interface needs to compare multiple anchor strategies against the same physical coordinates.
How relation-graph Is Used
RGProvider wraps the demo so hooks can access the active graph context. The graph itself runs with layout.layoutName = 'fixed', which means the nodes stay at the exact coordinates supplied during addNodes(...). The options also disable debug mode, set border junctions as the default attachment mode, and start the canvas in zoom-on-wheel and move-on-drag mode.
The example relies on relation-graph in four distinct ways:
RGHooks.useGraphInstance()drives graph setup and runtime control. It adds the fixed-position nodes, centers the viewport, fits the scene, clears prior fake lines, adds replacement fake lines, updates runtime options, and prepares the canvas for image export.RGSlotOnNodereplaces the default node renderer with aMapPinmarker. The node slot usescheckedstyling and forwards clicks back into the shared group-selection handler throughnode.data.myGroupId.RGSlotOnCanvasis where the surrounding scene is composed. Instead of using a plain node-link diagram, the demo renders the left map, the middle list, and the right map as one canvas-hosted interface.RGConnectTargetsupplies attachable DOM endpoints for the list cards and the right-side HTML markers. The left-side connector targets a real node, so that fake line setstoType = RGInnerConnectTargetType.Node. The right-side connector uses a normal connect-target id.
The floating helper window is a shared subcomponent rather than a feature unique to this example, but it still matters technically. Its settings panel uses RGHooks.useGraphStore() to reflect live canvas modes and graphInstance.setOptions(...) to switch wheel and drag behavior at runtime. For export, it calls prepareForImageGeneration(), captures the canvas DOM with modern-screenshot, downloads the blob, and then restores the graph state.
Styling is handled in the local SCSS file. The stylesheet gives group cards a purple checked halo, turns both marker types into animated pin-like targets, and makes the line labels inherit the active connector color through var(--rg-line-color).
Key Interactions
- A mount-time effect seeds the nodes, centers the viewport, fits the scene, and auto-selects group
aafter a short delay so the comparison is visible immediately. - Clicking a list card, a left-side graph marker, or a right-side HTML marker all call the same
onGroupClick(...)function. - Each selection updates
activeGroupId, clears the previous fake lines, and inserts a new pair of animated curved connectors for the chosen group. - The helper window can be dragged, minimized, switched into a settings overlay, and used to change wheel mode, change drag mode, or download an image of the current canvas.
Key Code Fragments
This fragment shows that the example starts from one inline dataset, then projects it into fixed-position graph nodes before any connector logic runs.
const myGroups = [
{ groupId: 'a', groupName: 'Sports Group', location: { x: 260, y: 300 } },
// ...
{ groupId: 'f', groupName: 'Science Research Group', location: { x: 600, y: 240 } }
];
setInterestGroups(myGroups);
graphInstance.addNodes(myGroups.map(n => ({
id: 'node-location-' + n.groupId,
text: n.groupName,
x: n.location.x,
y: n.location.y,
disableDrag: true,
data: { myGroupId: n.groupId }
})));
This fragment proves that one selection regenerates two different fake lines: one to a real node and one to a DOM marker.
const myFakeLines: JsonLine[] = [
{
from: 'group-' + groupId,
to: 'node-location-' + groupId,
toType: RGInnerConnectTargetType.Node,
lineShape: RGLineShape.StandardCurve,
text: 'Element To Node',
animation: 2
},
{
from: 'group-' + groupId,
to: 'el-location-' + groupId,
lineShape: RGLineShape.StandardCurve,
text: 'Element To Element',
animation: 2
}
];
graphInstance.clearFakeLines();
graphInstance.addFakeLines(myFakeLines);
This fragment shows how the graph-side destination is rendered as a custom node marker and routed back into the shared selection state.
<RGSlotOnNode>
{({ node, checked }: RGNodeSlotProps) => (
<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>
)}
</RGSlotOnNode>
This fragment shows that the same state also drives ordinary HTML connect targets on the comparison panel to the right.
{interestGroups.map(group => (
<div
key={group.groupId}
className="absolute"
style={{ left: group.location.x + 'px', top: group.location.y + 'px' }}
>
<RGConnectTarget targetId={`el-location-${group.groupId}`} junctionPoint={RGJunctionPoint.lr}>
<div onClick={() => onGroupClick(group.groupId)} className="pointer-events-auto cursor-point c-i-location">
<MapPin className="transform translate-y-[-25px] translate-x-[-5px]" size={24} />
</div>
</RGConnectTarget>
</div>
))}
What Makes This Example Distinct
According to the comparison data, this example is not just a basic element-to-node demo. Its distinctive point is that the same selected source item redraws two synchronized connectors at once: one ends on a real relation-graph node, while the other ends on a plain HTML marker. That makes it a stronger reference for attachment-strategy decisions than a single-pattern example.
Compared with element-line-edit, this demo is less about one node-backed destination and more about side-by-side contrast. Compared with element-lines, it keeps one destination inside the graph model instead of putting both endpoints in ordinary HTML. Compared with broader map-backed boards such as interest-group and inventory-structure-diagram, it spends less effort on domain structure and more on making the endpoint difference easy to inspect.
Another comparison-backed distinction is the way it preserves spatial equivalence. The same interestGroups coordinate set is mirrored into left-side graph nodes and right-side HTML markers, so the visual difference comes from endpoint type rather than from layout drift. The combination of fixed-layout node projection, RGSlotOnNode, RGSlotOnCanvas, paired RGConnectTarget usage, automatic default selection, and synchronized fake-line replacement is relatively uncommon across the example set.
Where Else This Pattern Applies
This pattern transfers well to products that need to evaluate whether an anchor should remain graph-native or move into plain DOM. Typical cases include campus and facility maps, logistics dashboards, seat or booth planners, retail floor systems, and monitoring panels where the same entity must be highlighted in a list and on a spatial surface at the same time.
It also works as a migration pattern. A team can keep one endpoint in the graph model for viewport-aware behavior, checked state, and slot rendering, while exposing another endpoint as ordinary HTML for easier integration with existing cards, panels, or overlays. The example is therefore useful both as a UI comparison board and as a starting point for hybrid graph-plus-DOM interfaces.