JavaScript is required

Graph Theme and Layout Preset Switcher

This example keeps one relation-graph dataset mounted while users switch both theme presets and layout presets from an on-canvas toolbar. It shows how to batch-update node and line classes, apply richer layout preset objects, and keep export and canvas controls in the same viewer shell.

Switching One Graph Between Themes and Layout Presets

What This Example Builds

This example builds a full-screen graph presentation workbench around one fixed relation-graph dataset. The canvas starts as a tree, then the user can open a palette popover to apply one of seven item-level themes or open a network popover to switch among center, tree, folder, circle, and io-tree layout presets. A minimap stays visible in the graph overlay, and a draggable helper window exposes canvas settings and image export.

The important result is not the sample data itself. The point is that one mounted graph instance keeps the same nodes and lines in memory while its visual skin, node geometry, connector style, and layout family are changed at runtime.

How the Data Is Organized

The data is declared inline inside initializeGraph() as an RGJsonData-shaped object with rootId, nodes, and lines. The nodes use simple IDs such as g2-a, g2-c, and g2-c1, and the lines define parent-child relationships between them. This makes the dataset easy to read while still covering upward and downward branches, which is useful for comparing different layout families on the same topology.

The example does not call setJsonData(). Instead, it takes the nodes and lines arrays from that literal and inserts them separately with addNodes() and addLines(). After the first doLayout(), it normalizes the viewport with moveToCenter() and zoomToFit(). In a real application, the same structure could represent an org chart, a service dependency map, an approval chain, or a compact knowledge graph where the business data stays stable but the presentation mode changes.

How relation-graph Is Used

index.tsx wraps the demo in RGProvider, and MyGraph.tsx uses RGHooks.useGraphInstance() as the control surface for all runtime behavior. The initial graph options disable debug mode, set a rectangular default node shape, define a baseline line color and width, hide the built-in toolbar, and start with a tree layout. That baseline matters because the example repeatedly resets current nodes and lines back to known defaults before applying a selected preset.

The graph stays mounted in <RelationGraph> while all changes are applied through instance APIs. Initial loading uses addNodes(), addLines(), doLayout(), moveToCenter(), and zoomToFit(). Theme changes use getNodes(), getLines(), updateNode(), and updateLine() to rewrite className values on existing items. Layout changes use updateOptions() to replace the active layout object, sleep(100) to wait for node-size rendering, doLayout() to recompute positions, optional post-layout callbacks for io-tree routing, and zoomToFit() to refit the result.

The overlay UI is built with RGSlotOnView, not the default toolbar. Inside that slot, the demo mounts RGMiniView and RGMiniToolBar, then places two custom selector components inside the toolbar. MyThemeSelect exposes the theme catalog. MyLayoutSelect exposes the layout catalog and uses remote preview PNGs for each preset card. The shared DraggableWindow component adds a floating description box that can open a settings panel. That panel uses RGHooks.useGraphStore() to read the current drag and wheel modes and uses setOptions() to update them.

Styling is implemented through SCSS overrides in my-relation-graph.scss. The demo does not swap an outer wrapper theme only. Instead, each selected theme assigns a class such as my-theme-1 or my-theme-7 to the live node and line items, and the SCSS rules then recolor node backgrounds, borders, text, line strokes, label backgrounds, and checked-state halos. The example is not a graph editor: it does not add or remove structure at runtime. The only editing-related call is setEditingNodes([]) on canvas click so any active UI state is dismissed cleanly.

Key Interactions

  • Clicking the palette button opens a popover of theme cards. Selecting a card immediately rewrites the className of every current node and line, so the graph changes skin without reloading data.
  • Clicking the network button opens a grid of layout cards. Each card represents a preset that bundles layout options with node and line defaults, and selecting one triggers a full relayout flow on the already loaded graph.
  • The layout selection handler also writes the chosen layout label to the clipboard before starting the relayout. The code does this directly, even though the UI does not explain the behavior.
  • Clicking empty canvas clears checked items, clears the editing-node list, and closes whichever popover was active.
  • The helper window can be dragged, minimized, and opened into a settings panel. That panel changes wheel behavior, changes canvas drag behavior, and downloads an image by preparing the graph for capture, rasterizing the DOM, and restoring the graph state afterward.
  • Node and line click handlers exist, but they only log to the console. They are not part of the main user-facing workflow.

