Public
Edited
May 17, 2024
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function getTree({
branchCount=7,
randomizeAngle=false,
randomizeLength=false,
randomizeThickness=false,
seed=randomSeed,
}={}) {
function branching(source, i) {
if (i > 0) { source.children = [{},{}].map(s => branching(s, i-1)) }
else { source.children = [{}]}
return source
}

const rngAngle = d3.randomNormal.source(d3.randomLcg(seed + 0))()
const rngLength = d3.randomNormal.source(d3.randomLcg(seed + 1))()
const rngThickness = d3.randomNormal.source(d3.randomLcg(seed + 2))()

// 使用d3生成一个层级对象
// const trunk = d3.hierarchy(branching({}, branchCount))
const trunk = d3.hierarchy(getAlgoTreeData())
const numTotal = trunk.count().value; // number of nodes in tree
console.log(numTotal)
const π = Math.PI
trunk.eachBefore(d => {
const numSub = Math.pow(2, d.height+1)-1 // number of descendents of this node
// const numSub=;
if (d === trunk) {
d.idx = 0
d.x = 0
d.y = 0
d.θ = π / 2
d.dx = 0
d.dy = 1.3 * d.height / 9
d.sign = -1
} else {
d.x = d.parent.x + d.parent.dx
d.y = d.parent.y + d.parent.dy

d.idx = d === trunk ? 0 : d.parent.children.indexOf(d)
const sign = d.sign = d.parent.children.length === 1 ? d.parent.sign : d.idx || -1
const weightFactor = numSub / numTotal
do {
d.θ = d.parent.θ -
sign * π * Math.max(.1, 0.3333 / Math.min(60, d.depth + 1)) +
.125 * -sign * π * Math.pow(weightFactor, .5) +
(randomizeAngle ? rngAngle() * .25 : 0)

} while (Math.abs(d.θ - d.parent.θ) < .1)

const μ = .5 * Math.pow(1 / d.depth, .5) *
(randomizeLength ? /*.125*/.333 + .666 * Math.abs(rngLength()) : 1)

d.dx = Math.cos(d.θ) * μ
d.dy = Math.sin(d.θ) * μ
}
d.thickness = d.parent ? d.parent.thickness * d.parent.taper / 2 : Math.max(1, numSub) * 0.12 *
1 / Math.pow(2, branchCount)
randomizeThickness && d.depth > 0 && (d.thickness *= 1 + .125 * Math.abs(rngThickness()))
const cosθpπ2 = Math.cos(d.θ + π/2),
sinθpπ2 = Math.sin(d.θ + π/2),
cosθmπ2 = Math.cos(d.θ - π/2),
sinθmπ2 = Math.sin(d.θ - π/2)
d.taper = Math.pow(.8, tapering * Math.pow(1 - taperingFalloff, d.depth))
d.interpolateL = d3.interpolate([
d.x + cosθpπ2 * d.thickness/2,
d.y + sinθpπ2 * d.thickness/2
], [
d.x + d.dx + cosθpπ2 * d.thickness/2 * d.taper,
d.y + d.dy + sinθpπ2 * d.thickness/2 * d.taper,
])
d.interpolateR = d3.interpolate([
d.x + cosθmπ2 * d.thickness/2,
d.y + sinθmπ2 * d.thickness/2
], [
d.x + d.dx + cosθmπ2 * d.thickness/2 * d.taper,
d.y + d.dy + sinθmπ2 * d.thickness/2 * d.taper,
])
})
trunk.eachBefore(d => {
// d.bL = !d.parent || d.height < 1 ? d.interpolateL(0).concat() : lineLineIntersection(
// d.sign === 1 ? d.parent.children[0].interpolateR(0).concat() : d.parent.interpolateL(0).concat(),
// d.sign === 1 ? d.parent.children[0].interpolateR(1).concat() : d.parent.interpolateL(1).concat(),
// d.interpolateL(0).concat(), d.interpolateL(1).concat()
// )
// d.bR = !d.parent || d.height < 1 ? d.interpolateR(0).concat() : lineLineIntersection(
// d.sign === -1 ? d.parent.children[1].interpolateL(0).concat() : d.parent.interpolateR(0).concat(),
// d.sign === -1 ? d.parent.children[1].interpolateL(1).concat() : d.parent.interpolateR(1).concat(),
// d.interpolateR(0).concat(), d.interpolateR(1).concat()
// )
d.interpolators = [
d === trunk && d3.interpolate([-1.05, -.05], [1.05, -.05]),
d === trunk && d3.interpolate([-1, 0], [1, 0]),
d3.interpolate(d.bL, d.bR),
d === trunk
? d3.interpolate(
d.interpolateL(.75).map((c,i) => i === 1 ? c*.25 : c),
d.interpolateR(.75).map((c,i) => i === 1 ? c*.25 : c)
)
: null,
d === trunk
? d3.interpolate(d.interpolateL(.9).concat(), d.interpolateR(.9).concat())
: d.height > 1 ? d3.interpolate(d.interpolateL(.75).concat(), d.interpolateR(.75).concat()) : null,
].filter(Boolean)
})

return trunk
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function renderHierarchy(container, root, {
viewSize=[100, 100], bundleSize,
circles, thicknesses, branching,
parallelPaths, smoothParallelPaths, pathColor,
}={}) {
let layers
if (Array.isArray(container)) {
layers = container
container = layers[0]
}

const bounds = d3.zip(
d3.extent(root.descendants(), d => d.x),
d3.extent(root.descendants(), d => d.y)
)
const scale = Math.min(
viewSize[0] / (bounds[1][0] - bounds[0][0]),
viewSize[1] / (bounds[1][1] - bounds[0][1])
) * .95

const sel = container ? d3.select(container) : d3.create('g')
const g = sel.selectAll('g.nodes')
.data([null])
.join('g').attr('class', 'nodes')
.attr('transform', `translate(${
viewSize[0] / 2 -
scale * (bounds[0][0] + bounds[1][0]) / 2
} ${
viewSize[1] / 2 +
scale * (bounds[0][1] + bounds[1][1]) / 2
}) scale(${scale} ${-scale})`)
g.selectAll('circle.node')
.data(!circles ? [] : root.descendants())
.join('circle').attr('class', 'node')
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', 2/scale)

g.selectAll('line.link')
.data(branching ? root.links() : [])
.join('line').attr('class', 'link')
.attr('x1', l => l.source.x)
.attr('y1', l => l.source.y)
.attr('x2', l => l.target.x)
.attr('y2', l => l.target.y)
.attr('stroke', '#333')
.attr('stroke-width', 1/scale)

g.selectAll('line.thickness')
.data(!thicknesses ? [] : root.links())
.join('line').attr('class', 'thickness')
.attr('x1', l => (l.target.sign === -1 ? l.source.bL : l.source.bR)[0])
.attr('y1', l => (l.target.sign === -1 ? l.source.bL : l.source.bR)[1])
.attr('x2', l => (l.target.sign === -1 ? l.target.bL : l.target.bR)[0])
.attr('y2', l => (l.target.sign === -1 ? l.target.bL : l.target.bR)[1])
.attr('stroke', '#000')
.attr('stroke-width', 1/scale)
.attr('stroke-dasharray', `${4/scale} ${3/scale}`)

g.selectAll('line.thickness2')
.data(!thicknesses ? [] : root.descendants().flatMap(d => d.interpolators.map(interpolate => [
interpolate(0).concat(), interpolate(1).concat()
])))
.join('line').attr('class', 'thickness2')
.attr('x1', l => l[0][0])
.attr('y1', l => l[0][1])
.attr('x2', l => l[1][0])
.attr('y2', l => l[1][1])
.attr('stroke', '#00c')
.attr('stroke-width', 1/scale)
//.attr('stroke-dasharray', `${4/scale} ${3/scale}`)
const color = pathColor || (
(d, i, a) => '#33d' && (d3.interpolateTurbo || d3.interpolateRdBu)(i/a.length)
)
const ordinal = d3.scaleOrdinal().range(layers ? layers.map((l,i) => i) : [0])
;(layers ? d3.selectAll(layers) : sel)
.selectAll('path.parallel')
.data(!parallelPaths ? [] : (_, k) => getPaths(root, {bundleSize}).map((path, i, a) => {
path.color = color(path, i, a)
return path
}).filter(path => ordinal(path.color) === k))
.join('path').attr('class', 'parallel')
.attr('stroke', path => path.color).attr('fill', 'none')
.attr('stroke-width', 1*.25 / scale)
//.style('opacity', .5)
.attr('d', d3.line().curve(smoothParallelPaths ? d3.curveBasis : d3.curveLinear))
.attr('transform', `translate(${
viewSize[0] / 2 -
scale * (bounds[0][0] + bounds[1][0]) / 2
} ${
viewSize[1] / 2 +
scale * (bounds[0][1] + bounds[1][1]) / 2
}) scale(${scale} ${-scale})`)

return sel.node()
}
Insert cell
s=d3.scaleOrdinal().range(['A', 'B', 'C'])
Insert cell
s('g')
Insert cell
function getPaths(tree, { bundleSize=1}={}) {
return tree.leaves().map((leaf, i, a) => leaf.path(tree).reverse()//.slice(1)
// .filter(d => d.height > 1)
.flatMap(d => {
const leaves = (d || d.parent || d).leaves()
const bundled = Math.pow(2, bundleSize - 1)
const index = Math.floor(leaves.indexOf(leaf) / bundled) * bundled
const frac = (index + .5) / leaves.length
return Object.assign(
d.interpolators
.map(interpolate => interpolate(frac).concat())
.slice(0, d.height + 1)// slice because at the tips we don't want midpoints
.concat(d.height <= 1 ? [d.interpolators[d.interpolators.length-1](frac).concat()] : []),
{}
)
})
)
}
Insert cell
function lineLineIntersection(A,B,C,D){
// Line AB represented as a1x + b1y = c1
const a1 = B[1] - A[1]
const b1 = A[0] - B[0]
const c1 = a1*(A[0]) + b1*(A[1])

// Line CD represented as a2x + b2y = c2
const a2 = D[1] - C[1]
const b2 = C[0] - D[0]
const c2 = a2*(C[0])+ b2*(C[1])

const determinant = a1*b2 - a2*b1

if (determinant == 0) { return null }
else {
return [
(b2*c1 - b1*c2)/determinant,
(a1*c2 - a2*c1)/determinant,
]
}
}
Insert cell
Insert cell
thumbSz = [2*200,2*200]
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more