Published
Edited
Feb 23, 2020
1 star
Insert cell
Insert cell
Insert cell
function Point1() {
function chart(selection) {
// This is the Point side of the interface between Point and Plane.
// Point doesn't know where it is on the plane
// Only that it is given a selection which it assumes is a set of <g>'s
// ... upon which it will draw stuff.
selection
.append("circle")
.attr("r", d => d.r)
.attr("fill", "#cccccc")
selection
.append("text")
.text(d => d.a)
}
return chart
}
Insert cell
function XYPlane1() {
const defaultPointRenderer = () => {
return (selection) => {
selection.append("circle")
.attr("fill", "black")
.attr("r", 1)
}
}
let pointRenderer = defaultPointRenderer
function chart(selection) {
selection.selectAll("g")
.data((inheritedData, index, arrayOfNodes) => {
return inheritedData
})
.join("g")
.attr("transform", d => `translate(${d.x}, ${d.y})`)
.call(pointRenderer())
// This join("g") is where the <g> is actually joined,
// and then the selection is passed to the pointRenderer()
// which will put stuff into that node
}
// How do we configure/change what pointRenderer we use?
// Here, by defining a configuration function, I can pass
// in a new renderer.
chart.pointRenderer = (newRenderer) => {
pointRenderer = newRenderer
return chart
}
return chart
}
Insert cell
Insert cell
data = [
{ x: 10, y: 20, a: "0", r: 10 },
{ x: 120, y: 20, a: "1", r: 20 },
{ x: 120, y: 130, a: "2", r: 30 }
]
Insert cell
{
const svg = d3.create('svg')
.attr("viewbox", [0, 0, width, width]);
const plane = svg.append("g")
.datum(data)
.call(
XYPlane1()
.pointRenderer(Point1) // Composition happens here; We're asking XYPlane1 to use Point1 as the renderer
)
return svg.node()
}
Insert cell
Insert cell
Insert cell
{
const svg = d3.create('svg')
.attr("viewbox", [0, 0, width, width]);
const plane = svg.append("g")
.datum(changingData)
.call(
XYPlane1()
.pointRenderer(Point1) // Composition happens here; We're asking XYPlane1 to use Point1 as the renderer
)
return svg.node()
}
Insert cell
Insert cell
function XYPlane2() {
const defaultPointRenderer = () => {
return (selection) => {
selection.append("circle")
.attr("fill", "black")
.attr("r", 1)
}
}
const defaultTransition = d3.transition()
.duration(200)
.ease(d3.easeLinear);
let pointRenderer = defaultPointRenderer
let t = defaultTransition
function chart(selection) {
selection.selectAll("g")
.data((inheritedData, index, arrayOfNodes) => {
return inheritedData
})
.join( // Here I emulated the code in https://observablehq.com/@d3/selection-join
enter => {
return enter.append("g")
.call(enter => enter
.transition(t)
.attr("transform", d => `translate(${d.x}, ${d.y})`)
)
},
update => {
return update
.call(update => update
.transition(t)
.attr("transform", d => `translate(${d.x}, ${d.y})`)
)
},
exit => {
return exit
.call(exit => exit.transition(t).attr("transform", d => `translate(${width}, ${width})`).remove())
}
)
.call(pointRenderer())
}
chart.pointRenderer = (newRenderer) => {
pointRenderer = newRenderer
return chart
}
return chart
}
Insert cell
{
const svg = d3.create('svg')
.attr("viewbox", [0, 0, width, width]);
const plane = svg.append("g")
.datum(changingData)
.call(
XYPlane2()
.pointRenderer(Point1)
)
return svg.node()
}
Insert cell
Insert cell
chart2 = {
const svg = d3.create('svg')
.attr("viewbox", [0, 0, width, width]);
const plane = svg.append("g")
// Ok, what's going on here?
return Object.assign(
svg.node(), // This is the node
{ // and we're giving the node an `update` method
update(data) {
plane.datum(data)
.call(
XYPlane2()
.pointRenderer(Point1)
)
}
}
)
}
Insert cell
chart2.update(changingData)
Insert cell
Insert cell
Insert cell
chart3.update(changingData)
Insert cell
function Point2() {
const defaultTransition = d3.transition()
.duration(500)
.ease(d3.easeLinear);
let t = defaultTransition
function chart(selection) {
selection.each(function(p, j) {
let g = d3.select(this)
let circle = g.select("circle")
if (circle.empty()) {
circle = g.append("circle")
.attr("fill", "#cccccc")
}
circle
.transition(t)
.attr("r", d => d.r)
let text = g.select("text")
if (text.empty()) {
text = g.append("text")
.text(d => d.a)
}
// This feels not quite right..
// Is there a way to do this with selection.join?
})

return selection
}
return chart
}
Insert cell
md`## The inner component transitions! ... but feels wrong? 🤔

I couldn't figure out how to use enter/update/exit here, so instead I used selection.each and added the &lt;circle&gt; and &lt;text&gt; manually for each point. It works, and seems to achieve the effect I want.

... but I can't help but feel there might be a better, more d3 idiomatic way of doing this.

Is there?
`
Insert cell
Insert cell
chart4.update(changingData)
Insert cell
function Point3() {
const defaultTransition = d3.transition()
.duration(500)
.ease(d3.easeLinear);
let t = defaultTransition
function chart(selection) {
selection
.selectAll("circle")
.data(d => d)
.join(
(enter) => {
// This doesn't append anywhere!
enter.append("circle")
.attr("fill", "#cccccc")
.attr("r", d => d.r)
return enter
}
)

return selection
}
return chart
}
Insert cell
md`# Help! 🤦‍♂️

This is the one that I couldn't get to work. I can't help but think the secret is in Nested Selections: https://bost.ocks.org/mike/nest/ but I couldn't figure it out...`
Insert cell
d3 = require("d3@5")
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