JavaScript is required

Force Layout Performance Mode Stress Lab

This example demonstrates a full-screen relation-graph force-layout stress playground that regenerates synthetic datasets from 100 to 10000 nodes and edges, keeps `performanceMode` enabled, and exposes six live force-coefficient sliders plus a continuous-versus-fixed layout switch. It also keeps a custom avatar-style node slot, a draggable utility window, a minimap, and shared canvas settings active so teams can evaluate richer rendering behavior under heavier force-directed loads.

Force Layout Performance Stress Lab with Live Runtime Controls

What This Example Builds

This example builds a full-screen force-layout stress playground around relation-graph’s built-in force layout. The canvas renders a dense network of circular avatar nodes with colored caption pills, a floating draggable control window, and a bottom-right minimap for overview navigation. Users can switch across six graph-size presets, tune six force coefficients while the graph is running, and choose whether the solver runs continuously or stops after a fixed number of iterations.

The main point is to observe how relation-graph behaves under heavier synthetic force-layout workloads while a richer slot-based node renderer remains enabled. It is a scale-and-tuning lab rather than a domain-specific viewer or an editing example.

How the Data Is Organized

The example starts from one local RGJsonData seed in example-data.ts. That seed defines rootId: 'a', a branching set of base nodes, and matching base lines. Before the graph is loaded, generateTestJsonData(testDataSize) deep-clones the seed, expands every node whose id contains -, and appends extra descendants and extra lines according to the selected scale factor.

The expansion rule changes at larger sizes. For values up to 30, the generator adds one extra fan-out loop per eligible node. For values above 30, it switches to a two-level expansion based on Math.sqrt(testDataSize) so the graph can grow to much larger sizes without hand-authoring the data. After expansion, every node receives a random image URL and a random color, and every line receives a generated id and a random color.

In a production system, the same structure could stand in for progressively expanded relationship neighborhoods, dependency radii, customer-account tiers, or infrastructure blast-radius views. The important pattern is that the page regenerates disposable test data on demand instead of editing one persistent business graph.

How relation-graph Is Used

index.tsx wraps the example with RGProvider, and MyGraph.tsx retrieves the live graph instance through RGHooks.useGraphInstance(). The graph options enable performanceMode, set the built-in layout to force, choose border-anchored straight lines, and use circular 80x80 nodes with no default border. The initial force run is configured with maxLayoutTimes: Number.MAX_SAFE_INTEGER, so the page starts in a continuous layout mode.

The example uses relation-graph imperatively in three different ways. Dataset changes trigger a hard reload path: stopAutoLayout(), clearGraph(), sleep(500), setJsonData(...), moveToCenter(), and setZoom(30). Force-coefficient changes use a lighter path: the code reads graphInstance.layoutor, casts it to RGLayouts.ForceLayout, and calls updateOptions(myForceLayoutOptions) so the running layouter is mutated in place. Iteration-mode changes use a third path: updateOptions({ layout }), then doLayout(), then recenter and reset zoom.

Slots are a major part of the visual result. RGSlotOnView mounts RGMiniView in the bottom-right corner, which gives the page an overview map without replacing the main canvas. RGSlotOnNode renders a custom node template that uses node.data.pic as the circular background image and node.color as the caption-pill background. The stylesheet in my-relation-graph.scss shapes that slot into a full circular photo node and overrides checked-state styles so selected nodes and lines receive a magenta highlight.

The floating utility shell comes from the shared DraggableWindow.tsx component, and the segmented selectors come from SimpleUISelect.tsx. The settings overlay in the shared window uses graph-instance APIs to switch wheel behavior, switch canvas drag behavior, and export the current graph image through domToImageByModernScreenshot.ts. Those tools are inherited shared utilities, not force-specific code unique to this example.

Key Interactions

  • Switching the dataset preset regenerates the graph at a new scale, reloads the data into relation-graph, recenters the viewport, and resets zoom to 30.
  • Moving any of the six range inputs updates the active ForceLayout instance at runtime, so users can observe the same graph under different global solver coefficients without remounting the component.
  • Toggling between Layout Forever and Layout Fixed Times(...) changes the force solver’s duration policy. When fixed mode is active, an extra range slider appears so the iteration cap can be adjusted.
  • The floating control window can be dragged, minimized, restored, and switched into the shared canvas-settings panel.
  • The shared settings panel changes wheel behavior, changes canvas drag behavior, and exports the current graph as an image.

Key Code Fragments

This block proves the page is configured as a built-in force-layout example in performanceMode, with circular node geometry and straight border-anchored lines.

