lib/template.html
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<title>{{title}}</title>
<style>
html {
box-sizing: border-box;
}
*,
*::before,
*::after {
box-sizing: inherit;
}
body {
margin: 0;
background-color: {{styles.colors.background}};
}
.node {
cursor: pointer;
}
.node circle {
fill: {{styles.colors.nodeFill}};
stroke: {{styles.colors.nodeStroke}};
stroke-width: 2px;
}
.node text {
font-family: sans-serif;
background: {{styles.colors.nodeFill}};
}
.node text.default {
font-size: 16px;
}
.node text.http-method {
font-size: 11px;
font-weight: bold;
}
.link {
fill: none;
stroke: {{styles.colors.edgeStroke}};
stroke-width: 2px;
}
.legendCells .label {
font: 16px sans-serif;
fill: {{styles.colors.foreground}};
}
</style>
</head>
<body>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/2.25.6/d3-legend.min.js"></script>
<script>
const routes = JSON.parse('{{data}}')
const styles = JSON.parse('{{styles}}')
const margin = {
top: 20,
right: 150,
bottom: 20,
left: 50
}
const width = document.body.clientWidth - margin.right - margin.left
const height = window.innerHeight - margin.top - margin.bottom
const duration = 250
let i = 0
let root
const svg = d3.select("body")
.append("svg")
.attr("width", width + margin.right + margin.left - 10)
.attr("height", height + margin.top + margin.bottom - 10)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
const treemap = d3.tree()
.size([ height, width ])
root = d3.hierarchy(routes, d => d.children)
root.x0 = height / 2
root.y0 = 0
const collapse = d => {
if (d.children) {
d._children = d.children;
d._children.map(collapse);
d.children = null;
}
}
const update = source => {
// Compute the new tree layout
const treeData = treemap(root)
const nodes = treeData.descendants()
const links = treeData.descendants().slice(1)
// Normalize for fixed-depth
nodes.forEach(d => { d.y = d.depth * 275 })
// Update the nodes
const node = svg.selectAll("g.node")
.data(nodes, d => d.id || (d.id = ++i))
// Enter any new nodes at the parent's previous position
const nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", d => "translate(" + source.y0 + "," + source.x0 + ")")
.on("click", click)
nodeEnter.append("circle")
.attr('class', 'node')
.attr("r", 1e-5)
nodeEnter.select('circle.node')
.style("fill", d => d.children && d.children.length
? styles.colors.circle.fill
: styles.colors.circle.leaf.fill
)
.style("stroke", d => d.children && d.children.length
? styles.colors.circle.stroke
: styles.colors.circle.leaf.stroke
)
const isHttpMethod = str => str.match(/GET|POST|DELETE|PUT|PATCH/)
// Add labels for the nodes
nodeEnter.append('text')
.attr('class', d => isHttpMethod(d.data.name) ? 'http-method' : 'default')
.attr("dy", ".35em")
.attr("x", d => 17)
.attr("text-anchor", "start")
.text(d => d.data.name)
.style("fill-opacity", 1)
.style('fill', d => d.data.name.match(/:/)
? styles.colors.parameterizedRoute
: isHttpMethod(d.data.name)
? styles.colors.nodeFill
: styles.colors.foreground
)
.call(selection => selection.each(function(d){ d.bbox = this.getBBox() }))
nodeEnter.insert("rect", "text")
.attr('class', d => isHttpMethod(d.data.name) ? 'http-method' : 'default')
.attr("width", d => d.bbox.width + 12)
.attr("height", d => d.bbox.height + (isHttpMethod(d.data.name) ? 8 : 15))
.attr("y", d => -1.0 * d.bbox.height)
.attr("x", d => 10)
.attr("rx", d => isHttpMethod(d.data.name) ? 0 : 4)
.attr("ry", d => isHttpMethod(d.data.name) ? 0 : 4)
.style("fill", d => {
return {
GET: styles.colors.httpMethods.GET.fill,
POST: styles.colors.httpMethods.POST.fill,
DELETE: styles.colors.httpMethods.DELETE.fill,
PUT: styles.colors.httpMethods.PUT.fill,
PATCH: styles.colors.httpMethods.PATCH.fill
}[d.data.name] || styles.colors.background
})
.style("stroke", d => isHttpMethod(d.data.name) ? styles.colors.nodeFill : styles.colors.nodeStroke)
// Transition nodes to their new position.
const nodeUpdate = nodeEnter.merge(node)
nodeUpdate.transition()
.duration(duration)
.attr("transform", d => "translate(" + d.y + "," + d.x + ")");
nodeUpdate.select("circle.node")
.attr("r", 5)
.attr('cursor', 'pointer')
// Transition exiting nodes to the parent's new position.
const nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", d => "translate(" + source.y + "," + source.x + ")")
.remove()
nodeExit.select("circle")
.attr("r", 1e-6)
nodeExit.select("text")
.style("fill-opacity", 1e-6)
nodeExit.select('rect')
.style("fill-opacity", 1e-6)
.style('stroke-opacity', 1e-6)
// Update the links…
const link = svg.selectAll("path.link")
.data(links, d => d.id)
// Enter any new links at the parent's previous position.
const linkEnter = link.enter().insert("path", "g")
.attr("class", "link")
.style("stroke-dasharray", ("3, 5"))
.attr("d", d => {
const o = { x: source.x0, y: source.y0 }
return diagonal(o, o)
})
const linkUpdate = linkEnter.merge(link)
// Transition links to their new position
linkUpdate.transition()
.duration(duration)
.attr("d", d => diagonal(d, d.parent))
// Transition exiting nodes to the parent's new position
linkExit = link.exit().transition()
.duration(duration)
.attr('d', d => {
const o = {x: source.x, y: source.y}
return diagonal(o, o)
})
.remove()
// Stash the old positions for transition
nodes.forEach(d => {
d.x0 = d.x
d.y0 = d.y
})
}
function diagonal(s, d) {
const path = `M ${s.y} ${s.x}
C ${(s.y + d.y) / 2} ${s.x},
${(s.y + d.y) / 2} ${d.x},
${d.y} ${d.x}`
return path
}
update(root)
const ordinal = d3.scaleOrdinal()
.domain([ 'endpoint', 'parameterized'])
.range([
styles.colors.foreground,
styles.colors.parameterizedRoute
]);
svg.append("g")
.attr("class", "legendOrdinal")
.attr("transform", "translate(" + (width - margin.left) + "," + margin.top + ")");
const legendOrdinal = d3.legendColor()
.shape("path", d3.symbol().type(d3.symbolSquare).size(150)())
.shapePadding(10)
.scale(ordinal)
svg.select(".legendOrdinal")
.call(legendOrdinal)
d3.select(self.frameElement).style("height", "1400px")
// Toggle children on click.
function click(d) {
if (d.children) {
d._children = d.children
d.children = null
} else {
d.children = d._children
d._children = null
}
update(d)
}
</script>
</body>