Canvas Coordinate Ruler
A full-screen relation-graph viewer that overlays top and left SVG rulers on the visible canvas. It composes a center cluster and a right-side tree with separate layout passes, then converts viewport coordinates back into canvas coordinates so the ruler stays accurate while users pan or zoom.
Canvas Coordinate Ruler for a Mixed Graph Workspace
What This Example Builds
This example builds a full-screen relation-graph viewer with two extra workspace aids layered over it: drafting-style rulers along the top and left edges, and a floating helper window above the canvas. The graph itself combines two different structures on one surface: a circular center cluster and a separate tree placed to its right.
Users can pan and zoom the graph, switch canvas wheel and drag modes from the helper panel, move or minimize the floating window, and export the current graph as an image. The main point of interest is not the graph data alone, but the ruler overlay that keeps showing canvas coordinates for the currently visible area.
How the Data Is Organized
The graph data is not loaded through a single setJsonData call. Instead, MyGraph.tsx declares two inline RGJsonData objects: treeJsonData with root a, and centerJsonData with root 2. Both datasets are inserted into the same graph instance with separate addNodes and addLines calls.
Before layout runs, the code mutates both datasets. Tree nodes are made semi-transparent with transparent fills so the line geometry stays visible. Center-cluster nodes are restyled into 80x80 circles with smaller text, and the center-cluster links are switched to straight border-attached lines. After those mutations, the example places the two connected components manually rather than relying on one global automatic layout.
In a real application, this same data shape could represent a main equipment hub plus a dependency tree, a process core plus downstream branches, or a central entity with one attached hierarchy. The inline demo labels are placeholders, but the pattern transfers directly to business IDs, names, and relationship labels.
How relation-graph Is Used
index.tsx wraps the demo in RGProvider, and MyGraph.tsx uses RGHooks.useGraphInstance() to work directly with the active graph instance. The graph starts with layoutName: 'fixed', which is important because the example wants to compose multiple layouts manually on one canvas instead of letting a single layout own the whole scene.
The composition flow is:
- insert the tree dataset;
- restyle and insert the center dataset;
- create a
centerlayout for the center cluster; - measure that cluster with
getNodesRectBox(...); - shift the tree root to the right;
- create a left-to-right
treelayout for the second cluster; - finish with
moveToCenter()andzoomToFit().
The ruler itself is mounted with RGSlotOnView, not inside node content and not in a canvas-wide background slot. MyCanvasCaliper.tsx uses getViewBoundingClientRect() together with getCanvasXyByViewXy(...) to compute the visible canvas range, then derives SVG tick marks and labels from that range plus options.canvasZoom from RGHooks.useGraphStore(). That is why the ruler stays aligned with the current viewport and changes density as zoom changes.
The floating helper window is a local subcomponent rather than a relation-graph built-in. It still uses graph APIs heavily: setOptions(...) switches drag and wheel modes, prepareForImageGeneration() and restoreAfterImageGeneration() wrap the export flow, and getOptions() provides the background color fallback used during screenshot capture. Styling is lightweight: my-relation-graph.scss mostly leaves relation-graph selectors open, while MyCanvasCaliper.scss positions the overlay absolutely with pointer-events: none so it does not block graph interaction.
Key Interactions
- Panning and zooming change the ruler immediately because the overlay recalculates visible canvas coordinates and tick intervals from the current viewport and zoom state.
- The floating helper window can be dragged by its title bar, minimized, and switched into a settings panel without leaving the graph view.
- The settings panel changes wheel behavior between scroll, zoom, and none, and changes canvas drag behavior between selection, move, and none.
- The
Download Imageaction exports the current graph view through the shared screenshot helper after relation-graph prepares the canvas DOM for image generation.
Key Code Fragments
This fragment shows that the outer graph is intentionally kept in fixed mode so separate layouts can be composed manually:
const graphOptions: RGOptions = {
defaultNodeBorderWidth: 1,
defaultNodeWidth: 170,
defaultNodeHeight: 40,
toolBarDirection: 'h',
toolBarPositionH: 'right',
toolBarPositionV: 'bottom',
defaultLineShape: RGLineShape.StandardCurve,
defaultJunctionPoint: RGJunctionPoint.lr,
defaultLineTextOnPath: true,
lineTextMaxLength: 10,
defaultLineColor: '#000000',
layout: {
layoutName: 'fixed'
}
};
This fragment shows that the center dataset is restyled before insertion, so the mixed scene has visibly different geometry and junction behavior:
centerJsonData.nodes.forEach(node => {
node.opacity = 0.5;
node.nodeShape = RGNodeShape.circle;
node.borderWidth = 1;
node.width = 80;
node.height = 80;
node.fontSize = 10;
node.color = 'transparent';
});
centerJsonData.lines.forEach(line => {
line.lineShape = RGLineShape.StandardStraight;
line.fromJunctionPoint = RGJunctionPoint.border;
line.toJunctionPoint = RGJunctionPoint.border;
});
This fragment shows the two-stage layout composition: place the center cluster, measure it, then place the tree to the right:
const myCenterLayout = graphInstance.createLayout({
layoutName: 'center'
});
const centerNodes = graphInstance.getNetworkNodesByNode(centerRootNode);
myCenterLayout.placeNodes(centerNodes, centerRootNode);
const nodesRectInfo = graphInstance.getNodesRectBox(centerNodes);
treeRootNode.x = nodesRectInfo.maxX + 100;
const myTreeLayout = graphInstance.createLayout({
layoutName: 'tree',
from: 'left',
treeNodeGapH: 200,
treeNodeGapV: 30
});
This fragment shows the core ruler technique: convert viewport coordinates back into canvas coordinates, then scale the marks by the current zoom:
const graphViewBox = graphInstance.getViewBoundingClientRect();
const visibleAreaStart = graphInstance.getCanvasXyByViewXy({
x: 0,
y: 0
});
const visibleAreaEnd = graphInstance.getCanvasXyByViewXy({
x: graphViewBox.width,
y: graphViewBox.height
});
const visibleArea = {
x: visibleAreaStart.x,
y: visibleAreaStart.y,
width: visibleAreaEnd.x - visibleAreaStart.x,
height: visibleAreaEnd.y - visibleAreaStart.y
};
const scale = options.canvasZoom! / 100 || 1;
This fragment shows that the floating panel changes relation-graph runtime behavior and exposes image export as a workspace utility:
<SettingRow
label="Wheel Event:"
options={[
{ label: 'Scroll', value: 'scroll' },
{ label: 'Zoom', value: 'zoom' },
{ label: 'None', value: 'none' },
]}
value={wheelMode}
onChange={(newValue: string) => { graphInstance.setOptions({ wheelEventAction: newValue }); }}
/>
What Makes This Example Distinct
Its most important distinction is that the mixed-layout graph is not the final lesson by itself. The comparison data shows that multiple-layout-mixing-in-single-canvas already demonstrates a very similar center-plus-tree composition. What this example adds is a measurement-oriented overlay in RGSlotOnView, driven by viewport bounds, viewport-to-canvas conversion, and live zoom state.
It is also different from graph-offset, which uses a large background-style grid to explain canvas offsets and recentering. Here the overlay is tied to the current visible area rather than to a static graph-space grid, so the rulers behave like an inspection tool for whatever part of the scene the user is looking at.
Within other viewer-style examples that reuse the same floating helper window, this example puts the extra UI to analytical use. The panel and export flow are shared infrastructure, but the notable combination here is a manually composed center-plus-tree scene, transparent nodes that keep line geometry legible, and top-and-left rulers that turn the canvas into a coordinate-aware workspace.
Where Else This Pattern Applies
This pattern is useful when a graph needs to behave like a workspace instead of a plain viewer. Typical transfer cases include engineering diagrams, factory layouts, equipment topology review, map-like planning screens, and any graph where users need to inspect approximate canvas positions while navigating.
It is also a good starting point for tools that need lightweight measurement aids without becoming full editors. Teams could extend the same approach into snapping guides, selection rulers, print-safe margins, viewport annotations, or drag-time alignment helpers while keeping the actual graph model unchanged.