const graphOptions: RGOptions = {
    debug: true,
    defaultNodeBorderWidth: 0,
    defaultNodeShape: RGNodeShape.circle,
    defaultNodeWidth: 80,
    defaultNodeHeight: 80,
    defaultLineColor: 'rgb(0,139,189)',
    defaultNodeColor: 'rgb(0,139,189)',
    defaultLineShape: RGLineShape.StandardStraight,
    toolBarPositionH: 'center',
    layout: {
        layoutName: 'force',
        maxLayoutTimes: Number.MAX_SAFE_INTEGER
    },
    defaultJunctionPoint: RGJunctionPoint.border,
    performanceMode: true
};

This block shows the reload path used when the dataset size changes.

const initializeGraph = async () => {
    const myJsonData = generateTestJsonData(testDataSize);
    graphInstance.stopAutoLayout();
    graphInstance.clearGraph();
    await graphInstance.sleep(500);
    await graphInstance.setJsonData(myJsonData);
    graphInstance.moveToCenter();
    graphInstance.setZoom(30);
};

This block shows how the example updates the running ForceLayout instance instead of rebuilding the whole graph for every slider change.

const updateMyOptions = async () => {
    const forceLayout = graphInstance.layoutor as InstanceType<typeof RGLayouts.ForceLayout>;
    if (forceLayout) {
        forceLayout.updateOptions(myForceLayoutOptions);
    }
};

This block proves the page separates continuous-versus-fixed layout duration from the live coefficient tuning path.

const restartForceLayout = async () => {
    graphInstance.updateOptions({
        layout: {
            layoutName: 'force',
            maxLayoutTimes: layoutForever ? Number.MAX_SAFE_INTEGER : maxLayoutTimes
        }
    });
    await graphInstance.doLayout();
    graphInstance.moveToCenter();
    graphInstance.setZoom(30);
};

This block shows that dataset scale is synthesized by expanding a seeded branching graph and then randomizing node and line presentation data.

for (const node of data.nodes) {
    if (node.id.includes('-')) {
        if (testDataSize > 30) {
            const level1 = Math.sqrt(testDataSize);
            for (let a = 1; a <= level1; a++) {
                const aNodeId = node.id + '-' + a;
                newNodes.push({ id: aNodeId, text: aNodeId });
                newLines.push({ from: node.id, to: aNodeId });
            }
        } else {
            for (let i = 1; i <= testDataSize; i++) {
                newNodes.push({ id: node.id + '-' + i, text: node.text + '-' + i });
                newLines.push({ from: node.id, to: node.id + '-' + i });
            }
        }
    }
}

This block proves the final graph keeps both a minimap and a custom node slot active during the stress test.

<RelationGraph options={graphOptions} onNodeClick={onNodeClick} onLineClick={onLineClick}>
    <RGSlotOnView>
        <RGMiniView width="300px" height="150px" position="br" />
    </RGSlotOnView>
    <RGSlotOnNode>
        {(nodeSlotProps: RGNodeSlotProps) => {
            return <MyNodeSlot node={nodeSlotProps.node} />;
        }}
    </RGSlotOnNode>
</RelationGraph>

What Makes This Example Distinct

According to the prepared comparison data, this example is distinct because it combines several patterns that are usually separated across other demos: relation-graph’s built-in force layout, performanceMode, six dataset-size presets from 100 to 10000 nodes and edges, six live force-coefficient sliders, a draggable utility overlay, a minimap, and a custom avatar-style node slot that stays enabled as the graph scales upward. That combination makes it a stronger reference for force-layout stress testing than a normal force-layout showcase.

Compared with performance-test-tree-layout, this example puts its UI budget into force-solver coefficients and layout duration policy rather than tree direction, node spacing, or line-routing controls. Compared with layout-force-options, it keeps the same general runtime force-tuning idea but pushes it into explicit performance-mode stress with much larger regenerated datasets, a minimap, and a richer node renderer. Compared with layout-force, it is not the minimal built-in-force baseline; it goes further by stressing scale presets and slot-based rendering under load.

The comparison data also narrows what should not be claimed. This is not the only built-in force example, not the only demo that updates a running ForceLayout, and not evidence of a formal benchmark suite because the code does not collect FPS, timing, or memory metrics.

Where Else This Pattern Applies

This pattern applies to internal labs where teams need to judge how far a built-in graph engine can be pushed before they invest in a custom layout engine. Suitable examples include knowledge-graph exploration tools, dependency-network viewers, service-topology workbenches, and relationship-analysis consoles where graph radius can change sharply between use cases.

It also transfers well to pre-production diagnostics. A team can swap the synthetic generator for sampled production neighborhoods, keep the same runtime force controls, keep the same minimap and screenshot tools, and use the page to compare layout stability and renderer cost across different graph densities. The key reusable idea is to separate data regeneration, live layouter mutation, and rerun policy so each control has a predictable effect.