JavaScript is required

Custom Toolbar and Cross-Layer Tooltips

This example shows how to disable relation-graph's built-in toolbar and replace it with a custom floating toolbar inside `RGSlotOnView`. It also demonstrates one hover system that spans toolbar buttons, view-slot anchors, canvas-slot anchors, node-slot content, and graph-rendered nodes or lines through recursive tooltip discovery and graph hit-testing.

Building a Custom Toolbar with Cross-Layer Tooltips

What This Example Builds

This example builds a read-only relation-graph scene that replaces the built-in toolbar with a floating custom toolbar and drives one hover system across several graph surfaces. Users see a small sample graph, a light vertical toolbar, four tooltip anchor blocks in the view layer, two more anchors in the canvas layer, custom node content, and dark tooltip cards that appear as the pointer moves across those elements.

The important result is not just “toolbar icons with English tooltips.” The same wrapper-level hover flow also covers a badge inside a custom node slot and falls back to graph-rendered nodes and lines when the pointer is no longer on custom HTML. That makes the demo a compact reference for products that mix overlay DOM with native graph objects in one scene.

How the Data Is Organized

The graph payload is assembled inline inside initializeGraph() as a static RGJsonData object with rootId: 'a', four nodes, and seven lines. The node records already contain their visual overrides, including color, fontColor, nodeShape, width, and height. The line records also encode repeated edges between the same nodes, per-line lineShape overrides, a custom fromJunctionPoint, and one line with showEndArrow: false.

There is no preprocessing pipeline before setJsonData(). The component constructs the final dataset directly in code and loads it as-is, then recenters and fits the viewport. In a real application, the same shape could represent services and dependencies, people and reporting links, or devices and network routes, while the per-node and per-line overrides could come from type, status, or route metadata.

How relation-graph Is Used

index.tsx wraps the page in RGProvider, and both MyGraph.tsx and MyToolbar.tsx use RGHooks to reach relation-graph state and APIs. The example does not set an explicit layout option, so the graph uses relation-graph’s default layout behavior for this small dataset. Instead, the options focus on presentation and toolbar replacement: lineTextMaxLength, multiLineDistance, defaultLineTextOnPath, defaultLineShape, and showToolBar: false.

After mount, the graph instance loads the inline JSON with setJsonData(...), then calls moveToCenter() and zoomToFit(). From there, the graph instance stays central to the interaction model. MyGraph.tsx uses getViewBoundingClientRect() to convert hovered HTML elements into graph-relative tooltip positions, and it uses isLine(...), isNode(...), and getViewXyByEvent(...) to keep hover feedback working on graph-rendered objects. MyToolbar.tsx uses the same instance for fullscreen(), zoom(...), setZoom(100), moveToCenter(), and zoomToFit().

Three slot layers carry the custom UI. RGSlotOnView hosts the floating toolbar and four directional tooltip anchors. RGSlotOnCanvas adds more tooltip-enabled HTML directly over the canvas. RGSlotOnNode replaces node content and inserts a tooltip-enabled badge inside node e. There is no editing flow here: the graph is a viewer with utility controls and hover inspection. The local SCSS stays focused on the dark hover badges in .c-tips and the white toolbar button styling in .my-toolbar-button.

Key Interactions

  • Moving the pointer over any element with data-tooltip opens a dark tooltip card positioned according to data-tooltipposition.
  • That hover lookup works across toolbar buttons, view-slot blocks, canvas-slot blocks, and a badge rendered inside the custom node slot.
  • If no tooltip-enabled HTML ancestor is found, the same onMouseMove handler falls back to isLine(...) and isNode(...), so graph-rendered lines and nodes still show a small hover badge.
  • Clicking the toolbar buttons triggers fullscreen, zoom step, fit-to-view, and profile actions through the graph instance or a local click callback.
  • The download button is present as UI chrome, but its handler is only a placeholder comment and does not export an image.

Key Code Fragments

This recursive helper proves that tooltip discovery is based on DOM ancestry, assigned slots, and Shadow DOM host boundaries instead of on one specific layer.

