An entity relationship diagram of a shop database where each foreign key link lights up in turn with its join written below.
╭────────────────────────╮ ╭────────────────────────╮ │ users 4 cols │ │ orders 5 cols │ ├────────────────────────┤ ├────────────────────────┤ ╭┼│ PK id bigint │┼─╮ │ PK id bigint │┼─╮ │ │ email text │ ╰<│ FK user_id bigint │ │ │ │ name text │ │ total numeric │ │ │ │ created_at datetime │ │ status text │ │ │ ╰────────────────────────╯ │ placed_at datetime │ │ │ ╰────────────────────────╯ │ │ │ │ │ │ │ │ ╭────────────────────────╮ ╭────────────────────────╮ │ ╭────────────────────────╮ │ │ addresses 4 cols │ │ order_items 5 cols │ │ │ products 4 cols │ │ ├────────────────────────┤ ├────────────────────────┤ │ ├────────────────────────┤ │ │ PK id bigint │ │ PK id bigint │ │╭┼│ PK id bigint │ ╰<│ FK user_id bigint │ │ FK order_id bigint │>─╯│ │ sku text │ │ city text │ │ FK product_id bigint │>──╯ │ name text │ │ country char(2) │ │ qty int │ │ price numeric │ ╰────────────────────────╯ │ price numeric │ ╰────────────────────────╯ ╰────────────────────────╯ ────────────────────────────────────────────────────────────────────────────────────────── sql> JOIN orders ON o█ 1 ─< many 5 tables
Paste two lines into any page, play it in a terminal, or put a GIF in a README. The code follows the look you pick above.
<script type="module" src="https://www.ascii-magic.com/ascii/ascii.js"></script>
<ascii-art piece="er-diagram"></ascii-art>// copy er-diagram.js and kit.js into your project, then:
import { useEffect, useRef } from "react";
import create from "./er-diagram.js";
export function Piece() {
const ref = useRef(null);
useEffect(() => {
const draw = create(), t0 = performance.now();
let raf;
const tick = () => {
const f = draw((performance.now() - t0) / 1000);
ref.current.textContent = f.text ?? f;
raf = requestAnimationFrame(tick);
};
tick();
return () => cancelAnimationFrame(raf);
}, []);
return <pre ref={ref} style={{ lineHeight: 1, fontFamily: "Menlo, Consolas, monospace" }} />;
}npx --yes https://www.ascii-magic.com/ascii/cli.tgz er-diagram<!-- Download GIF below, commit it, then: -->
<p align="center">
<img src="assets/er-diagram.gif" alt="ER Diagram, animated ASCII art" width="720">
</p>Pause on the frame you like, pick a look, then download it at an exact size.
MIT licensed. It imports one small helper file, kit.js.
/* er-diagram: database tables with fields and relationship lines lighting up one join at a time. */
import { Grid, palette } from "../lib/kit.js";
export const meta = {
slug: "er-diagram",
name: "ER Diagram",
category: "explainers",
note: "An entity relationship diagram of a shop database where each foreign key link lights up in turn with its join written below.",
tags: ["database", "schema", "erd", "sql", "diagram", "docs"],
cols: 90, rows: 23, fps: 20,
palette: palette(["#e6edf3", "#8b949e", "#30363d", "#79c0ff", "#7ee787", "#ffa657", "#d2a8ff", "#f2cc60", "#ffffff", "#6e7681", "#56d4dd"]),
};
const CORNER = { RD: "╮", RU: "╯", LD: "╭", LU: "╰", DR: "╰", DL: "╯", UR: "╭", UL: "╮" };
const dirOf = (a, b) => (b[0] > a[0] ? "R" : b[0] < a[0] ? "L" : b[1] > a[1] ? "D" : "U");
function cellsOf(pts) {
const out = [pts[0]];
for (let i = 1; i < pts.length; i++) {
let [x, y] = out[out.length - 1];
const [tx, ty] = pts[i];
while (x !== tx || y !== ty) { x += Math.sign(tx - x); y += Math.sign(ty - y); out.push([x, y]); }
}
return out;
}
// name, x, y, fields [name, type, key]
const TABLES = {
users: [3, 0, [["id", "bigint", "PK"], ["email", "text", ""], ["name", "text", ""], ["created_at", "datetime", ""]]],
orders: [33, 0, [["id", "bigint", "PK"], ["user_id", "bigint", "FK"], ["total", "numeric", ""], ["status", "text", ""], ["placed_at", "datetime", ""]]],
addresses: [3, 12, [["id", "bigint", "PK"], ["user_id", "bigint", "FK"], ["city", "text", ""], ["country", "char(2)", ""]]],
order_items: [33, 12, [["id", "bigint", "PK"], ["order_id", "bigint", "FK"], ["product_id", "bigint", "FK"], ["qty", "int", ""], ["price", "numeric", ""]]],
products: [64, 12, [["id", "bigint", "PK"], ["sku", "text", ""], ["name", "text", ""], ["price", "numeric", ""]]],
};
const W = 26;
const fy = (tb, i) => TABLES[tb][1] + 3 + i;
const RELS = [
["users", 0, "orders", 1, [[29, fy("users", 0)], [31, fy("users", 0)], [31, fy("orders", 1)], [32, fy("orders", 1)]], "5"],
["users", 0, "addresses", 1, [[2, fy("users", 0)], [1, fy("users", 0)], [1, fy("addresses", 1)], [2, fy("addresses", 1)]], "4"],
["orders", 0, "order_items", 1, [[59, fy("orders", 0)], [61, fy("orders", 0)], [61, fy("order_items", 1)], [59, fy("order_items", 1)]], "6"],
["products", 0, "order_items", 2, [[63, fy("products", 0)], [62, fy("products", 0)], [62, fy("order_items", 2)], [59, fy("order_items", 2)]], "a"],
];
export default function create() {
const { cols, rows } = meta;
const g = new Grid(cols, rows);
const paths = RELS.map((r) => cellsOf(r[4]));
return (t) => {
g.clear();
const STEP = 2.2;
const k = Math.floor(t / STEP) % RELS.length;
const p = Math.min(1, (t % STEP) / 0.7);
const [ta, fa, tb, fb, , rco] = RELS[k];
// relationship lines
paths.forEach((cells, i) => {
const on = i === k;
const n = on ? Math.max(1, Math.floor(p * cells.length)) : cells.length;
for (let j = 0; j < cells.length; j++) {
const [x, y] = cells[j];
const din = j ? dirOf(cells[j - 1], cells[j]) : dirOf(cells[j], cells[j + 1]);
const dout = j < cells.length - 1 ? dirOf(cells[j], cells[j + 1]) : din;
let ch = din === dout ? (din === "R" || din === "L" ? "─" : "│") : CORNER[din + dout];
if (j === 0) ch = "┼";
if (j === cells.length - 1) ch = din === "R" ? "<" : ">";
g.set(x, y, ch, on && j < n ? RELS[i][5] : "9");
}
});
// tables
for (const [name, [x, y, fields]] of Object.entries(TABLES)) {
const on = name === ta || name === tb;
const h = fields.length + 4;
const co = on ? rco : "1";
g.box(x, y, W, h, "round", co);
g.hline(x + 1, x + W - 2, y + 2, "─", co);
g.set(x, y + 2, "├", co); g.set(x + W - 1, y + 2, "┤", co);
g.text(x + 2, y + 1, name, on ? "8" : "0");
g.text(x + W - 2 - String(fields.length).length - 5, y + 1, fields.length + " cols", "9");
fields.forEach(([f, type, key], i) => {
const hot = (name === ta && i === fa) || (name === tb && i === fb);
g.text(x + 2, y + 3 + i, key ? key : " ", key === "PK" ? "7" : "3");
g.text(x + 5, y + 3 + i, f, hot ? "8" : key ? "0" : "1");
g.text(x + W - 2 - type.length, y + 3 + i, type, hot ? rco : "9");
if (hot && p >= 1) g.set(x + 1, y + 3 + i, "►", rco);
});
}
// the join, typed out
const join = `JOIN ${tb} ON ${tb}.${TABLES[tb][2][fb][0]} = ${ta}.${TABLES[ta][2][fa][0]}`;
const n = Math.floor(Math.min(1, (t % STEP) / 1.2) * join.length);
g.hline(0, cols - 1, rows - 2, "─", "2");
g.text(1, rows - 1, "sql>", "9");
g.text(6, rows - 1, join.slice(0, n), "0");
if (n < join.length || Math.floor(t * 3) % 2) g.set(6 + n, rows - 1, "█", "1");
g.text(cols - 22, rows - 1, "1 ─< many", "1");
g.text(cols - 11, rows - 1, "5 tables", "1");
return g.frame();
};
}
/YOUR OWN
The pieces here are hand-built animations. To turn your own photo, logo or video into animated ASCII, use the ASCII Magic editor.