Database Foreign Key Diagram
A fixed-layout database schema viewer that renders each table as a custom card and connects specific column rows with curved fake lines. It preprocesses table, column, and relation metadata into slotted node content plus `RGConnectTarget` endpoint ids, then layers in a draggable helper window for canvas settings and image export.
Column-Level Foreign Key Lines in Fixed Database Table Cards
What This Example Builds
This example builds a fixed-layout database schema viewer inside a full-height relation-graph canvas. The screen shows six orange table cards, each card lists columns and data types, and curved purple or blue relationship lines connect specific field rows instead of connecting only to whole nodes.
Users can inspect the prepared schema, drag or minimize the floating helper window, open a settings panel to change wheel and canvas-drag behavior, and export the current graph as an image. The main technical point is the column-level wiring: custom table markup is not just decorative, it gives relation-graph exact DOM targets for foreign-key lines.
How the Data Is Organized
The dataset is declared inline inside initializeGraph() as three arrays: tables, tableCols, and columnRelations. tables defines the table ids, visible labels, and authored x and y coordinates. tableCols holds the per-table field list, and columnRelations describes source column, target column, and relation type.
Before setJsonData(...), the example performs a small preprocessing pass. It maps tables into graph nodes with data.columns, then maps columnRelations into fakeLines whose from and to values point at generated endpoint ids such as col-name-SYS_USER-dept_id. Ordinary lines stay empty because the relationships are attached to DOM elements rendered inside each node slot, not to default node anchors.
In a real application, the same structure could come from database metadata, ORM model definitions, API schema registries, field-mapping catalogs, or data lineage records. The fixed coordinates could remain authored for a curated reference diagram or be generated elsewhere before the final payload is passed into relation-graph.
How relation-graph Is Used
index.tsx wraps the example in RGProvider, and MyGraph.tsx uses RGHooks.useGraphInstance() to load the graph after mount. The graph options keep relation-graph in layoutName: 'fixed', set defaultJunctionPoint: RGJunctionPoint.border, remove the default node border, set an orange default node color, and define a custom triangular line marker. After loading the JSON, the example calls moveToCenter() and zoomToFit() so the prepared schema fills the viewport immediately.
The main customization happens through RGSlotOnNode. Each node becomes a 300-pixel table card with a header and an HTML table of fields. Inside each column-name cell, RGConnectTarget registers a targetId derived from the table id and column name. The fakeLines payload then connects to those ids, which is what makes the foreign-key curves land on exact field rows.
The example does not implement editing, authoring, or runtime graph mutation. Node and line click handlers are present, but they only log the clicked objects. Additional utility behavior comes from the shared DraggableWindow and CanvasSettingsPanel: the settings panel reads current drag and wheel modes with RGHooks.useGraphStore(), updates them with graphInstance.setOptions(...), and uses prepareForImageGeneration() plus restoreAfterImageGeneration() to export the graph as an image. The local stylesheet adds the tiled background, checked-state emphasis, and the orange table styling.
Key Interactions
- The graph initializes on mount, then recenters and fits the authored schema so the viewer opens on a complete diagram.
- The floating helper window can be dragged by its title bar and minimized, which keeps the description and tools movable instead of fixed over one part of the canvas.
- Opening the settings panel exposes live wheel-mode and drag-mode switches, and each choice updates the mounted relation-graph instance immediately.
- The download action prepares the graph for capture, renders the canvas DOM into an image blob, downloads it, and restores the graph state afterward.
- Clicking a node or a relationship line only logs the related object, so the example remains a read-only inspection view rather than a schema editor.
Key Code Fragments
This fragment shows how table metadata is converted into fixed-position graph nodes with per-table column data attached to node.data:
const graphNodes = tables.map(table => {
const { tableName, tableComents, x, y } = table;
return {
id: tableName,
text: tableComents,
x,
y,
nodeShape: RGNodeShape.rect,
nodeShape: RGNodeShape.rect,
data: {
columns: tableCols.filter(col => col.tableName === table.tableName)
}
};
});
This fragment shows the crucial preprocessing step: each foreign-key definition becomes a curved fakeLine whose endpoints are column-level target ids:
const myFakeLines = columnRelations.map((relation, index) => {
return {
id: `rel-line-${index}`,
from: `col-name-${relation.sourceTableName}-${relation.sourceColumnName}`,
to: `col-name-${relation.targetTableName}-${relation.targetColumnName}`,
color: relation.type === 'ONE_TO_ONE' ? 'rgba(29,169,245,0.76)' : 'rgba(159,23,227,0.65)',
text: '',
fromJunctionPoint: RGJunctionPoint.left,
toJunctionPoint: RGJunctionPoint.lr,
lineShape: RGLineShape.StandardCurve,
lineWidth: 3
};
});
This fragment shows that the loaded payload intentionally leaves ordinary lines empty and relies on fakeLines instead:
const myJsonData: RGJsonData = {
nodes: graphNodes,
lines: [],
fakeLines: myFakeLines
};
await graphInstance.setJsonData(myJsonData);
graphInstance.moveToCenter();
graphInstance.zoomToFit();
This fragment shows the fixed-layout graph configuration and the custom line marker used by the schema viewer:
const graphOptions: RGOptions = {
debug: false,
defaultJunctionPoint: RGJunctionPoint.border,
defaultNodeColor: '#f39930',
defaultNodeBorderWidth: 0,
defaultLineMarker: {
markerWidth: 20,
markerHeight: 20,
refX: 3,
refY: 3,
viewBox: '0 0 6 6',
data: "M 0 0, V 6, L 4 3, Z"
},
layout: {
layoutName: 'fixed'
}
};
This fragment shows how the custom node slot renders table rows and registers each column name as a line endpoint:
<RGConnectTarget
targetId={`col-name-${node.id}-${column.columnName}`}
junctionPoint={RGJunctionPoint.lr}
>
<div className="w-fit px-2">{column.columnName}</div>
</RGConnectTarget>
This fragment shows the runtime settings pattern: the shared panel reads current graph-store values and pushes updates through setOptions(...):
const { options } = RGHooks.useGraphStore();
const dragMode = options.dragEventAction;
const wheelMode = options.wheelEventAction;
<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 }); }}
/>
This fragment shows the export flow that prepares relation-graph for capture, renders the canvas DOM, and restores the graph afterward:
const canvasDom = await graphInstance.prepareForImageGeneration();
let graphBackgroundColor = graphInstance.getOptions().backgroundColor;
if (!graphBackgroundColor || graphBackgroundColor === 'transparent') {
graphBackgroundColor = '#ffffff';
}
const imageBlob = await domToImageByModernScreenshot(canvasDom, {
backgroundColor: graphBackgroundColor
});
await graphInstance.restoreAfterImageGeneration();
What Makes This Example Distinct
The comparison data makes the distinction clear. Compared with drag-and-wheel-event and layout-center, the floating settings panel is secondary here. Those neighbors are mainly about canvas-behavior tuning or runtime restyling, while this example uses the same shared utility shell to support a more specialized lesson: wiring schema relationships to exact rows inside custom node content.
Compared with node and use-d3-layout, the slotted HTML is structural rather than cosmetic. The custom markup exists so RGConnectTarget can expose per-column endpoints and fakeLines can land on those endpoints, not mainly to showcase skinning or survive an external relayout pass. Compared with adv-dynamic-data, the complexity is concentrated in one preprocessing step that turns table metadata into a fixed snapshot; there is no staged growth after mount.
The rare combination identified in the prepared analysis is the important takeaway: layoutName = 'fixed', repeated table-card node slots, column-level DOM endpoints, color-coded curved fakeLines, a tiled technical canvas, a legend, and shared export or canvas-settings utilities in one read-only viewer. That combination makes this example a strong starting point for schema-style diagrams where relationships must attach to embedded fields instead of whole nodes.
Where Else This Pattern Applies
This pattern transfers well to database documentation pages, entity-relationship viewers, and internal schema explorers where teams need a curated fixed arrangement instead of an automatic layout. The same preprocessing approach can also be adapted for API payload comparisons, ETL field mappings, permission-to-resource matrices, or data-lineage views where links must terminate on named rows inside a larger card.
It is also useful when a product needs a technical reference screen rather than an editor. A team can keep the row-level endpoint technique, swap in different metadata, and preserve the floating export or canvas-settings tools for analyst workspaces, architecture reviews, or support documentation snapshots.