const getTooltip = (el: HTMLElement | null): HTMLElement | null => {
    if (!el) return null;
    if (el.dataset.tooltip) return el;
    if (el.classList.contains('my-graph')) return null;

    let nextParent: HTMLElement | null = null;
    if (el.assignedSlot) {
        nextParent = el.assignedSlot as HTMLElement;
    } else {
        nextParent = el.parentElement;
    }

This options block shows that relation-graph’s built-in toolbar is intentionally disabled and that repeated-line spacing and label rendering are controlled at the graph level.

const graphOptions: RGOptions = {
    lineTextMaxLength: 6,
    multiLineDistance: 20,
    defaultLineTextOnPath: true,
    defaultLineShape: RGLineShape.StandardCurve,
    showToolBar: false
};

This payload fragment shows that the demo data already carries node styling, repeated links, and per-line route overrides before it is loaded into the graph.

const myJsonData: RGJsonData = {
    rootId: 'a',
    nodes: [
        { id: 'a', text: 'A' },
        { id: 'b', text: 'B', color: '#43a2f1', fontColor: 'yellow' },
        { id: 'c', text: 'C', nodeShape: RGNodeShape.rect, width: 120, height: 80 },
        { id: 'e', text: 'E', nodeShape: RGNodeShape.circle, width: 150, height: 150 }
    ],
    lines: [
        { id: 'l1', from: 'a', to: 'b', text: 'text a -> b', color: '#43a2f1' },
        { id: 'l10', from: 'a', to: 'e', text: 'text a -> e', lineShape: RGLineShape.Curve7 }
    ]
};

This mouse-move branch proves that HTML tooltip anchors are measured against the graph view and converted into reusable tooltip state.

const tooltipElement = getTooltip(eventTaregtElement);
if (tooltipElement) {
    const tooltipText = tooltipElement.dataset.tooltip || '';
    const tooltipPostion = tooltipElement.dataset.tooltipposition || 'left';
    const tooltipElementRect = tooltipElement.getBoundingClientRect();
    const viewRect = graphInstance.getViewBoundingClientRect();
    setTooltipInfo({
        overObjectType: 'element',
        overObject: {
            text: tooltipText,
            width: tooltipElementRect.width,
            height: tooltipElementRect.height,

This fallback branch shows how the same hover flow still supports graph-rendered lines and nodes when the pointer is not over custom HTML.

const line = graphInstance.isLine(eventTaregtElement);
if (line) {
    const basePositionXy = graphInstance.options.fullscreen
        ? { x: 0, y: 0 }
        : graphInstance.getViewXyByEvent($event.nativeEvent);
    setTooltipInfo({
        overObjectType: 'line',
        overObject: line,
        position: {
            x: basePositionXy.x + 10,
            y: basePositionXy.y + 10
        }
    });
}

This slot composition is the core relation-graph pattern: one viewer scene combines custom view overlays, canvas overlays, and node content in the same graph instance.

<RGSlotOnView>
    <MyToolbar onUserAvatarClick={onUserAvatarClick} />
    <div data-tooltipposition="top" data-tooltip="Top Tooltip Content">Top</div>
</RGSlotOnView>
<RGSlotOnCanvas>
    <div data-tooltipposition="top" data-tooltip="Canvas Slot Content 1">
        Canvas Slot Content 1
    </div>
</RGSlotOnCanvas>
<RGSlotOnNode>

This toolbar fragment proves that viewport controls are implemented as plain custom buttons that call relation-graph instance APIs directly.

const zoomToFit = async () => {
    graphInstance.setZoom(100);
    graphInstance.moveToCenter();
    graphInstance.zoomToFit();
};
const addZoom = (buff: number) => {
    graphInstance.zoom(buff);
};

What Makes This Example Distinct

The comparison data shows that this example stands out because it unifies one tooltip flow across RGSlotOnView, RGSlotOnCanvas, RGSlotOnNode, custom toolbar buttons, and graph-rendered nodes or lines. That is broader than a node-only tooltip recipe and more interaction-focused than a toolbar-only customization sample.

Compared with toolbar-buttons, the lesson here is not reuse of RGToolBar or RGMiniToolBar containers. The toolbar is deliberately plain, and most of the implementation effort goes into hover discovery, directional placement, and fallback from DOM anchors to graph hit-testing. Compared with node-line-tips-contentmenu, this example is lighter on commands and broader on surface coverage: it does not add right-click menus, but it does show how HTML overlays and graph objects can share one hover pipeline. Compared with node-tips, it is not limited to node hover cards because it also covers toolbar buttons, canvas-slot content, and line hover badges.

That combination makes it a strong starting point for teams that need hover help to behave consistently across mixed graph layers without introducing editing logic or separate tooltip implementations for each surface.

Where Else This Pattern Applies

This pattern transfers well to graph-based admin consoles, investigation tools, architecture maps, and monitoring dashboards where products mix graph-native objects with custom overlay controls. It is useful when the requirement is “one tooltip language everywhere” rather than a specialized node inspector.

The same structure can be extended with richer tooltip content, delayed hover timing, pinned detail cards, export actions behind the placeholder download button, or viewport-aware clamping logic. It can also support branded utility chrome around a graph while keeping the graph itself in viewer mode.