{
const height = width / 2;
const svg = d3.create('svg')
.attr("width", width)
.attr("height", height)
.attr("viewBox", `0 0 ${width} ${height}`)
.attr("style", "font: 12px sans-serif; user-select: none;");
const gRoot = svg.append("g").attr("aria-label", "org-chart")
.attr("transform","translate(0, 0) scale(1)");
const gLinks = gRoot.append("g").attr("aria-label", "links");
const gNodes = gRoot.append("g").attr("aria-label", "nodes");
const dx = 100;
const dy = 360;
const nodeWidth = dy - 100;
const nodeHeight = dx - 40;
const margin = {top: 20, right: 20, bottom: 20, left: 20}
const tree = d3.tree()
.nodeSize([dx, dy])
.separation((a, b) => a.parent == b.parent ? 1 : 1.5);
let zoom = d3.zoom()
.scaleExtent([0.5, 2])
.on('zoom', handleZoom);
function handleZoom(e) {
console.log({transform: e.transform})
d3.select('svg g')
.attr('transform', e.transform);
}
const drawLink = d3.linkHorizontal()
.source(d => (
{
...d.source,
y: d.source.y + d.source.offsetY + nodeWidth,
x: d.source.x + d.source.offsetX + nodeHeight / 2
}
))
.target(d => (
{
...d.target,
y: d.target.y + d.target.offsetY,
x: d.target.x + d.target.offsetX + nodeHeight / 2
}
))
.x(d => d.y)
.y(d => d.x)
function dragstarted(event, d) {
const node = d3.select(this)
.raise()
node.select("rect")
.attr("stroke", "black");
console.log("dragStart", d)
}
function dragged(event, d) {
d.offsetX += event.dy;
d.offsetY += event.dx;
d3.select(this)
.attr("transform", `translate(${d.y + d.offsetY}, ${d.x + d.offsetX})`);
gLinks.selectAll("path")
.filter(p => p.target.id == d.id || p.source.id == d.id)
.attr("d", drawLink);
}
function dragended(event, d) {
const node = d3
.select(this)
node.select("rect")
.attr("stroke", "#eee");
console.log("dragEnd", d)
}
const drag = d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
function update(event, source) {
tree(hierarchy)
const nodes = hierarchy.descendants().reverse()
const edges = hierarchy.links();
const transition = svg.transition()
.duration(500)
.tween("resize", window.ResizeObserver ? null : () => () => svg.dispatch("toggle"));
const node = gNodes.selectAll("g")
.data(nodes, d => d.id);
const nodeEnter = node.enter()
.append("g")
.attr("id", d => `node-${d.id}`)
.attr("transform", d => `translate(${source.y0 + source.offsetY}, ${source.x0 + source.offsetX})`)
.attr("fill-opacity", 1)
.attr("stroke-opacity", 1)
.on("dblclick", (event, d) => {
d.children = d.children ? null : d._children;
update(event, d);
})
.call(drag)
nodeEnter
.append("rect")
.attr("width", nodeWidth)
.attr("height", nodeHeight)
.attr("fill", "white")
.attr("stroke", "#eee")
.attr("rx", 10)
.style("filter","drop-shadow(2px 4px 4px rgba(0, 0, 0, 0.1))")
nodeEnter
.append("defs")
.append("clipPath")
.attr("id", d => `clip-${d.id}`)
.append("rect")
.attr("width", nodeWidth)
.attr("height", nodeHeight)
.attr("rx", 10)
nodeEnter
.append("image")
.attr("clip-path", d => `url(#clip-${d.id})`)
.attr("rx", 10)
.attr("width", nodeHeight)
.attr("height", nodeHeight)
.attr("href",d => `https://i.pravatar.cc/300?img=${d.id}`)
nodeEnter
.append("text")
.attr("id", "nodeName")
.attr("fill", "black")
.attr("font-weight", "bold")
.attr("dy", 20)
.attr("dx", nodeHeight + 10)
nodeEnter
.append("text")
.attr("id", "nodeTitle")
.attr("fill", "black")
.attr("dy", 40)
.attr("dx", nodeHeight + 10)
.text(d => d.data.title)
nodeEnter.append("text")
.attr("id", "nodeExpand")
.attr("dy", nodeHeight/2)
.attr("dx", nodeWidth - 20)
.attr("fill", "black")
.attr("fill-opacity", 0)
.attr("alignment-baseline", "central")
.text("⛶")
const nodeUpdate = node.merge(nodeEnter);
nodeUpdate.transition(transition)
.attr("transform", d => `translate(${d.y + d.offsetY}, ${d.x + d.offsetX})`)
nodeUpdate.select("#nodeName")
.text(d => d.data.name)
nodeUpdate.select("#nodeTitle")
.text(d => d.data.title)
nodeUpdate.select("#nodeExpand")
.transition(transition)
.attr("fill-opacity", d => d._children ? 1 : 0)
const nodeExit = node.exit()
.transition(transition)
.attr("transform", d => `translate(${source.y + source.offsetY},${source.x + source.offsetX})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0)
.remove()
const link = gLinks.selectAll("path").data(edges, d => d.target.id);
const linkEnter = link.enter()
.append('path')
.attr("id", d => `edge-${d.target.id}`)
.attr("stroke", "#ccc")
.attr("fill", "none")
.attr("d", d => {
const o = {
x: source.x0,
offsetX: source.offsetX,
y: source.y0,
offsetY: source.offsetY
};
return drawLink({source: o, target: o});
})
const linkUpdate = link.merge(linkEnter)
.transition(transition)
.attr("d", drawLink)
const linkExit = link.exit()
.remove()
hierarchy.eachBefore(d => {
d.x0 = d.x;
d.y0 = d.y;
});
}
svg.call(zoom)
.on("dblclick.zoom", null);
svg.call(zoom.transform, d3.zoomIdentity.translate(10, 300))
gRoot.attr("transform",`translate(10, 300) scale(1)`)
hierarchy.descendants().forEach((d,i) => {
d._children = d.children;
d.offsetX = 0;
d.offsetY = 0;
if (d.depth > 0) d.children = null
})
update(null, hierarchy)
return svg.node()
}