Published
Edited
Dec 2, 2019
Importers
Insert cell
md`# Tree view`
Insert cell
tree = new TreeRenderer(400)
Insert cell
tree.render(data)
Insert cell
data = ({
value: 1,
fill:'#1f1f1f',
children: [{
children: [{
children: [], fill: '#1f1f1f'
},{children: [], fill: '#1f1f1f'}]},{children: []},]
})
Insert cell
// {
// tree.addEventListener('selected', ({node}) => {
// console.log('selected', node)
// })
// tree.addEventListener('unselected', ({node}) => {
// console.log('unselected', node)
// })
// }
Insert cell
class TreeRenderer extends EventTarget {
constructor(height) {
super()
this.height = height
this.container = DOM.element('div')
this.svg = d3.select(DOM.svg(width, height))
this.container.append(this.svg.node())
const root = this.svg.append('g')
this.root = root
this.tooltip = DOM.element('div')
this.tooltip.className = 'tooltip'
this.container.append(this.tooltip)

// event handling
this.offset = { x: width / 2, y: height / 2 }
this.scale = 1 // todo calc auto scale
const node = this.svg.node()
let mousedown = false
this.svg.on('mousedown', () => mousedown = true)
window.addEventListener('mouseup', e => mousedown = false)
window.addEventListener('mousemove', e => {
if (mousedown)
this.offset = {
x: this.offset.x + e.movementX,
y: this.offset.y + e.movementY
}
e.preventDefault()
})
node.addEventListener('mousewheel', e => {
this.scale -= Math.sign(e.deltaY) / 3
e.preventDefault()
})
node.addEventListener('dblclick', e => {
// todo: fix animations
// this.root.style('transition-duration', '0.3s')
this.moveHome()
// setTimeout(() => this.root.style('transition-duration', ''), 350)
})
}
render(tree) {
const treeRoot = d3.hierarchy(tree)
const tr = d3.tree().nodeSize([20, 20])(treeRoot)
this.root.remove()
this.root = this.svg.append('g')
this.updateTransforms()
this.root.append("g")
.classed('links', true)
.selectAll('line.link')
.data(treeRoot.links())
.enter()
.append('line')
.classed('link', true)
.attr('stroke', d => '#1f1f1f')
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y)
const nodes = this.root
.append("g")
.classed('nodes', true)
const node = nodes
.selectAll('g.node')
.data(treeRoot.descendants())
.enter()
.append('g')
.classed('node', true)
.attr('data-type', d => d.data.type)
.attr('transform', d => `translate(${d.x},${d.y})`)

const that = this
let selected = null
node
.append('ellipse')
.attr('rx', 5)
.attr('ry', 5)
.attr('stroke', d => d.data.active ? 'red' : '#1f1f1f')
.attr('fill', d => d.data.fill || 'red')
.style('cursor', 'pointer')
.on('click', function (node) {
if (selected == this) {
d3.select(selected).attr('stroke-width', 1)
const unselectedEvent = new Event('unselected')
unselectedEvent.node = node
that.dispatchEvent(unselectedEvent)
selected = null
that.selected = null
that.tooltip.innerText = ''
}
else {
if (selected) {
d3.select(selected).attr('stroke-width', 1)
const unselectedEvent = new Event('unselected')
unselectedEvent.node = node
that.dispatchEvent(unselectedEvent)
that.tooltip.innerText = ''
}
selected = this
that.tooltip.innerText = node.data.display || node.data.name || node.data.value || ''
d3.select(this).attr('stroke-width', 2.2)
const selectedEvent = new Event('selected')
selectedEvent.node = node
that.dispatchEvent(selectedEvent)
that.selected = node
}
})
.on('mouseover', function(node) {
})
setTimeout(() => this.moveHome(), 1)
return this.container
}
updateTransforms() {
this.root.attr('transform', `translate(${this._offset.x}, ${this._offset.y}), scale(${this.scale})`)
}
autoScale() {
const {width: bbWidth,height} = this.root.node().getBoundingClientRect()
const dw = (width - bbWidth)
const dh = (this.height - height)
if (dh < dw) this.scale = (this.height - 120) / (height / this.scale)
else this.scale = (width - 120) / (bbWidth / this.scale)
}
moveHome() {
this.autoScale()
const {height} = this.root.node().getBoundingClientRect()
this.offset = { x: width / 2, y: (this.height - height) / 2 + 5 * this.scale }
}
set offset(value) {
this._offset = value
this.updateTransforms()
}
get offset() {
return this._offset || {x:width/2,y:30}
}
set scale(value) {
this._scale = Math.max(0.5, value)
this.updateTransforms()
}
get scale() {
return this._scale || 1
}
}
Insert cell
d3 = import('d3')
Insert cell
html`<style>
.tooltip {
position: absolute;
line-height: 30px;
background: #f1f1f1;
right: 0;
top: 0;
min-width: 100px;
height: 30px;
text-align: center;
font-family: sans-serif;
}
</style>`
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more