Swap between JointJS, Cytoscape, D3, vis.js, Sigma, GoJS, XYFlow and 13 more — zero code changes.
Quick Start • Renderers • API • Examples • Companion Packages
Note —
dbrel-vizis purely a rendering frontend. It expects a JSON payload describing tables, rows, and computed relationships. Use one of our companion data packages (PHP or Node.js) to produce that payload, or generate it yourself.
- Why dbrel-viz?
- Features
- Screenshots
- Quick Start
- Installation
- Configuration
- Data Format
- API Reference
- Renderer Reference
- Writing a Custom Renderer
- Examples
- Architecture
- Companion Packages
- Browser Support
- Contributing
- License
Most graph-visualization tools lock you in. Pick D3 and you own the D3 learning curve forever. Pick Cytoscape and you're stuck if you hate its style system. dbrel-viz is the abstraction layer.
┌────────────────┐
│ Your Data │ (any source, any backend)
└───────┬────────┘
│ JSON payload
▼
┌────────────────────────────────────────────┐
│ DbRel shell.js (shared core) │
│ layout • pivots • distance focus • sidebar│
└────────────────────────────────────────────┘
│ common renderer interface
▼
┌────────┬──────────┬──────┬──────────┬─────┐
│JointJS │Cytoscape │D3.js │vis.js ...│ 20+ │
└────────┴──────────┴──────┴──────────┴─────┘
The same data renders everywhere. Your users pick the engine that fits their brain. You never rewrite.
- 20 renderers, one API — JointJS, Cytoscape, Sigma.js, vis.js, D3, GoJS, force-graph, VivaGraph, Springy, AntV G6, C3, dc.js, NVD3, p5.js, Raphael, Vega, maxGraph, React Diagrams, XYFlow, Recharts
- Live preview on hover — 1-second hover delay previews renderers in-place; click to confirm, move away to revert
- Distance-based focus fading — click any node, and the rest of the graph fades based on BFS hop distance
- Pivot system — re-center the view on any VPS host, switch, VLAN, server, asset or website master with a click
- Grouped / separate display modes — one node per table, or one node per row
- Shared smart layout — BFS + column bin-packing algorithm with golden-ratio aspect targeting
- Link styling by relationship type — direct FKs solid,
FIND_IN_SETdashed purple, cross-DB dotted orange - Auto color palettes per database — blue palette for primary, green for Kayako, orange for PowerDNS
- Custom table icons — 90+ built-in table icon mappings; fully customizable
- Breadcrumb pivot trail — visual path showing how you navigated from customer to current focal point
- Live scripts & CSS loader — lazy-loads each renderer's CDN deps only when you switch to it
- Zero build step — plain ES5, loads from
src/directly - Row detail modal — click any row to see all fields in a clean table
- Sidebar with counts — per-table row counts, hover to highlight in renderer
- Keyboard navigation — arrow keys + Enter to browse renderers
- Filter by database and relationship type — toolbar chips toggle visibility
- Zoom controls + fit-to-screen — every renderer implements the same zoom interface
- D3 version juggling — shell automatically swaps D3 v3/v5/v7 as needed between renderers
- Works inside AdminLTE 3, Bootstrap, or standalone —
<div id="db-rel-app">is all you need
Minimal HTML page using dbrel-viz with a static JSON payload:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>dbrel-viz demo</title>
<!-- jQuery + Bootstrap 4 (AdminLTE 3 compatible) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- dbrel-viz -->
<link rel="stylesheet" href="node_modules/@detain/dbrel-viz/src/core/styles.css">
<script>
// Override paths BEFORE loading the shell
window.DbRel = { paths: {
renderers: '/node_modules/@detain/dbrel-viz/src/renderers/',
rendererPrefix: '', rendererSuffix: '.js'
}};
</script>
</head>
<body>
<div id="db-rel-app">
<!-- toolbar, sidebar, paper will be populated by shell.js -->
<div id="db-rel-paper-wrap"><div id="db-rel-paper"></div></div>
<input id="db-rel-custid" type="hidden" value="12345">
</div>
<script src="node_modules/@detain/dbrel-viz/src/core/shell.js"></script>
<script src="node_modules/@detain/dbrel-viz/src/renderers/jointjs.js"></script>
<script>
// Load data from your own endpoint or just set it directly
fetch('/api/db-relationships?custid=12345')
.then(r => r.json())
.then(data => {
DbRel.data = data;
DbRel.renderers['jointjs'].render();
DbRel.updateSidebar();
});
</script>
</body>
</html>That's it. Now switch to Cytoscape by clicking the library dropdown — the renderer swaps with zero code changes.
npm install @detain/dbrel-viz
# or
yarn add @detain/dbrel-vizThe package ships its src/ folder directly — there is no build step. You can either:
- Serve
src/as static files (Express, nginx, etc.) and reference them from your HTML, or - Bundle via webpack/vite/rollup — each file is a classic script wrapped in an IIFE
<link rel="stylesheet"
href="https://unpkg.com/@detain/dbrel-viz/src/core/styles.css">
<script src="https://unpkg.com/@detain/dbrel-viz/src/core/shell.js"></script>
<script src="https://unpkg.com/@detain/dbrel-viz/src/renderers/jointjs.js"></script>const dbrel = require('@detain/dbrel-viz');
console.log(dbrel.paths.shell); // absolute path to shell.js
console.log(dbrel.paths.styles); // absolute path to styles.css
console.log(dbrel.rendererPath('d3')); // absolute path to the D3 renderer
console.log(dbrel.renderers); // list of all renderer keys
console.log(dbrel.version); // current package versionHandy for Express apps:
const express = require('express');
const dbrel = require('@detain/dbrel-viz');
const app = express();
app.use('/dbrel', express.static(require('path').dirname(dbrel.paths.shell) + '/..'));
// Now: /dbrel/core/shell.js, /dbrel/core/styles.css, /dbrel/renderers/*.jsOverride configuration before shell.js loads by setting window.DbRel early:
<script>
window.DbRel = {
paths: {
renderers: '/assets/js/', // where renderer files live
rendererPrefix: 'db_relationships_', // prepended to renderer key
rendererSuffix: '.js', // appended to renderer key
libIcons: '/assets/lib-icons/', // renderer logo images
tableIcons: '/assets/table-icons/', // per-table icon images
ajaxUrl: '/api/data', // where to fetch data on load
ajaxChoice: 'db_relationships_data' // `choice=` query param
}
};
</script>
<script src="/assets/dbrel-viz/core/shell.js"></script>| Path key | What it controls |
|---|---|
renderers |
Directory URL where renderer JS files are served from |
rendererPrefix |
String prepended to each renderer key when building its URL |
rendererSuffix |
String appended to each renderer key (usually .js) |
libIcons |
Directory URL for the library logo icons used in the dropdown |
tableIcons |
Directory URL for the per-table icon PNGs used in node headers |
ajaxUrl |
Endpoint queried by DbRel.loadData(custid) |
ajaxChoice |
Query-string choice= value sent with data requests |
The final URL a renderer loads from is:
<paths.renderers><paths.rendererPrefix><key><paths.rendererSuffix>
So cytoscape becomes /js/db_relationships_cytoscape.js with the defaults, or /assets/dbrel-viz/src/renderers/cytoscape.js if you point renderers at the source folder and set prefix/suffix to empty.
The library consumes a single JSON payload, assigned to DbRel.data. Shape:
{
"custid": 12345,
"tables": {
"my.accounts": {
"rows": [ { "account_id": 12345, "account_lid": "demo", ... } ],
"columns": ["account_id", "account_lid", ...],
"total": 1,
"truncated": false
},
"my.vps": {
"rows": [ { "vps_id": 99, "vps_hostname": "host.example.com", ... } ],
"columns": ["vps_id", "vps_hostname", ...],
"total": 3,
"truncated": false
}
},
"relationships": [
{
"source": "my.accounts",
"target": "my.vps",
"source_field": "account_id",
"target_field": "vps_custid",
"type": "direct",
"cardinality": "1:N",
"label": "Account → VPS",
"matches": [ [0, [0, 1, 2]] ]
}
],
"metadata": {
"databases": ["my", "kayako_v4", "pdns"],
"table_count": 14,
"total_rows": 42,
"relationship_count": 9,
"query_time_ms": 127.4,
"custid": 12345,
"pivot_table": null,
"pivot_id": null
},
"prefixes": { "accounts": "account_", "vps": "vps_" },
"primaryKeys": { "accounts": "account_id", "vps": "vps_id" },
"hiddenFields": ["password", "api_token"]
}Field-by-field reference (click to expand)
| Key | Type | Description |
|---|---|---|
custid |
number |
Customer ID (echoed in metadata) |
tables["db.name"].rows |
object[] |
Row objects (keys are column names) |
tables["db.name"].columns |
string[] |
Ordered list of column names |
tables["db.name"].total |
number |
Total matching rows before any limit |
tables["db.name"].truncated |
bool |
Whether rows was cut short |
relationships[].source |
string |
"db.table" key of the source |
relationships[].target |
string |
"db.table" key of the target |
relationships[].source_field |
string |
Column in source holding the reference |
relationships[].target_field |
string |
Column in target being referenced |
relationships[].type |
string |
direct | find_in_set | cross_db |
relationships[].cardinality |
string |
1:1 | 1:N | N:1 | N:M |
relationships[].label |
string |
Human-readable label shown in tooltip |
relationships[].matches |
array |
[[sourceRowIdx, [targetRowIdxs]], …] |
prefixes[table] |
string |
Column prefix stripped for display |
primaryKeys[table] |
string |
PK column used to label nodes |
hiddenFields |
string[] |
Columns never shown anywhere |
Everything lives on the global DbRel namespace (created by core/shell.js).
| Property | Type | Description |
|---|---|---|
DbRel.data |
object | null |
The current payload (see Data Format) |
DbRel.displayMode |
"separate" | "grouped" |
One node per row vs. one node per table |
DbRel.showFullContent |
bool |
Whether to render full cell values instead of truncated |
DbRel.activeRendererKey |
string | null |
Key of the currently active renderer |
DbRel.renderers |
object |
Registry of all registered renderer instances |
DbRel.pivot |
object | null |
Current pivot info: { table, id, tableKey, idField, label } |
DbRel.paths |
object |
Configured paths (see Configuration) |
DbRel.RENDERERS |
object |
Manifest of all available renderers (name, icon, CDN URLs, category) |
DbRel.PIVOT_TABLES |
object |
Tables that can serve as a pivot focal point |
DbRel.DB_COLORS |
object |
Per-database color scheme |
DbRel.TABLE_PALETTES |
object |
Per-database color palettes for individual tables |
DbRel.LINK_STYLES |
object |
Stroke styles per relationship type |
DbRel.TABLE_ICONS |
object |
Per-table icon image map |
DbRel.loadData(custid); // fetch via AJAX, auto-renders
DbRel.pivotTo(tableKey, rowIndex); // re-center on a specific row
DbRel.pivotReset(); // back to the account-centric view
DbRel.loadPivotDirect(table, id, fallbackCustid); // direct-jump without custidDbRel.switchRenderer('cytoscape'); // swap to a different renderer
DbRel.registerRenderer(key, rendererObj); // register a custom renderer
DbRel.updateSidebar(); // refresh the sidebar panel
DbRel.resetTableColors(); // clear the auto-color cacheDbRel.getNodeHeader(tableKey, rowIndex); // e.g. "accounts 12345"
DbRel.getNodeLines(tableKey, rowIndex); // array of "field: value" lines
DbRel.computeNodeSize(header, lines); // { w, h } for layout
DbRel.getGroupedLines(tableKey); // ASCII-art table for grouped mode
DbRel.computeGroupedNodeSize(tableName, lines);// Shared BFS + column bin-packing layout, respecting aspect ratio targets
const positions = DbRel.computeLayout(containerWidth, containerHeight);
// Returns: { [nodeId]: { x, y, w, h } }// BFS distance from a focused node to every other node
const distances = DbRel.computeNodeDistances(focusNodeId);
// { nodeId: 0|1|2|…|Infinity }
DbRel.distanceToOpacity(distance); // 1.0 | 0.6 | 0.35 | 0.12DbRel.getTableColor(tableKey); // { header, bg, border }
DbRel.fmtVal(value); // smart-truncate for display
DbRel.pickDisplayColumns(columns, tableName);
DbRel.padRight(str, len);
DbRel.getPrimaryKey(tableName);
DbRel.shortenColName(col, tableName);
DbRel.getTableIconHtml(tableName); // '<img class="…"> ' or ''
DbRel.getTableIconInfo(tableName); // { type: 'img', src } or nullDbRel.showTooltip(html, x, y);
DbRel.hideTooltip();
DbRel.getLinkTooltipHtml(relData);
DbRel.showRowModal(tableKey, rowIndex);DbRel.getPivotConfig(tableName); // { idField, label } | null
DbRel.getNodePivotInfo(tableKey, rowIndex); // { table, id, tableKey, idField, label }DbRel.getDbFilters(); // { my: true, kayako_v4: false, pdns: true }
DbRel.getTypeFilters(); // { direct: true, find_in_set: true, cross_db: false }DbRel.loadScript(url); // returns a Promise; caches by URL
DbRel.loadCSS(url); // returns a Promise; caches by URLEvery renderer implements the same interface — the shell talks to any of them identically.
| Key | Library | Category | License | Unique Strength |
|---|---|---|---|---|
jointjs |
JointJS | graph | MPL-2.0 | SVG, orthogonal link routing, custom shapes |
cytoscape |
Cytoscape.js | graph | MIT | Rich selector engine, built for biology workloads |
sigma |
Sigma.js | graph | MIT | WebGL, handles huge graphs |
visjs |
vis.js | graph | Apache-2.0 | Physics simulation, timeline friendly |
d3 |
D3.js | graph | BSD-3 | Force layout, custom everything |
gojs |
GoJS | graph | Commercial | Polished diagrams, flowchart-grade layouts |
forcegraph |
force-graph | graph | MIT | Canvas force layout, buttery-smooth |
vivagraph |
VivaGraph | graph | MIT | WebGL, layout algorithm library |
springy |
Springy | graph | MIT | Tiny (~4KB), minimal spring layout |
g6 |
AntV G6 | graph | MIT | Rich built-in behaviors, enterprise-focused |
c3 |
C3.js | chart | MIT | D3 wrapper, clean chart defaults |
dcjs |
dc.js | chart | Apache-2.0 | Dimensional crossfilter charts |
nvd3 |
NVD3 | chart | Apache-2.0 | Reusable D3 v3 chart components |
p5 |
p5.js | other | LGPL-2.1 | Creative-coding canvas, artistic layouts |
raphael |
Raphael | other | MIT | Legacy VML/SVG, ultra-compatible |
vega |
Vega | other | BSD-3 | Declarative JSON grammar, reproducible |
maxgraph |
maxGraph | other | Apache-2.0 | mxGraph successor, diagram editor-grade |
reactdiagrams |
React Diagrams | react | MIT | React-native node editor, pre-built iframe |
xyflow |
XYFlow | react | MIT | React Flow successor, beautiful out of the box |
recharts |
Recharts | chart | MIT | React composable charts |
- graph — node-edge graph libraries (most renderers)
- chart — chart-first libraries that re-purpose their bar/pie primitives into graphs
- other — everything else (creative-coding, declarative, legacy)
- react — requires React to be loaded; mounted via the shell's dynamic loader
Every renderer registers itself with DbRel.registerRenderer(key, obj). The object must implement this interface:
(function() {
'use strict';
var containerEl, myScene, zoomLevel = 100;
DbRel.registerRenderer('myrenderer', {
/** Called once, when the renderer is activated. */
init: function(el) {
containerEl = el;
myScene = new MyLibrary(el);
},
/** Called every time data is loaded or display mode changes. */
render: function() {
myScene.clear();
buildFromDbRelData(myScene);
},
/** Re-run the layout without rebuilding graph elements. */
doLayout: function() { myScene.relayout(); },
/** Zoom controls (percent: 10-400). */
setZoom: function(pct) { zoomLevel = pct; myScene.setZoom(pct / 100); },
getZoom: function() { return zoomLevel; },
fitToScreen: function() { myScene.fit(); },
/** Filter chips in the toolbar. */
applyFilters: function(dbFilters, typeFilters) {
// dbFilters = { my: true|false, kayako_v4: ..., pdns: ... }
// typeFilters = { direct: ..., find_in_set: ..., cross_db: ... }
},
/** Click-to-focus. Dim everything not within 2 hops. */
focusNode: function(nodeId) { /* ... */ },
unfocusNode: function() { /* ... */ },
centerOnTable: function(tk) { /* ... */ },
/** Sidebar hover highlight (optional). */
highlightTable: function(tk) { /* ... */ },
clearHighlightTable: function() { /* ... */ },
/** Return { nodes, links } for the metadata panel. */
getStats: function() {
return { nodes: myScene.nodeCount(), links: myScene.edgeCount() };
},
/** Window resize (optional). */
resize: function() { myScene.resize(); },
/** Full teardown — the shell calls this before switching to another renderer. */
destroy: function() {
if (myScene) myScene.dispose();
if (containerEl) containerEl.innerHTML = '';
myScene = null; containerEl = null;
}
});
})();Then add it to the manifest (edit DbRel.RENDERERS before the shell's init fires, or patch the shell):
DbRel.RENDERERS['myrenderer'] = {
name: 'My Renderer',
icon: '/icons/mine.svg',
github: 'https://github.com/me/my-renderer',
cat: 'graph',
file: '/js/renderers/myrenderer.js',
js: ['https://cdn.example.com/my-library.min.js'],
css: []
};That's it — it shows up in the library dropdown and works with all the shell's features (pivot, hover, focus fading, etc).
Example 1 — Static payload from a JSON file
fetch('/data/customer-12345.json')
.then(r => r.json())
.then(data => {
DbRel.data = data;
DbRel.renderers[DbRel.activeRendererKey].render();
DbRel.updateSidebar();
});Example 2 — Programmatic renderer switch
// Try every renderer in rotation
const libs = ['jointjs', 'cytoscape', 'visjs', 'd3', 'sigma'];
let i = 0;
setInterval(() => {
DbRel.switchRenderer(libs[i % libs.length]);
i++;
}, 3000);Example 3 — Custom table icons
// Override BEFORE loading shell.js
window.DbRel = {
paths: { tableIcons: '/my-icons/' },
TABLE_ICONS: {
accounts: { img: '/my-icons/person.png' },
vps: { img: '/my-icons/server.png' },
domains: { img: '/my-icons/globe.png' }
}
};Example 4 — Pivot to a specific VPS host
// Jump straight to VPS host 42 without loading the customer first
DbRel.loadPivotDirect('vps_masters', 42, 0);Example 5 — Express server hosting static assets
const express = require('express');
const path = require('path');
const dbrel = require('@detain/dbrel-viz');
const app = express();
// Serve the package's src/ at /dbrel
app.use('/dbrel', express.static(path.dirname(dbrel.paths.shell) + '/..'));
app.get('/', (req, res) => res.sendFile(__dirname + '/index.html'));
app.listen(3000);┌────────────────────────────────────────────────────────────────────┐
│ Your application │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ div#db-rel-app │ │
│ │ ┌──────────────┬───────────────────────────────────────┐ │ │
│ │ │ │ Toolbar (lib selector, filters, ...) │ │ │
│ │ │ Sidebar ├───────────────────────────────────────┤ │ │
│ │ │ │ │ │ │
│ │ │ • tables │ div#db-rel-paper-wrap │ │ │
│ │ │ • legend │ └─ div#db-rel-paper │ │ │
│ │ │ • stats │ └─ Active renderer's canvas │ │ │
│ │ │ │ │ │ │
│ │ └──────────────┴───────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────┘
▲ ▲
│ registers │ reads data
│ │
┌──────────┴───────────┐ ┌───────────┴───────────┐
│ Renderer (one of 20)│ │ DbRel.data (JSON) │
│ │ │ │
│ init() │ │ tables[], rels[], │
│ render() │ │ prefixes, PKs, meta │
│ doLayout() │ │ │
│ setZoom() ... │ └───────────────────────┘
│ destroy() │
└──────────────────────┘
DbRel.switchRenderer('cytoscape')
├─▶ current renderer.destroy()
├─▶ reset #db-rel-paper (clear children/styles/classes)
├─▶ DbRel.loadCSS(manifest.css[])
├─▶ DbRel.loadScript(manifest.js[]) (sequential for dep ordering)
├─▶ if first time: DbRel.loadScript(manifest.file)
├─▶ new renderer.init(paperEl)
├─▶ new renderer.render()
└─▶ DbRel.updateSidebar()
- Build BFS adjacency from the relationships
- Root = the
*.accountstable (or first table if absent) - Assign each table to a BFS layer
- Sort layers by connectivity (most-connected first)
- Pack blocks into columns with bin-packing (keeping within target aspect)
- Honor
containerWidth/containerHeight— fall back to 16:9 × 0.85
dbrel-viz is a rendering frontend. Pair it with a data producer:
| Package | Language | Purpose |
|---|---|---|
@detain/dbrel-viz |
Browser JS | This package — the frontend |
detain/dbrel-data-php |
PHP ≥ 7.4 | Collects rows via mysqli, computes matches, emits the JSON |
@detain/dbrel-data-js |
Node ≥ 14 | Same output, Node + mysql2/promise |
Data flow end-to-end:
┌──────────┐ ┌───────────────────────────────┐ ┌──────────────┐
│ MySQL │────▶│ dbrel-data-php │────▶│ │
│ │ │ (or dbrel-data-js) │ │ dbrel-viz │
│ accounts│ │ │ │ (browser) │
│ vps │ │ • Loads schema JSON │ │ │
│ domains │ │ • Collects rows per table │JSON │ • 20 libs │
│ ... │ │ • Computes relationship │────▶│ • Pivot │
└──────────┘ │ matches │ │ • Focus │
│ • Emits the payload │ │ │
└───────────────────────────────┘ └──────────────┘
| Browser | Version |
|---|---|
| Chrome | Latest |
| Firefox | Latest |
| Edge | Latest |
| Safari | 13+ |
| IE 11 | Shell works (ES5); some renderers require polyfills |
Requirements
- jQuery 3+
- Bootstrap 4 (for modals and dropdowns)
- Font Awesome 5 (for toolbar icons, optional)
Contributions are welcome!
git clone https://github.com/detain/dbrel-viz.git
cd dbrel-viz
npm install
npm test- Additional renderers (e.g.
mermaid,nvd3-network,chartjs-graph) - TypeScript definitions for the
DbRelnamespace - Per-renderer screenshot generation
- Storybook with example payloads
- One feature or fix per PR
- Keep the public shell API stable — new functionality goes on renderer interfaces
- Add a Jest test for any change to
core/shell.js - Run
npm testbefore pushing - Lowercase, descriptive commit messages (
add cytoscape dim on focus,fix zoom in grouped mode)
- Plain ES5 in the shell (must work without a build step)
- Modern ES in renderers is fine if the target library requires it
- No frameworks in
core/— jQuery for DOM, Bootstrap for modals
MIT © 2025 Joe Huss / InterServer
Made with care by InterServer — because one graph library is never enough.





