learn-sql-model/templates/er_diagram.html

129 lines
4.1 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 = {{tables}};
const links = {{links}};
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>