129 lines
No EOL
4.4 KiB
HTML
129 lines
No EOL
4.4 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<title>ER Diagram</title>
|
|
<!-- Include d3.js -->
|
|
<script src="https://d3js.org/d3.v6.min.js"></script>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
}
|
|
|
|
.table {
|
|
font-family: Arial, sans-serif;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.table-name {
|
|
font-weight: bold;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.foreign-key {
|
|
fill: #b30000;
|
|
}
|
|
|
|
.link {
|
|
stroke: #999;
|
|
stroke-opacity: 0.6;
|
|
stroke-width: 2px;
|
|
fill: none;
|
|
}
|
|
|
|
.link-curved-path {
|
|
pointer-events: none;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div id="er-diagram"></div>
|
|
<script>
|
|
// Generate ER diagram
|
|
const tables = [{'name': 'pet', 'columns': ['name', 'birthday', 'id'], 'foreign_keys': []}, {'name': 'hero', 'columns': ['name', 'secret_name', 'x', 'y', 'size', 'age', 'shoe_size', 'pet_id', 'id'], 'foreign_keys': [{'id': 0, 'from': 'pet_id', 'to_table': 'pet', 'to': 'id'}]}];
|
|
const links = [{'source': {'x': 320, 'y': 230}, 'target': {'x': 50, 'y': 130}}];
|
|
|
|
const width = window.innerWidth;
|
|
const height = window.innerHeight;
|
|
|
|
const tableElemWidth = 120;
|
|
const tableElemHeight = d => 20 * (d.columns.length + 1);
|
|
|
|
let svg = d3.select("#er-diagram")
|
|
.append("svg")
|
|
.attr("width", width)
|
|
.attr("height", height);
|
|
|
|
let g = svg.append("g");
|
|
|
|
let linkGroup = g.selectAll(".link")
|
|
.data(links)
|
|
.join("path")
|
|
.attr("class", "link");
|
|
|
|
let tableGroup = g.selectAll(".table")
|
|
.data(tables)
|
|
.join("g")
|
|
.attr("class", "table")
|
|
.classed("collapsed", false)
|
|
.on("click", (event, d) => {
|
|
d3.select(event.currentTarget).classed("collapsed", !d3.select(event.currentTarget).classed("collapsed"));
|
|
});
|
|
|
|
let zoomBehavior = d3.zoom()
|
|
.scaleExtent([0.1, 4])
|
|
.on("zoom", function (event) {
|
|
g.attr("transform", event.transform);
|
|
});
|
|
|
|
svg.call(zoomBehavior);
|
|
|
|
let rect = tableGroup.append("rect")
|
|
.attr("width", tableElemWidth)
|
|
.attr("height", tableElemHeight)
|
|
.attr("fill", "#eee");
|
|
|
|
let text = tableGroup.append("text")
|
|
.attr("class", "table-name")
|
|
.attr("x", 10)
|
|
.attr("y", 20)
|
|
.text(d => d.name);
|
|
|
|
let columnText = tableGroup.selectAll(".column")
|
|
.data(d => d.columns.map(col => ({name: col, is_foreign_key: d.foreign_keys.some(fk => fk.from === col)})))
|
|
.join("text")
|
|
.attr("class", d => d.is_foreign_key ? "column foreign-key" : "column")
|
|
.attr("x", 10)
|
|
.attr("y", (d, i) => 40 + i * 20)
|
|
.text(d => d.name);
|
|
|
|
// Physics simulation and force layout
|
|
let simulation = d3.forceSimulation(tables)
|
|
.force("link", d3.forceLink(links).id(d => d.name).distance(200))
|
|
.force("charge", d3.forceManyBody().strength(-800))
|
|
.force("x", d3.forceX(width / 2).strength(0.1))
|
|
.force("y", d3.forceY(height / 2).strength(0.1))
|
|
.on("tick", () => {
|
|
tableGroup.attr("transform", d => `translate(${d.x}, ${d.y})`);
|
|
linkGroup.attr("d", d => {
|
|
const srcX = d.source.x + tableElemWidth;
|
|
const srcY = d.source.y + 40 + d.source.columns.findIndex(c => c === d.source_col) * 20;
|
|
const tgtX = d.target.x;
|
|
const tgtY = d.target.y + 40 + d.target.columns.findIndex(c => c === d.target_col) * 20;
|
|
const deltaX = tgtX - srcX;
|
|
const deltaY = tgtY - srcY;
|
|
const curveFactor = 50;
|
|
const curveY = deltaY < 0 ? -curveFactor : curveFactor;
|
|
return `M${srcX},${srcY}C${srcX + deltaX / 2},${srcY + curveY} ${tgtX - deltaX / 2},${tgtY - curveY} ${tgtX},${tgtY}`;
|
|
});
|
|
columnText.style("display", (d, i, nodes) => {
|
|
return d3.select(nodes[i].parentNode).classed("collapsed") ? "none" : null;
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
|
|
</html> |