'D3 selection.join() using classes
I'm trying to use the new D3 selection.join() paradigm to draw a randomly positioned circle within every svg. While I can get this to work using ids in the selector, I haven't been successful using classes for the selector. Since I don't have data associated with each circle or svg, I'm just synthesizing an array whose length is the number of selected elements. I've commented out returning the enter and update selectors, since enabling them results in the exception:
TypeError: r.compareDocumentPosition is not a function
at Pt.order (https://cdnjs.cloudflare.com/ajax/libs/d3/5.15.1/d3.min.js:2:14333)
at Pt.join (https://cdnjs.cloudflare.com/ajax/libs/d3/5.15.1/d3.min.js:2:13931)
at addCircle (c:\github\xxxxxxx\xxxxx\wwwroot\testd3.html:48:19)
at HTMLButtonElement.onclick (c:\github\xxxxx\xxxxx\wwwroot\testd3.html:20:36)
const randomColor = () => {
return "hsl(" + Math.random() * 360 + ",100%,50%)";
}
const addDiv = () => {
d3.select("div").append("svg")
.attr("width", 100)
.attr("height", 100)
.style("background", randomColor())
.classed("mysvg", true);
}
const addCircle = () => {
var svgs = d3.selectAll(".mysvg");
var nodes = svgs.nodes();
console.log('nodes: ' + nodes.length);
// add a random colored circle to each SVG.
svgs.select("circle")
// .data([1, 2], d=> d)
.data(d3.range(0, nodes.length), d => d)
.join(
enter => {
enter
.append('circle')
.attr("cx", d => 50 + Math.random() * 50 * d)
.attr("cy", d => 50 + Math.random() * 50 * d)
.attr("r", 10)
.style("fill", randomColor());
console.log('enter: ' + enter.nodes().length);
// return enter;
},
update => {
console.log('update: ' + update.nodes().length);
// return update;
},
exit => {
console.log('exit: ' + exit.nodes().length);
exit.remove();
}
);
}
.mysvg {}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.15.1/d3.min.js"></script>
<div>
<button onclick="addDiv()"> addDiv </button>
<button onclick="addCircle()"> addCircle </button>
<svg class="mysvg" style="background: lightblue" width=100 height=100>
</svg>
</div>
Solution 1:[1]
The problem is caused by this line:
svgs.select("circle")
As per the documentation ofd3.select
:
selects the first descendant element that matches the specified selector string
In order to select all circles for the data binding, d3.selectAll
should be used instead.
Adapted snippet below:
const randomColor = () => {
return "hsl(" + Math.random() * 360 + ",100%,50%)";
}
const addDiv = () => {
d3.select("div").append("svg")
.attr("width", 100)
.attr("height", 100)
.style("background", randomColor())
.classed("mysvg", true);
}
const addCircle = () => {
var svgs = d3.selectAll(".mysvg");
var nodes = svgs.nodes();
console.log('nodes: ' + nodes.length);
// add a random colored circle to each SVG.
svgs.selectAll("circle")
// .data([1, 2], d=> d)
.data(d3.range(0, nodes.length), d => d)
.join(
enter => {
enter
.append('circle')
.attr("cx", d => 50 + Math.random() * 50 * d)
.attr("cy", d => 50 + Math.random() * 50 * d)
.attr("r", 10)
.style("fill", randomColor());
console.log('enter: ' + enter.nodes().length);
// return enter;
},
update => {
console.log('update: ' + update.nodes().length);
// return update;
},
exit => {
console.log('exit: ' + exit.nodes().length);
exit.remove();
}
);
}
.mysvg {}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.15.1/d3.min.js"></script>
<div>
<button onclick="addDiv()"> addDiv </button>
<button onclick="addCircle()"> addCircle </button>
<svg class="mysvg" style="background: lightblue" width=100 height=100>
</svg>
</div>
Solution 2:[2]
The only way I could figure out to solve this was using selectAll() per Mehdi's suggestion, changing the selection to the .svg and moving the .append(circle) to the update. I also figured out the the .data() was superfluous.
const randomColor = () => {
return "hsl(" + Math.random() * 360 + ",100%,50%)";
}
const addDiv = () => {
d3.select("body").append("svg")
.attr("width", 100)
.attr("height", 100)
.style("background", randomColor())
.classed("mysvg", true);
}
const addCircle = () => {
var svgs = d3.selectAll(".mysvg");
// add a random colored circle to each SVG.
svgs
.join(
enter => {
console.log('enter: ' + enter.nodes().length);
return enter;
},
update => {
update
.append('circle')
.attr("cx", Math.random() * 100)
.attr("cy", Math.random() * 100)
.attr("r", 10)
.style("fill", randomColor());
console.log('update: ' + update.nodes().length);
return update;
},
exit => {
console.log('exit: ' + exit.nodes().length);
exit.remove()
}
);
}
.mysvg {}
<div>
<button onclick="addDiv()">addDiv</button>
<button onclick="addCircle()">addCircle</button>
<svg class="mysvg" style="background: lightblue" width=100 height=100> </svg>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.15.1/d3.js" charset="utf-8"></script>
</div>
Solution 3:[3]
Following this comment from gitHub: "... you are returning the enter selection from the join’s enter method; you need to return a materialized selection instead ...", it can be concluded that the enter selection cannot be returned directly, so I found that an option is to use a temporary selection, originating from .append()
:
// it works
.join(
enter => enter
.append("path")
.attr("class", "item")
.attr("fill", "red")
.attr("d", arc)
)
// it does not work
.join(
enter => {
enter
.append("path")
.attr("class", "item")
.attr("fill", "red")
.attr("d", arc);
return enter;
}
)
// it works!
.join(
enter => {
let sel = enter
.append("path")
.attr("class", "item")
.attr("fill", "red")
.attr("d", arc);
return sel;
}
)
So, with the last alternative, it is possible to do custom changes to selection, append new svg sub-items, etc.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Mehdi |
Solution 2 | Jay Borseth |
Solution 3 | ragan |