Key Code Fragments

This fragment shows that the example keeps a small graph dataset inline and loads it into the live instance once.

const network2JsonData: RGJsonData = {
    rootId: 'g2-a',
    nodes: [
        { id: 'g2-a', text: 'Root' }, { id: 'g2-R-b', text: 'R-b' },
        // ...
    ],
    lines: [
        { id: 'g2-l-1', from: 'g2-R-b', to: 'g2-a' },
        // ...
    ]
};
graphInstance.addNodes(network2JsonData.nodes);
graphInstance.addLines(network2JsonData.lines);
await graphInstance.doLayout();

This fragment shows that theme switching is a batch update over already rendered graph items, not a dataset rebuild.

const onChangeNextworkStyles = (themeLabel: string) => {
    const themeInfo = exampleThemes.find(n => n.label === themeLabel);
    if (!themeInfo) return;
    const groupNodes = graphInstance.getNodes();
    const groupLines = graphInstance.getLines();
    groupNodes.forEach(n => { graphInstance.updateNode(n, { className: themeInfo.themeOptions.itemsClassName }); });
    groupLines.forEach(l => { graphInstance.updateLine(l, { className: themeInfo.themeOptions.itemsClassName }); });
};

This fragment shows that a layout preset is a richer object than a bare layoutName, because it bundles geometry defaults and optional post-layout logic.

{
    label: 'IO-Tree-1',
    layoutOptions: {
        layoutName: 'io-tree',
        from: 'left',
        treeNodeGapH: 50,
        treeNodeGapV: 10
    },
    defaultOptions: {
        node: { nodeShape: RGNodeShape.circle, width: 50, height: 50 },
        line: { lineShape: RGLineShape.StandardOrthogonal, fromJunctionPoint: RGJunctionPoint.right, toJunctionPoint: RGJunctionPoint.top }
    },
    afterLayoutCallback: (graphInstance, nodes) => {
        updateLinesForIOTree(graphInstance, nodes, 'left')
    }
}

This fragment shows how the graph view is extended with a minimap and custom toolbar controls through RGSlotOnView.

<RGSlotOnView>
    <RGMiniView />
    <RGMiniToolBar>
        <MyThemeSelect
            actived={acitveButton === 'theme'}
            onClickButton={() => { setAcitveButton('theme'); }}
            onClickTheme={(clickedLabel) => {
                onChangeNextworkStyles(clickedLabel);
            }} />
        <MyLayoutSelect
            actived={acitveButton === 'layout'}
            // ...
        />
    </RGMiniToolBar>
</RGSlotOnView>

What Makes This Example Distinct

Compared with nearby examples, this demo stands out because it combines two kinds of runtime switching on one mounted graph: item-level theme switching and multi-family layout switching. The comparison analysis identifies css-theme as the closest theming neighbor, but that example keeps one stable layout and concentrates on skin changes. Here, theme switching is only half of the lesson.

The strongest difference from switch-layout is preset richness. This example does not just rotate among layout objects. Each preset can also change node shape, node size, line shape, junction points, and, for io-tree variants, attach an afterLayoutCallback that rewrites connector anchors after layout. That makes it a better starting point when a “layout preset” in product terms actually means a coordinated visual package rather than a new layoutName alone.

The comparison with io-tree-layout is also useful. That neighbor is the more focused routing demo, while this example uses io-tree routing as one advanced option inside a broader gallery. The rare part is the combination: a compact mini-toolbar, seven named themes plus None, a layout catalog spanning center, tree, folder, circle, and io-tree, live node and line normalization, and a minimap-driven viewer shell. The helper window and export utilities are not unique by themselves, but in this example they support a more complete presentation-comparison workflow.

Where Else This Pattern Applies

  • A product can offer “view presets” for the same relationship data, such as analyst mode, executive mode, compact mode, and routing-focused mode, without rebuilding the graph.
  • An internal tool can let users compare org charts, dependency maps, or lineage graphs under different layout families before choosing a default presentation.
  • A design system for graph-based applications can package layout, node geometry, and connector style into reusable presets instead of treating them as unrelated options.
  • A reporting workflow can reuse the same live graph for both interactive inspection and exported images, with canvas settings and capture logic exposed in a shared overlay panel.