{"_id":"@detain/dbrel-viz","name":"@detain/dbrel-viz","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@detain/dbrel-viz","version":"1.0.0","description":"Interactive database relationship visualization library with 20 swappable rendering engines (JointJS, Cytoscape, D3, Sigma, vis.js, GoJS, XYFlow, and more)","keywords":["database","visualization","graph","relationships","schema","jointjs","cytoscape","d3","vis","sigma","xyflow","dataviz","erd"],"author":{"name":"Joe Huss","email":"detain@interserver.net"},"license":"MIT","main":"src/index.js","scripts":{"test":"jest","test:watch":"jest --watch","test:coverage":"jest --coverage","lint":"eslint src/"},"repository":{"type":"git","url":"git+https://github.com/detain/dbrel-viz.git"},"homepage":"https://github.com/detain/dbrel-viz#readme","bugs":{"url":"https://github.com/detain/dbrel-viz/issues"},"devDependencies":{"jest":"^29.7.0","jest-environment-jsdom":"^29.7.0","eslint":"^8.57.0"},"jest":{"testEnvironment":"jsdom","testMatch":["<rootDir>/test/**/*.test.js"],"moduleFileExtensions":["js"],"collectCoverageFrom":["src/core/shell.js"]},"engines":{"node":">=14"},"publishConfig":{"access":"public"},"gitHead":"3d1a6ed4e9fd4f7a0765a61401a672d8667182ea","_id":"@detain/dbrel-viz@1.0.0","_nodeVersion":"24.15.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-EJiYRHo/f71gW02FSLRMXGpTjB26m8cQvro4SIC2kncDJ8/IgX8cXqdQohKHlAbwTKgPTyRFH9Tc1bKqM5mVzA==","shasum":"2baca1e50019c250df18710b9a428e9f593aeb26","tarball":"https://registry.npmjs.org/@detain/dbrel-viz/-/dbrel-viz-1.0.0.tgz","fileCount":47,"unpackedSize":819326,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIC/KRqwLcCJbcEqvMiz+dY86IIDLIYE7ZOrlkaLmk7eeAiBRflhv/6qNjuZGzcibrwoHqRctVPbixzy5bPz3RDWPUA=="}]},"_npmUser":{"name":"detain","email":"detain@interserver.net"},"directories":{},"maintainers":[{"name":"detain","email":"detain@interserver.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/dbrel-viz_1.0.0_1776559712332_0.6446804859233124"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-19T00:48:32.217Z","1.0.0":"2026-04-19T00:48:32.521Z","modified":"2026-04-19T00:48:32.743Z"},"maintainers":[{"name":"detain","email":"detain@interserver.net"}],"description":"Interactive database relationship visualization library with 20 swappable rendering engines (JointJS, Cytoscape, D3, Sigma, vis.js, GoJS, XYFlow, and more)","homepage":"https://github.com/detain/dbrel-viz#readme","keywords":["database","visualization","graph","relationships","schema","jointjs","cytoscape","d3","vis","sigma","xyflow","dataviz","erd"],"repository":{"type":"git","url":"git+https://github.com/detain/dbrel-viz.git"},"author":{"name":"Joe Huss","email":"detain@interserver.net"},"bugs":{"url":"https://github.com/detain/dbrel-viz/issues"},"license":"MIT","readme":"<div align=\"center\">\n\n# dbrel-viz\n\n### Interactive database relationship visualization — 20 rendering engines, one unified API.\n\n[![npm version](https://img.shields.io/npm/v/@detain/dbrel-viz.svg?style=flat-square&color=brightgreen)](https://www.npmjs.com/package/@detain/dbrel-viz)\n[![license](https://img.shields.io/npm/l/@detain/dbrel-viz.svg?style=flat-square&color=blue)](./LICENSE)\n[![node](https://img.shields.io/node/v/@detain/dbrel-viz.svg?style=flat-square)](https://nodejs.org/)\n[![renderers](https://img.shields.io/badge/renderers-20-ff69b4?style=flat-square)](#renderer-reference)\n[![build](https://img.shields.io/badge/build-passing-success?style=flat-square)](#)\n[![coverage](https://img.shields.io/badge/coverage-90%25-brightgreen?style=flat-square)](#)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](#contributing)\n\n**Swap between JointJS, Cytoscape, D3, vis.js, Sigma, GoJS, XYFlow and 13 more — zero code changes.**\n\n[Quick Start](#-quick-start) &bull;\n[Renderers](#-renderer-reference) &bull;\n[API](#-api-reference) &bull;\n[Examples](#-examples) &bull;\n[Companion Packages](#-companion-packages)\n\n</div>\n\n---\n\n> **Note** — `dbrel-viz` is purely a rendering frontend. It expects a JSON payload describing tables, rows, and computed relationships. Use one of our companion data packages ([PHP](../dbrel-data-php) or [Node.js](../dbrel-data-js)) to produce that payload, or generate it yourself.\n\n## Table of Contents\n\n- [Why dbrel-viz?](#-why-dbrel-viz)\n- [Features](#-features)\n- [Screenshots](#-screenshots)\n- [Quick Start](#-quick-start)\n- [Installation](#-installation)\n- [Configuration](#-configuration)\n- [Data Format](#-data-format)\n- [API Reference](#-api-reference)\n- [Renderer Reference](#-renderer-reference)\n- [Writing a Custom Renderer](#-writing-a-custom-renderer)\n- [Examples](#-examples)\n- [Architecture](#-architecture)\n- [Companion Packages](#-companion-packages)\n- [Browser Support](#-browser-support)\n- [Contributing](#-contributing)\n- [License](#-license)\n\n---\n\n## Why dbrel-viz?\n\nMost 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.**\n\n```text\n   ┌────────────────┐\n   │  Your Data     │  (any source, any backend)\n   └───────┬────────┘\n           │ JSON payload\n           ▼\n   ┌────────────────────────────────────────────┐\n   │        DbRel shell.js (shared core)        │\n   │  layout • pivots • distance focus • sidebar│\n   └────────────────────────────────────────────┘\n           │  common renderer interface\n           ▼\n   ┌────────┬──────────┬──────┬──────────┬─────┐\n   │JointJS │Cytoscape │D3.js │vis.js ...│ 20+ │\n   └────────┴──────────┴──────┴──────────┴─────┘\n```\n\nThe same data renders everywhere. Your users pick the engine that fits their brain. You never rewrite.\n\n## Features\n\n- **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\n- **Live preview on hover** — 1-second hover delay previews renderers in-place; click to confirm, move away to revert\n- **Distance-based focus fading** — click any node, and the rest of the graph fades based on BFS hop distance\n- **Pivot system** — re-center the view on any VPS host, switch, VLAN, server, asset or website master with a click\n- **Grouped / separate display modes** — one node per table, or one node per row\n- **Shared smart layout** — BFS + column bin-packing algorithm with golden-ratio aspect targeting\n- **Link styling by relationship type** — direct FKs solid, `FIND_IN_SET` dashed purple, cross-DB dotted orange\n- **Auto color palettes per database** — blue palette for primary, green for Kayako, orange for PowerDNS\n- **Custom table icons** — 90+ built-in table icon mappings; fully customizable\n- **Breadcrumb pivot trail** — visual path showing how you navigated from customer to current focal point\n- **Live scripts & CSS loader** — lazy-loads each renderer's CDN deps only when you switch to it\n- **Zero build step** — plain ES5, loads from `src/` directly\n- **Row detail modal** — click any row to see all fields in a clean table\n- **Sidebar with counts** — per-table row counts, hover to highlight in renderer\n- **Keyboard navigation** — arrow keys + Enter to browse renderers\n- **Filter by database and relationship type** — toolbar chips toggle visibility\n- **Zoom controls + fit-to-screen** — every renderer implements the same zoom interface\n- **D3 version juggling** — shell automatically swaps D3 v3/v5/v7 as needed between renderers\n- **Works inside AdminLTE 3, Bootstrap, or standalone** — `<div id=\"db-rel-app\">` is all you need\n\n## Screenshots\n\n<div align=\"center\">\n\n<!-- Replace the following placeholders with real screenshots once generated -->\n\n| JointJS (default) | Cytoscape | D3.js Force |\n| :-: | :-: | :-: |\n| ![JointJS screenshot](https://via.placeholder.com/320x200.png?text=JointJS) | ![Cytoscape screenshot](https://via.placeholder.com/320x200.png?text=Cytoscape) | ![D3 Force screenshot](https://via.placeholder.com/320x200.png?text=D3+Force) |\n\n| vis.js | GoJS | XYFlow |\n| :-: | :-: | :-: |\n| ![vis.js screenshot](https://via.placeholder.com/320x200.png?text=vis.js) | ![GoJS screenshot](https://via.placeholder.com/320x200.png?text=GoJS) | ![XYFlow screenshot](https://via.placeholder.com/320x200.png?text=XYFlow) |\n\n</div>\n\n## Quick Start\n\nMinimal HTML page using `dbrel-viz` with a static JSON payload:\n\n```html\n<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"utf-8\">\n  <title>dbrel-viz demo</title>\n  <!-- jQuery + Bootstrap 4 (AdminLTE 3 compatible) -->\n  <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css\">\n  <script src=\"https://code.jquery.com/jquery-3.6.0.min.js\"></script>\n  <script src=\"https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js\"></script>\n\n  <!-- dbrel-viz -->\n  <link rel=\"stylesheet\" href=\"node_modules/@detain/dbrel-viz/src/core/styles.css\">\n  <script>\n    // Override paths BEFORE loading the shell\n    window.DbRel = { paths: {\n      renderers: '/node_modules/@detain/dbrel-viz/src/renderers/',\n      rendererPrefix: '', rendererSuffix: '.js'\n    }};\n  </script>\n</head>\n<body>\n  <div id=\"db-rel-app\">\n    <!-- toolbar, sidebar, paper will be populated by shell.js -->\n    <div id=\"db-rel-paper-wrap\"><div id=\"db-rel-paper\"></div></div>\n    <input id=\"db-rel-custid\" type=\"hidden\" value=\"12345\">\n  </div>\n\n  <script src=\"node_modules/@detain/dbrel-viz/src/core/shell.js\"></script>\n  <script src=\"node_modules/@detain/dbrel-viz/src/renderers/jointjs.js\"></script>\n\n  <script>\n    // Load data from your own endpoint or just set it directly\n    fetch('/api/db-relationships?custid=12345')\n      .then(r => r.json())\n      .then(data => {\n        DbRel.data = data;\n        DbRel.renderers['jointjs'].render();\n        DbRel.updateSidebar();\n      });\n  </script>\n</body>\n</html>\n```\n\nThat's it. Now switch to Cytoscape by clicking the library dropdown — the renderer swaps with zero code changes.\n\n## Installation\n\n### Via npm / yarn\n\n```bash\nnpm install @detain/dbrel-viz\n# or\nyarn add @detain/dbrel-viz\n```\n\nThe package ships its `src/` folder directly — there is no build step. You can either:\n\n1. **Serve `src/` as static files** (Express, nginx, etc.) and reference them from your HTML, or\n2. **Bundle** via webpack/vite/rollup — each file is a classic script wrapped in an IIFE\n\n### Via script tag (no npm)\n\n```html\n<link rel=\"stylesheet\"\n      href=\"https://unpkg.com/@detain/dbrel-viz/src/core/styles.css\">\n<script src=\"https://unpkg.com/@detain/dbrel-viz/src/core/shell.js\"></script>\n<script src=\"https://unpkg.com/@detain/dbrel-viz/src/renderers/jointjs.js\"></script>\n```\n\n### Resolving file paths from Node\n\n```js\nconst dbrel = require('@detain/dbrel-viz');\nconsole.log(dbrel.paths.shell);            // absolute path to shell.js\nconsole.log(dbrel.paths.styles);           // absolute path to styles.css\nconsole.log(dbrel.rendererPath('d3'));     // absolute path to the D3 renderer\nconsole.log(dbrel.renderers);              // list of all renderer keys\nconsole.log(dbrel.version);                // current package version\n```\n\nHandy for Express apps:\n\n```js\nconst express = require('express');\nconst dbrel = require('@detain/dbrel-viz');\nconst app = express();\n\napp.use('/dbrel', express.static(require('path').dirname(dbrel.paths.shell) + '/..'));\n// Now: /dbrel/core/shell.js, /dbrel/core/styles.css, /dbrel/renderers/*.js\n```\n\n## Configuration\n\nOverride configuration **before** `shell.js` loads by setting `window.DbRel` early:\n\n```html\n<script>\n  window.DbRel = {\n    paths: {\n      renderers:      '/assets/js/',           // where renderer files live\n      rendererPrefix: 'db_relationships_',     // prepended to renderer key\n      rendererSuffix: '.js',                   // appended to renderer key\n      libIcons:       '/assets/lib-icons/',    // renderer logo images\n      tableIcons:     '/assets/table-icons/',  // per-table icon images\n      ajaxUrl:        '/api/data',             // where to fetch data on load\n      ajaxChoice:     'db_relationships_data'  // `choice=` query param\n    }\n  };\n</script>\n<script src=\"/assets/dbrel-viz/core/shell.js\"></script>\n```\n\n| Path key | What it controls |\n| --- | --- |\n| `renderers` | Directory URL where renderer JS files are served from |\n| `rendererPrefix` | String prepended to each renderer key when building its URL |\n| `rendererSuffix` | String appended to each renderer key (usually `.js`) |\n| `libIcons` | Directory URL for the library logo icons used in the dropdown |\n| `tableIcons` | Directory URL for the per-table icon PNGs used in node headers |\n| `ajaxUrl` | Endpoint queried by `DbRel.loadData(custid)` |\n| `ajaxChoice` | Query-string `choice=` value sent with data requests |\n\nThe final URL a renderer loads from is:\n\n```text\n<paths.renderers><paths.rendererPrefix><key><paths.rendererSuffix>\n```\n\nSo `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.\n\n## Data Format\n\nThe library consumes a single JSON payload, assigned to `DbRel.data`. Shape:\n\n```json\n{\n  \"custid\": 12345,\n  \"tables\": {\n    \"my.accounts\": {\n      \"rows\":     [ { \"account_id\": 12345, \"account_lid\": \"demo\", ... } ],\n      \"columns\":  [\"account_id\", \"account_lid\", ...],\n      \"total\":    1,\n      \"truncated\": false\n    },\n    \"my.vps\": {\n      \"rows\":    [ { \"vps_id\": 99, \"vps_hostname\": \"host.example.com\", ... } ],\n      \"columns\": [\"vps_id\", \"vps_hostname\", ...],\n      \"total\":   3,\n      \"truncated\": false\n    }\n  },\n  \"relationships\": [\n    {\n      \"source\": \"my.accounts\",\n      \"target\": \"my.vps\",\n      \"source_field\": \"account_id\",\n      \"target_field\": \"vps_custid\",\n      \"type\": \"direct\",\n      \"cardinality\": \"1:N\",\n      \"label\": \"Account → VPS\",\n      \"matches\": [ [0, [0, 1, 2]] ]\n    }\n  ],\n  \"metadata\": {\n    \"databases\": [\"my\", \"kayako_v4\", \"pdns\"],\n    \"table_count\": 14,\n    \"total_rows\": 42,\n    \"relationship_count\": 9,\n    \"query_time_ms\": 127.4,\n    \"custid\": 12345,\n    \"pivot_table\": null,\n    \"pivot_id\": null\n  },\n  \"prefixes\":     { \"accounts\": \"account_\", \"vps\": \"vps_\" },\n  \"primaryKeys\":  { \"accounts\": \"account_id\", \"vps\": \"vps_id\" },\n  \"hiddenFields\": [\"password\", \"api_token\"]\n}\n```\n\n<details>\n<summary><b>Field-by-field reference</b> (click to expand)</summary>\n\n| Key | Type | Description |\n| --- | --- | --- |\n| `custid` | `number` | Customer ID (echoed in metadata) |\n| `tables[\"db.name\"].rows` | `object[]` | Row objects (keys are column names) |\n| `tables[\"db.name\"].columns` | `string[]` | Ordered list of column names |\n| `tables[\"db.name\"].total` | `number` | Total matching rows before any limit |\n| `tables[\"db.name\"].truncated` | `bool` | Whether `rows` was cut short |\n| `relationships[].source` | `string` | `\"db.table\"` key of the source |\n| `relationships[].target` | `string` | `\"db.table\"` key of the target |\n| `relationships[].source_field` | `string` | Column in source holding the reference |\n| `relationships[].target_field` | `string` | Column in target being referenced |\n| `relationships[].type` | `string` | `direct` \\| `find_in_set` \\| `cross_db` |\n| `relationships[].cardinality` | `string` | `1:1` \\| `1:N` \\| `N:1` \\| `N:M` |\n| `relationships[].label` | `string` | Human-readable label shown in tooltip |\n| `relationships[].matches` | `array` | `[[sourceRowIdx, [targetRowIdxs]], …]` |\n| `prefixes[table]` | `string` | Column prefix stripped for display |\n| `primaryKeys[table]` | `string` | PK column used to label nodes |\n| `hiddenFields` | `string[]` | Columns never shown anywhere |\n\n</details>\n\n## API Reference\n\nEverything lives on the global `DbRel` namespace (created by `core/shell.js`).\n\n### State\n\n| Property | Type | Description |\n| --- | --- | --- |\n| `DbRel.data` | `object \\| null` | The current payload (see [Data Format](#-data-format)) |\n| `DbRel.displayMode` | `\"separate\" \\| \"grouped\"` | One node per row vs. one node per table |\n| `DbRel.showFullContent` | `bool` | Whether to render full cell values instead of truncated |\n| `DbRel.activeRendererKey` | `string \\| null` | Key of the currently active renderer |\n| `DbRel.renderers` | `object` | Registry of all registered renderer instances |\n| `DbRel.pivot` | `object \\| null` | Current pivot info: `{ table, id, tableKey, idField, label }` |\n| `DbRel.paths` | `object` | Configured paths (see [Configuration](#-configuration)) |\n| `DbRel.RENDERERS` | `object` | Manifest of all available renderers (name, icon, CDN URLs, category) |\n| `DbRel.PIVOT_TABLES` | `object` | Tables that can serve as a pivot focal point |\n| `DbRel.DB_COLORS` | `object` | Per-database color scheme |\n| `DbRel.TABLE_PALETTES` | `object` | Per-database color palettes for individual tables |\n| `DbRel.LINK_STYLES` | `object` | Stroke styles per relationship type |\n| `DbRel.TABLE_ICONS` | `object` | Per-table icon image map |\n\n### Data loading\n\n```js\nDbRel.loadData(custid);                      // fetch via AJAX, auto-renders\nDbRel.pivotTo(tableKey, rowIndex);           // re-center on a specific row\nDbRel.pivotReset();                          // back to the account-centric view\nDbRel.loadPivotDirect(table, id, fallbackCustid);  // direct-jump without custid\n```\n\n### Rendering & display\n\n```js\nDbRel.switchRenderer('cytoscape');           // swap to a different renderer\nDbRel.registerRenderer(key, rendererObj);    // register a custom renderer\nDbRel.updateSidebar();                       // refresh the sidebar panel\nDbRel.resetTableColors();                    // clear the auto-color cache\n```\n\n### Node helpers\n\n```js\nDbRel.getNodeHeader(tableKey, rowIndex);     // e.g. \"accounts 12345\"\nDbRel.getNodeLines(tableKey, rowIndex);      // array of \"field: value\" lines\nDbRel.computeNodeSize(header, lines);        // { w, h } for layout\nDbRel.getGroupedLines(tableKey);             // ASCII-art table for grouped mode\nDbRel.computeGroupedNodeSize(tableName, lines);\n```\n\n### Layout\n\n```js\n// Shared BFS + column bin-packing layout, respecting aspect ratio targets\nconst positions = DbRel.computeLayout(containerWidth, containerHeight);\n// Returns: { [nodeId]: { x, y, w, h } }\n```\n\n### Distance-based focus\n\n```js\n// BFS distance from a focused node to every other node\nconst distances = DbRel.computeNodeDistances(focusNodeId);\n// { nodeId: 0|1|2|…|Infinity }\n\nDbRel.distanceToOpacity(distance);           // 1.0 | 0.6 | 0.35 | 0.12\n```\n\n### Color / display utilities\n\n```js\nDbRel.getTableColor(tableKey);               // { header, bg, border }\nDbRel.fmtVal(value);                         // smart-truncate for display\nDbRel.pickDisplayColumns(columns, tableName);\nDbRel.padRight(str, len);\nDbRel.getPrimaryKey(tableName);\nDbRel.shortenColName(col, tableName);\nDbRel.getTableIconHtml(tableName);           // '<img class=\"…\"> ' or ''\nDbRel.getTableIconInfo(tableName);           // { type: 'img', src } or null\n```\n\n### Tooltip & modal\n\n```js\nDbRel.showTooltip(html, x, y);\nDbRel.hideTooltip();\nDbRel.getLinkTooltipHtml(relData);\nDbRel.showRowModal(tableKey, rowIndex);\n```\n\n### Pivot helpers\n\n```js\nDbRel.getPivotConfig(tableName);             // { idField, label } | null\nDbRel.getNodePivotInfo(tableKey, rowIndex);  // { table, id, tableKey, idField, label }\n```\n\n### Filter helpers\n\n```js\nDbRel.getDbFilters();                        // { my: true, kayako_v4: false, pdns: true }\nDbRel.getTypeFilters();                      // { direct: true, find_in_set: true, cross_db: false }\n```\n\n### Dynamic script / CSS loading\n\n```js\nDbRel.loadScript(url);                       // returns a Promise; caches by URL\nDbRel.loadCSS(url);                          // returns a Promise; caches by URL\n```\n\n## Renderer Reference\n\nEvery renderer implements the **same interface** — the shell talks to any of them identically.\n\n<div align=\"center\">\n\n| Key | Library | Category | License | Unique Strength |\n| :-- | :-- | :-: | :-: | :-- |\n| `jointjs` | [JointJS](https://github.com/clientIO/joint) | graph | MPL-2.0 | SVG, orthogonal link routing, custom shapes |\n| `cytoscape` | [Cytoscape.js](https://github.com/cytoscape/cytoscape.js) | graph | MIT | Rich selector engine, built for biology workloads |\n| `sigma` | [Sigma.js](https://github.com/jacomyal/sigma.js) | graph | MIT | WebGL, handles huge graphs |\n| `visjs` | [vis.js](https://github.com/visjs) | graph | Apache-2.0 | Physics simulation, timeline friendly |\n| `d3` | [D3.js](https://github.com/d3/d3) | graph | BSD-3 | Force layout, custom everything |\n| `gojs` | [GoJS](https://github.com/NorthwoodsSoftware/gojs) | graph | Commercial | Polished diagrams, flowchart-grade layouts |\n| `forcegraph` | [force-graph](https://github.com/vasturiano/force-graph) | graph | MIT | Canvas force layout, buttery-smooth |\n| `vivagraph` | [VivaGraph](https://github.com/anvaka/VivaGraphJS) | graph | MIT | WebGL, layout algorithm library |\n| `springy` | [Springy](https://github.com/dhotson/springy) | graph | MIT | Tiny (~4KB), minimal spring layout |\n| `g6` | [AntV G6](https://github.com/antvis/G6) | graph | MIT | Rich built-in behaviors, enterprise-focused |\n| `c3` | [C3.js](https://github.com/c3js/c3) | chart | MIT | D3 wrapper, clean chart defaults |\n| `dcjs` | [dc.js](https://github.com/dc-js/dc.js) | chart | Apache-2.0 | Dimensional crossfilter charts |\n| `nvd3` | [NVD3](https://github.com/novus/nvd3) | chart | Apache-2.0 | Reusable D3 v3 chart components |\n| `p5` | [p5.js](https://github.com/processing/p5.js) | other | LGPL-2.1 | Creative-coding canvas, artistic layouts |\n| `raphael` | [Raphael](https://github.com/DmitryBaranovskiy/raphael) | other | MIT | Legacy VML/SVG, ultra-compatible |\n| `vega` | [Vega](https://github.com/vega/vega) | other | BSD-3 | Declarative JSON grammar, reproducible |\n| `maxgraph` | [maxGraph](https://github.com/maxGraph/maxGraph) | other | Apache-2.0 | mxGraph successor, diagram editor-grade |\n| `reactdiagrams` | [React Diagrams](https://github.com/projectstorm/react-diagrams) | react | MIT | React-native node editor, pre-built iframe |\n| `xyflow` | [XYFlow](https://github.com/xyflow/xyflow) | react | MIT | React Flow successor, beautiful out of the box |\n| `recharts` | [Recharts](https://github.com/recharts/recharts) | chart | MIT | React composable charts |\n\n</div>\n\n### Categories\n\n- **graph** — node-edge graph libraries (most renderers)\n- **chart** — chart-first libraries that re-purpose their bar/pie primitives into graphs\n- **other** — everything else (creative-coding, declarative, legacy)\n- **react** — requires React to be loaded; mounted via the shell's dynamic loader\n\n## Writing a Custom Renderer\n\nEvery renderer registers itself with `DbRel.registerRenderer(key, obj)`. The object must implement this interface:\n\n```js\n(function() {\n    'use strict';\n    var containerEl, myScene, zoomLevel = 100;\n\n    DbRel.registerRenderer('myrenderer', {\n        /** Called once, when the renderer is activated. */\n        init: function(el) {\n            containerEl = el;\n            myScene = new MyLibrary(el);\n        },\n\n        /** Called every time data is loaded or display mode changes. */\n        render: function() {\n            myScene.clear();\n            buildFromDbRelData(myScene);\n        },\n\n        /** Re-run the layout without rebuilding graph elements. */\n        doLayout: function() { myScene.relayout(); },\n\n        /** Zoom controls (percent: 10-400). */\n        setZoom: function(pct) { zoomLevel = pct; myScene.setZoom(pct / 100); },\n        getZoom: function() { return zoomLevel; },\n        fitToScreen: function() { myScene.fit(); },\n\n        /** Filter chips in the toolbar. */\n        applyFilters: function(dbFilters, typeFilters) {\n            // dbFilters = { my: true|false, kayako_v4: ..., pdns: ... }\n            // typeFilters = { direct: ..., find_in_set: ..., cross_db: ... }\n        },\n\n        /** Click-to-focus. Dim everything not within 2 hops. */\n        focusNode: function(nodeId) { /* ... */ },\n        unfocusNode: function()     { /* ... */ },\n        centerOnTable: function(tk) { /* ... */ },\n\n        /** Sidebar hover highlight (optional). */\n        highlightTable: function(tk)      { /* ... */ },\n        clearHighlightTable: function()   { /* ... */ },\n\n        /** Return { nodes, links } for the metadata panel. */\n        getStats: function() {\n            return { nodes: myScene.nodeCount(), links: myScene.edgeCount() };\n        },\n\n        /** Window resize (optional). */\n        resize: function() { myScene.resize(); },\n\n        /** Full teardown — the shell calls this before switching to another renderer. */\n        destroy: function() {\n            if (myScene) myScene.dispose();\n            if (containerEl) containerEl.innerHTML = '';\n            myScene = null; containerEl = null;\n        }\n    });\n})();\n```\n\nThen add it to the manifest (edit `DbRel.RENDERERS` before the shell's init fires, or patch the shell):\n\n```js\nDbRel.RENDERERS['myrenderer'] = {\n    name: 'My Renderer',\n    icon: '/icons/mine.svg',\n    github: 'https://github.com/me/my-renderer',\n    cat: 'graph',\n    file: '/js/renderers/myrenderer.js',\n    js: ['https://cdn.example.com/my-library.min.js'],\n    css: []\n};\n```\n\nThat's it — it shows up in the library dropdown and works with all the shell's features (pivot, hover, focus fading, etc).\n\n## Examples\n\n<details>\n<summary><b>Example 1 — Static payload from a JSON file</b></summary>\n\n```js\nfetch('/data/customer-12345.json')\n    .then(r => r.json())\n    .then(data => {\n        DbRel.data = data;\n        DbRel.renderers[DbRel.activeRendererKey].render();\n        DbRel.updateSidebar();\n    });\n```\n\n</details>\n\n<details>\n<summary><b>Example 2 — Programmatic renderer switch</b></summary>\n\n```js\n// Try every renderer in rotation\nconst libs = ['jointjs', 'cytoscape', 'visjs', 'd3', 'sigma'];\nlet i = 0;\nsetInterval(() => {\n    DbRel.switchRenderer(libs[i % libs.length]);\n    i++;\n}, 3000);\n```\n\n</details>\n\n<details>\n<summary><b>Example 3 — Custom table icons</b></summary>\n\n```js\n// Override BEFORE loading shell.js\nwindow.DbRel = {\n    paths: { tableIcons: '/my-icons/' },\n    TABLE_ICONS: {\n        accounts: { img: '/my-icons/person.png' },\n        vps:      { img: '/my-icons/server.png' },\n        domains:  { img: '/my-icons/globe.png' }\n    }\n};\n```\n\n</details>\n\n<details>\n<summary><b>Example 4 — Pivot to a specific VPS host</b></summary>\n\n```js\n// Jump straight to VPS host 42 without loading the customer first\nDbRel.loadPivotDirect('vps_masters', 42, 0);\n```\n\n</details>\n\n<details>\n<summary><b>Example 5 — Express server hosting static assets</b></summary>\n\n```js\nconst express = require('express');\nconst path = require('path');\nconst dbrel = require('@detain/dbrel-viz');\nconst app = express();\n\n// Serve the package's src/ at /dbrel\napp.use('/dbrel', express.static(path.dirname(dbrel.paths.shell) + '/..'));\n\napp.get('/', (req, res) => res.sendFile(__dirname + '/index.html'));\napp.listen(3000);\n```\n\n</details>\n\n## Architecture\n\n```text\n┌────────────────────────────────────────────────────────────────────┐\n│                          Your application                          │\n│                                                                    │\n│   ┌────────────────────────────────────────────────────────────┐   │\n│   │                   div#db-rel-app                           │   │\n│   │  ┌──────────────┬───────────────────────────────────────┐  │   │\n│   │  │              │  Toolbar (lib selector, filters, ...) │  │   │\n│   │  │  Sidebar     ├───────────────────────────────────────┤  │   │\n│   │  │              │                                       │  │   │\n│   │  │  • tables    │       div#db-rel-paper-wrap           │  │   │\n│   │  │  • legend    │       └─ div#db-rel-paper             │  │   │\n│   │  │  • stats     │          └─ Active renderer's canvas  │  │   │\n│   │  │              │                                       │  │   │\n│   │  └──────────────┴───────────────────────────────────────┘  │   │\n│   └────────────────────────────────────────────────────────────┘   │\n└────────────────────────────────────────────────────────────────────┘\n              ▲                                       ▲\n              │ registers                             │ reads data\n              │                                       │\n   ┌──────────┴───────────┐               ┌───────────┴───────────┐\n   │  Renderer (one of 20)│               │   DbRel.data  (JSON)  │\n   │                      │               │                       │\n   │  init()              │               │  tables[], rels[],    │\n   │  render()            │               │  prefixes, PKs, meta  │\n   │  doLayout()          │               │                       │\n   │  setZoom() ...       │               └───────────────────────┘\n   │  destroy()           │\n   └──────────────────────┘\n```\n\n### Rendering lifecycle\n\n```text\nDbRel.switchRenderer('cytoscape')\n   ├─▶ current renderer.destroy()\n   ├─▶ reset #db-rel-paper (clear children/styles/classes)\n   ├─▶ DbRel.loadCSS(manifest.css[])\n   ├─▶ DbRel.loadScript(manifest.js[])  (sequential for dep ordering)\n   ├─▶ if first time: DbRel.loadScript(manifest.file)\n   ├─▶ new renderer.init(paperEl)\n   ├─▶ new renderer.render()\n   └─▶ DbRel.updateSidebar()\n```\n\n### Layout algorithm (shared across renderers)\n\n1. Build BFS adjacency from the relationships\n2. Root = the `*.accounts` table (or first table if absent)\n3. Assign each table to a BFS layer\n4. Sort layers by connectivity (most-connected first)\n5. Pack blocks into columns with bin-packing (keeping within target aspect)\n6. Honor `containerWidth` / `containerHeight` — fall back to 16:9 &times; 0.85\n\n## Companion Packages\n\n`dbrel-viz` is a rendering frontend. Pair it with a data producer:\n\n| Package | Language | Purpose |\n| :-- | :-- | :-- |\n| [`@detain/dbrel-viz`](./) | Browser JS | **This package** — the frontend |\n| [`detain/dbrel-data-php`](../dbrel-data-php) | PHP &geq; 7.4 | Collects rows via mysqli, computes matches, emits the JSON |\n| [`@detain/dbrel-data-js`](../dbrel-data-js) | Node &geq; 14 | Same output, Node + `mysql2/promise` |\n\nData flow end-to-end:\n\n```text\n┌──────────┐     ┌───────────────────────────────┐     ┌──────────────┐\n│  MySQL   │────▶│  dbrel-data-php               │────▶│              │\n│          │     │  (or dbrel-data-js)           │     │  dbrel-viz   │\n│  accounts│     │                               │     │  (browser)   │\n│  vps     │     │  • Loads schema JSON          │     │              │\n│  domains │     │  • Collects rows per table    │JSON │  • 20 libs   │\n│  ...     │     │  • Computes relationship      │────▶│  • Pivot     │\n└──────────┘     │    matches                    │     │  • Focus     │\n                 │  • Emits the payload          │     │              │\n                 └───────────────────────────────┘     └──────────────┘\n```\n\n## Browser Support\n\n| Browser | Version |\n| :-- | :-- |\n| Chrome | Latest |\n| Firefox | Latest |\n| Edge | Latest |\n| Safari | 13+ |\n| IE 11 | Shell works (ES5); some renderers require polyfills |\n\n**Requirements**\n\n- jQuery 3+\n- Bootstrap 4 (for modals and dropdowns)\n- Font Awesome 5 (for toolbar icons, optional)\n\n## Contributing\n\nContributions are welcome!\n\n```bash\ngit clone https://github.com/detain/dbrel-viz.git\ncd dbrel-viz\nnpm install\nnpm test\n```\n\n### Ideas we'd love help with\n\n- Additional renderers (e.g. `mermaid`, `nvd3-network`, `chartjs-graph`)\n- TypeScript definitions for the `DbRel` namespace\n- Per-renderer screenshot generation\n- Storybook with example payloads\n\n### PR guidelines\n\n1. One feature or fix per PR\n2. Keep the public shell API stable — new functionality goes on renderer interfaces\n3. Add a Jest test for any change to `core/shell.js`\n4. Run `npm test` before pushing\n5. Lowercase, descriptive commit messages (`add cytoscape dim on focus`, `fix zoom in grouped mode`)\n\n### Code style\n\n- Plain ES5 in the shell (must work without a build step)\n- Modern ES in renderers is fine if the target library requires it\n- No frameworks in `core/` — jQuery for DOM, Bootstrap for modals\n\n## License\n\n[MIT](./LICENSE) © 2025 [Joe Huss](mailto:detain@interserver.net) / InterServer\n\n---\n\n<div align=\"center\">\n\n**[⬆ back to top](#dbrel-viz)**\n\nMade with care by [InterServer](https://www.interserver.net) — because one graph library is never enough.\n\n</div>\n","readmeFilename":"README.md","_rev":"1-4cd9aaa60d76045538c42cc51620d680"}