<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ name }}</title>
<style>
* {
margin: 0 0;
}
*::-webkit-scrollbar {
width: 5px;
height: 5px;
}
*::-webkit-scrollbar-corner {
background-color: transparent;
}
*::-webkit-scrollbar-thumb {
border-radius: 10px;
-webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
background: #49b1f5;
}
*::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
border-radius: 10px;
background: #ededed;
}
*::-webkit-slider-container {
width: 5px;
height: 5px;
}
input {
width: 300px;
}
button {
background-color: #49b1f5;
border: none;
color: white;
text-align: center;
text-decoration: none;
display: inline-block;
border-radius: 4px;
}
button:hover {
box-shadow: 0 6px 8px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);
}
</style>
</head>
<body>
<div style="overflow: hidden;">
<div id="content"
style="overflow-x: hidden; position: fixed; height: 100vh; overflow-y: auto; background-color: rgb(244, 237, 237); width: 255px; z-index: 100;">
</div>
<div id="super" style="position: relative; overflow-y: visible; overflow-x: visible; margin-left: 255px; min-height: 100vh;">
<div id="container"
style="overflow: visible; height: 100%; background-color: transparent; display: flex;justify-content: center; z-index: 1;">
<div id="tree" style="overflow: visible;"></div>
</div>
</div>
<div style="position: absolute; left: 260px; top: 5px;">
<button id="reset">复位(reset)</button>
<button id="switch">转换为cluster (use cluster)</button>
<span>
dx
<input id="slider" type="range" min="32" max="512" step="1" value="128" />
</span>
<span>
dy
<input id="slider2" type="range" min="32" max="512" step="1" value="96" />
</span>
</div>
</div>
<script type="module">
{{ ast }}
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
const maxScale = 4, minScale = 0.05;
let config = {
tree_or_cluster: d3.tree,
current: ast,
scale: 1.0,
isPointerdown: false,
lastPointermove: { x: 0, y: 0 },
x: 0,
y: 0,
ast_width: 128,
ast_height: 96,
};
const content = indentTree(ast);
document.getElementById('content').appendChild(content);
const tree = collapsibleTree(ast, { tree: config.tree_or_cluster, dx: config.ast_width, dy: config.ast_height });
document.getElementById('tree').appendChild(tree);
bindEvents(document.getElementById('super'), document.getElementById('container'));
document.getElementById('reset').addEventListener('click', e => reset());
document.getElementById('switch').addEventListener('click', e => {
if (e.target.innerHTML.endsWith('(use cluster)')) {
config.tree_or_cluster = d3.cluster;
e.target.innerHTML = '转换为 tree (use tree)';
render();
}
else {
config.tree_or_cluster = d3.tree;
e.target.innerHTML = '转换为 cluster (use cluster)';
render();
}
});
document.getElementById('slider').onchange = (e) => {
config.ast_width = e.target.value;
render();
};
document.getElementById('slider2').onchange = (e) => {
config.ast_height = e.target.value;
render();
};
function indentTree(data, { nodeSize = 25, minWidth = 255 } = {}) {
let width = minWidth;
const root = d3.hierarchy(data);
root.descendants().forEach((d, i) => {
d.id = i;
d._children = d.children;
d.y = i * nodeSize;
const tmp = d.depth * nodeSize + d.data.name.length * 13 + 6;
width = tmp > width ? tmp : width;
});
const svg = d3.create("svg")
.attr("viewBox", [-nodeSize / 2, -nodeSize * 3 / 2, width, 2 * nodeSize])
.attr("font-family", "sans-serif")
.attr("font-size", 18)
.style("overflow", "visible");
const gLink = svg.append("g")
.attr("fill", "none")
.attr("stroke", "#999");
const gNode = svg.append("g");
function update(source, bias_y) {
root.eachBefore((d, i) => {
d.y = i * nodeSize;
});
const duration = 800;
const nodes = root.descendants();
const links = root.links();
const transition = svg.transition()
.duration(duration)
.attr("viewBox", [-nodeSize / 2, -nodeSize * 3 / 2, width, (nodes.length + 1) * nodeSize])
.tween("resize", window.ResizeObserver ? null : () => () => svg.dispatch("toggle"));
const node = gNode.selectAll("g").data(nodes, d => d.id);
const nodeEnter = node.enter().append("g")
.attr("transform", d => `translate(0, ${bias_y})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0);
nodeEnter.append("circle")
.attr("cx", d => d.depth * nodeSize)
.attr("r", 5)
.attr("fill", d => d._children ? null : "#999")
.on("click", (event, d) => {
d.children = d.children ? null : d._children;
update(d, d.y);
});
nodeEnter.append("text")
.attr("dy", "0.32em")
.attr("x", d => d.depth * nodeSize + 8)
.text(d => d.data.name)
.attr("cursor", "pointer")
.on("click", (event, d) => {
config.current = d.data;
render();
})
.clone(true).lower()
.attr("stroke-linejoin", "round")
.attr("stroke-width", 3)
.attr("stroke", "white");
node.merge(nodeEnter).transition(transition)
.attr("transform", d => `translate(0, ${d.y})`)
.attr("fill-opacity", 1)
.attr("stroke-opacity", 1);
node.exit().transition(transition).remove()
.attr("transform", d => `translate(0, ${bias_y})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0);
const link = gLink.selectAll("path")
.data(links, d => d.target.id);
const linkEnter = link.enter().append("path")
.attr("d", d => `
M${d.source.depth * nodeSize},${bias_y}
V${bias_y}
h${nodeSize}
`);
link.merge(linkEnter).transition(transition)
.attr("d", d => `
M${d.source.depth * nodeSize},${d.source.y}
V${d.target.y}
h${nodeSize}
`);
link.exit().transition(transition).remove()
.attr("d", d => `
M${d.source.depth * nodeSize},${bias_y}
V${bias_y}
h${nodeSize}`)
}
update(root, 0);
return svg.node();
}
function collapsibleTree(data, {
id = Array.isArray(data) ? d => d.id : null,
parentId = Array.isArray(data) ? d => d.parentId : null,
tree = d3.tree,
r = 5,
padding = 1,
stroke = "#555",
strokeWidth = 1.5,
strokeOpacity = 0.4,
curve = d3.curveBumpX,
dx = 128,
dy = 128,
margin = { top: 40, right: 120, bottom: 10, left: 40 }
} = {}) {
const root = d3.hierarchy(data);
const diagonal = d3.linkVertical(curve).x(d => d.x).y(d => d.y);
const tree_func = tree().nodeSize([dx, dy]);
root.descendants().forEach((d, i) => {
d.id = i;
d._children = d.children;
});
const svg = d3.create("svg")
.attr("font-size", 14)
.style("overflow", "visible")
.style("user-select", "none");
const gLink = svg.append("g")
.attr("fill", "none")
.attr("stroke", "#555")
.attr("stroke-opacity", 0.4)
.attr("stroke-width", 1.5);
const gNode = svg.append("g")
.attr("pointer-events", "all");
function update(source, from_x, from_y) {
const duration = d3.event && d3.event.altKey ? 2500 : 300;
const nodes = root.descendants().reverse();
const links = root.links();
tree_func(root);
let left = root;
let right = root;
let top = root;
let bottom = root;
root.eachBefore(node => {
if (node.x < left.x) left = node;
if (node.x > right.x) right = node;
if (node.y < top.y) top = node;
if (node.y > bottom.y) bottom = node;
});
const width = right.x - left.x + margin.left + margin.right;
const height = bottom.y - top.y + margin.top + margin.bottom;
const transition = svg.transition()
.duration(duration)
.attr('height', height)
.attr('width', width)
.attr("viewBox", [-margin.left + left.x, top.y - margin.top, width, height])
.tween("resize", window.ResizeObserver ? null : () => () => svg.dispatch("toggle"));
const node = gNode.selectAll("g")
.data(nodes, d => d.id);
const nodeEnter = node.enter().append("g")
.attr("transform", d => `translate(${from_x},${from_y})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0);
nodeEnter.append("title")
.text(d => d._children ? d.data.id: d.data.name);
nodeEnter.append("text")
.attr("y", 0)
.attr('font-weight', 'bold')
.attr("cursor", d => d._children ? "pointer" : "default")
.attr('fill', d => d._children ? "#333" : "#999")
.attr("text-anchor", "middle")
.text(d => d._children ? d.data.name: d.data.text)
.on("pointerdown", (event, d) => {
d.children = d.children ? null : d._children;
update(d, d.x, d.y);
})
.clone(true).lower()
.attr("stroke-linejoin", "round")
.attr("stroke-width", 3)
.attr("stroke", "white");
const nodeUpdate = node.merge(nodeEnter).transition(transition)
.attr("transform", d => `translate(${d.x},${d.y})`)
.attr("fill-opacity", 1)
.attr("stroke-opacity", 1);
const nodeExit = node.exit().transition(transition).remove()
.attr("transform", d => `translate(${source.x},${source.y})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0);
const link = gLink.selectAll("path")
.data(links, d => d.target.id);
const linkEnter = link.enter().append("path")
.attr("d", d => {
const o = { x: from_x, y: from_y };
return diagonal({ source: o, target: o });
});
link.merge(linkEnter).transition(transition)
.attr("d", diagonal);
link.exit().transition(transition).remove()
.attr("d", d => {
const o = { x: source.x, y: source.y };
return diagonal({ source: o, target: o });
});
}
update(root, 0, 0);
return svg.node();
}
function bindEvents(parent, element) {
document.body.onresize = () => {
setSize(element);
}
parent.addEventListener('wheel', (e) => {
let ratio = 1.1;
if (e.deltaY > 0) {
ratio = 1 / 1.1;
}
config.scale *= ratio;
if (config.scale > maxScale) config.scale = maxScale;
else if (config.scale < minScale) config.scale = minScale;
const transform = `translateX(${config.x}px) translateY(${config.y}px) scale(${config.scale})`;
element.style.transform = transform;
e.preventDefault();
});
parent.addEventListener('pointerdown', (e) => {
if (e.button == 0) {
config.isPointerdown = true;
element.setPointerCapture(e.pointerId);
config.lastPointermove = { x: e.clientX, y: e.clientY };
}
});
parent.addEventListener('pointerup', (e) => {
if (e.button == 0) {
config.isPointerdown = false;
e.preventDefault();
}
});
parent.addEventListener('pointermove', (e) => {
if (config.isPointerdown) {
const current = { x: e.clientX, y: e.clientY };
const dx = current.x - config.lastPointermove.x;
const dy = current.y - config.lastPointermove.y;
config.lastPointermove = { x: current.x, y: current.y };
config.x += dx; config.y += dy;
const transform = `translateX(${config.x}px) translateY(${config.y}px) scale(${config.scale})`;
element.style.transform = transform;
e.preventDefault();
}
});
setSize(element);
}
function setSize(element) {
if (element.scrollWidth > window.innerWidth || element.scrollHeight > window.innerHeight) {
element.style.width = window.innerWidth + 'px';
element.style.height = window.innerHeight + 'px';
}
}
function reset() {
config.scale = 1.0;
config.isPointerdown = false, config.lastPointermove = { x: 0, y: 0 };
config.x = 0, config.y = 0;
const element = document.getElementById('container');
element.style.transform = 'translateX(0px) translateY(0px) scale(1)';
}
function render() {
const tree = collapsibleTree(config.current, { tree: config.tree_or_cluster, dx: config.ast_width, dy: config.ast_height });
const dom = document.getElementById('tree');
dom.innerHTML = '';
dom.appendChild(tree);
reset();
}
</script>
</body>
</html>