d3js v5: Create a SVG with groups from a JSON file - d3.js

I am new to d3js and I want to create the following SVG from a JSON file. I am using d3js version 5.16.
<svg width="1024" height="700">
<g id="gp_node1">
<rect x="0" y="50" width="1024" height="10" id="node1"></rect>
<g id="gm_node1">
<circle cy="200" r="10" id="node1"></circle>
<text x="0" y="10" id="textnode1">node1</text>
</g>
</g>
<g id="gp_node2">
<rect x="0" y="100" width="1024" height="10" id="node2"></rect>
<g id="gm_node2">
<circle cx="200" cy="200" r="10" id="subnode2"></circle>
<text x="0" y="10" id="textsubnode2">subnode2</text>
</g>
</g>
</svg>
It worked (except the attributes) but I do not think, that it is the best way to do it, especially the update function (had to comment the return). The code I ended up with is:
var json = {"mainnodes":[{"name":"node1","y":50,"subnodes":[{"name":"subnode1","x":100}]},{"name":"node2","y":100,"subnodes":[{"name":"subnode2","x":200}]}]};
var json2 = {"mainnodes":[{"name":"node1","y":50,"subnodes":[{"name":"subnode1","x":100}]},{"name":"node2","y":150,"subnodes":[{"name":"subnode2","x":200}]}]};
var data = json;
var structure;
var svgContainer = d3.select("body")
.append("svg")
.attr("width", 1024)
.attr("height", 700);
function update(){
structure = svgContainer.selectAll("g")
.data(data.mainnodes)
.join(
function(enter) {
return enter
.append("g")
.attr("id",function(d){return "gp_"+d.name;})
.append("rect")
.attr("x",0)
.attr("y",function(d){return d.y;})
.attr("width",1024)
.attr("height",10)
.attr("id",function(d){return d.name;})
.each(function(d) {
var g = d3.select(this.parentNode)
.append("g")
.attr("id",function(d){return "gm_"+d.name;})
g.data(d.subnodes)
.append("circle")
.attr("cx",function(d){return d.x;})
.attr("cy",200)
.attr("r",10)
.attr("id",function(d){return d.name;});
g.data(d.subnodes)
.append("text")
.attr("x",0)
.attr("y",10)
.attr("id",function(d){return "text"+d.name;})
.text(function(d){return d.name;});
})
},
function(update) {
var s = d3.select("g");
s.attr("id",function(d){return "gp_"+d.name;});
s.select("rect")
.attr("x",0)
.attr("y",function(d){return d.y;})
.attr("width",1024)
.attr("height",10)
.attr("id",function(d){return d.name;});
var g = s.select("g");
g.attr("id",function(d){return "gm_"+d.name;});
g.select("circle")
.attr("cx",function(d){return d.x;})
.attr("cy",200)
.attr("r",10)
.attr("id",function(d){return d.name;});
g.select("text")
.attr("x",0)
.attr("y",10)
.attr("id",function(d){return "text"+d.name;})
.text(function(d){return d.name;});
//return update;
},
function(exit) {
return exit;
}
)
}
d3.select('svg').on('click', function(){
data = json2;
update();
})
update();
In all examples I saw so far the enter function had only one element appended (so update and exit is always is referring to this one element only). But if I split the append functions up into separate joins, I am not able to get the SVG structure I need. I already tried a lot of variations. Maybe someone can show me how to do it correctly. Thanks!

This is how I would do it (if I am not allowed to change the data structure?). Does this help? If I can change the data structure, I can do it easier and cleaner. Since D3 is "Data Driven Documents", I normally start with that.
const json = {
"mainnodes":[
{"name":"node1","y":50,"subnodes":[{"name":"subnode1","x":100}]},
{"name":"node2","y":75,"subnodes":[{"name":"subnode2","x":25}]}
]
};
const json2 = {
"mainnodes":[
{"name":"node3","y":50,"subnodes":[{"name":"subnode1","x":100}]},
{"name":"node4","y":150,"subnodes":[{"name":"subnode2","x":200}]}
]
};
const data = [json, json2];
let currIndex = 0;
const svg = d3.select("body")
.append("svg")
.attr("width", 1024)
.attr("height", 700);
function update(data){
const parentGroups = svg.selectAll('.gp')
.data(data.mainnodes);
parentGroups.enter()
.append("g")
.attr('class', 'gp')
.merge(parentGroups)
.transition()
.duration(750)
.attr("id",function(d){return "gp_"+d.name;});
const gr = parentGroups.selectAll('.gr')
.data(d => [d]);
gr.enter()
.append("rect")
.attr('class', 'gr')
.merge(gr)
.transition()
.duration(750)
.attr("x",0)
.attr("y",function(d){return d.y;})
.attr("width",1024)
.attr("height",10)
.attr("id",function(d){return d.name;});
const subGroups = parentGroups.selectAll('.gm')
.data(d => [d]);
subGroups.enter()
.append('g')
.attr('class', 'gm')
.attr("id",function(d){return "gm_"+d.name;})
.merge(subGroups)
.transition()
.duration(750);
const c_shapes = subGroups.selectAll('.circles')
.data(d => d.subnodes);
c_shapes.enter()
.append('circle')
.attr('class', 'circles')
.attr("id",function(d){return d.name;})
.merge(c_shapes)
.transition()
.duration(750)
.attr("cx",function(d){return d.x;})
.attr("cy",200)
.attr("r",10);
const t_shapes = subGroups.selectAll('.texts')
.data(d => d.subnodes);
t_shapes.enter()
.append('text')
.attr('class', 'texts')
.attr("id",function(d){return "text"+d.name;})
.merge(t_shapes)
.transition()
.duration(750)
.attr("x",function(d){return d.x;})
.attr("y",230)
.text(function(d){return d.name;});
///*
parentGroups.exit().remove();
gr.exit().remove();
subGroups.exit().remove();
c_shapes.exit().remove();
t_shapes.exit().remove();
//*/
}
console.log('start');
//not ideal, I think there is a way to avoid it but it is not coming to mind...
update(data[currIndex]);
update(data[currIndex]);
update(data[currIndex]);
svg.on('click', function(){
currIndex = (currIndex + 1) % 2;
//console.log('click', 'currIndex: ' + currIndex);
update(data[currIndex]);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<!-- desired output:
<svg width="1024" height="700">
<g id="gp_node1">
<rect x="0" y="50" width="1024" height="10" id="node1"></rect>
<g id="gm_node1">
<circle cy="200" r="10" id="node1"></circle>
<text x="0" y="10" id="textnode1">node1</text>
</g>
</g>
<g id="gp_node2">
<rect x="0" y="100" width="1024" height="10" id="node2"></rect>
<g id="gm_node2">
<circle cx="200" cy="200" r="10" id="subnode2"></circle>
<text x="0" y="10" id="textsubnode2">subnode2</text>
</g>
</g>
</svg>
-->
Output:
An alternative approach, changing the data structure and using d3-selection-multi:
const json = {
"mainnodes":[
{"shape":"g","name":"gp_node1",
"subnodes":[
{"shape":"rect","name":"rectnode1","width":1024,"height":10,"y":50,"x":0},
{"shape":"g","name":"gm_node1",
"subnodes":[
{"shape":"circle","name":"circlenode1","cy":150,"cx":200,"r":10},
{"shape":"text","name":"textnode1","text":"subnode1","y":170,"x":200},
]
}
]
},
{"shape":"g", "name":"gp_node2",
"subnodes":[
{"shape":"rect","name":"rectnode2","width":1024,"height":10,"y":75,"x":0},
{"shape":"g","name":"gm_node2",
"subnodes":[
{"shape":"circle","name":"circlenode2","cy":150,"cx":100,"r":10},
{"shape":"text","name":"textnode2","text":"subnode2","y":170,"x":100},
]
}
]
}
]
};
const json2 = {
"mainnodes":[
{"shape":"g","name":"gp_node3",
"subnodes":[
{"shape":"rect","name":"rectnode3","width":1024,"height":10,"y":50,"x":0},
{"shape":"g","name":"gm_node3",
"subnodes":[
{"shape":"circle","name":"circlenode3","cy":150,"cx":250},
{"shape":"text","name":"textnode3","text":"subnode3","y":170,"x":250},
]
}
]
},
{"shape":"g", "name":"gp_node4",
"subnodes":[
{"shape":"rect","name":"rectnode4","width":1024,"height":10,"y":100,"x":0},
{"shape":"g","name":"gm_node2",
"subnodes":[
{"shape":"circle","name":"circlenode2","cy":150,"cx":100},
{"shape":"text","name":"textnode2","text":"subnode4","y":170,"x":100},
]
}
]
}
]
};
const data = [json, json2];
let currIndex = 0;
const svg = d3.select("body")
.append("svg")
.attr("width", 1024)
.attr("height", 700);
function update(data){
const parentGroups = svg.selectAll('.gp')
.data(data.mainnodes);
parentGroups.enter()
.append("g")
.attr('class', 'gp')
.merge(parentGroups)
.transition()
.duration(750)
.attr("id",function(d){return d.name;});
const subGroups_d1 = parentGroups.selectAll('.gm')
.data(d => d.subnodes);
subGroups_d1.enter()
.append(d => document.createElementNS("http://www.w3.org/2000/svg", d.shape))
.attr('class', 'gm')
.attr("id",function(d){return d.name;})
.merge(subGroups_d1)
.transition()
.duration(750)
.attrs(d => {
const copy = {...d}
delete copy.shape;
delete copy.name;
return copy;
});
const subGroups_d2 = subGroups_d1.selectAll('.gn')
.data(d => d.subnodes || []);
subGroups_d2.enter()
.append(d => document.createElementNS("http://www.w3.org/2000/svg", d.shape))
.attr('class', 'gn')
.attr("id",function(d){return d.name;})
.merge(subGroups_d2)
.transition()
.duration(750)
.attrs(d => {
const copy = {...d}
delete copy.shape;
delete copy.name;
return copy;
})
.text(d => d.text || "");
///*
parentGroups.exit().remove();
subGroups_d1.exit().remove();
subGroups_d1.exit().remove();
//*/
}
console.log('start');
//not ideal, I think there is a way to avoid it but it is not coming to mind...
update(data[currIndex]);
update(data[currIndex]);
update(data[currIndex]);
svg.on('click', function(){
currIndex = (currIndex + 1) % 2;
//console.log('click', 'currIndex: ' + currIndex);
update(data[currIndex]);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
<!-- desired output:
<svg width="1024" height="700">
<g id="gp_node1">
<rect x="0" y="50" width="1024" height="10" id="node1"></rect>
<g id="gm_node1">
<circle cy="200" r="10" id="node1"></circle>
<text x="0" y="10" id="textnode1">node1</text>
</g>
</g>
<g id="gp_node2">
<rect x="0" y="100" width="1024" height="10" id="node2"></rect>
<g id="gm_node2">
<circle cx="200" cy="200" r="10" id="subnode2"></circle>
<text x="0" y="10" id="textsubnode2">subnode2</text>
</g>
</g>
</svg>
-->

Related

text align center for tspan text block

Below sample generate text block but current test is align left, how to align text by center?
demo()
function demo() {
var svg = d3.select('body').append('svg')
.attr('width','100')
.attr('height','100')
.attr('transform','translate(50,50)')
var text = ["12","123456789","1234"]
var fontsize = 40
var txt = svg.append('text')
.attr("text-anchor", 'start')
.attr("dominant-baseline","middle")//text-before-edge
.attr("font-size",fontsize)
.attr('alignment-baseline','middle')
var tspan = txt.selectAll('tspan')
.data(text)
.join('tspan')
.attr('class','tspan')
.attr("x", 0)
.attr('y',function(d,i) {
return i*fontsize
})
.text(d => d)
}
<script src="https://d3js.org/d3.v7.min.js"></script>
Update
After apply #herrstrietzel's answer, I create below demo to show the usecase to put text box at specific position, currently it always at the center of svg, I wish it deployed at specific location (x,y).
demo()
function demo() {
var svg = d3.select('body').append('svg')
.attr('width', 300)
.attr('height', 200)
.style('background','#ececec')
add_grid(svg)
var data = [
{
text:["B", "BB", "BBB",'BBBB'],
x:50,
y:50
},
{
text:["AAAAA", "AAA", "A"],
x:150,
y:100
}
]
var fontsize = 20
var txt = svg.selectAll('.box')
.data(data)
.join('text')
.attr("text-anchor", 'middle')
.attr("x", '50%')
.attr("y", '0%')
.attr("dominant-baseline", "central")
.attr("font-size", fontsize)
.style('border','1px solid red')
.attr('x',d => d.x)
.attr('y',d => d.y)
var tspan = txt.selectAll('tspan')
.data(d => d.text)
.join('tspan')
.attr('class', 'tspan')
.attr("x", '50%')
.attr('dy', function(d) {
return fontsize * 1.2
})
.text(d => d)
function add_grid(svg) {
var w = +svg.attr('width')
var step = 10
var mygrid = function(d) {
return `M 0,${d} l ${w},0 M ${d},0 l 0,${w}`
}
var grid = []
for(var i = 0; i < w; i+=step) {
grid.push(i);
}
svg.append('g')
.selectAll(null)
.data(grid).enter()
.append('path')
.attr('d',d => mygrid(d))
.attr('fill','none')
.attr('stroke','green')
.attr('stroke-width',.5)
}
}
<script src="https://d3js.org/d3.v7.min.js"></script>
Your text is aligned left since your text-anchor value is "start" instead of "middle".
Working example
demo()
function demo() {
var svg = d3.select('body').append('svg')
.attr('width', '100')
.attr('height', '100')
//.attr('viewBox','0 0 100 100')
var text = ["12", "123456789", "1234"]
var fontsize = 20
var txt = svg.append('text')
.attr("text-anchor", 'middle')
.attr("x", '50%')
.attr("y", '0%')
.attr("dominant-baseline", "central")
.attr("font-size", fontsize)
var tspan = txt.selectAll('tspan')
.data(text)
.join('tspan')
.attr('class', 'tspan')
.attr("x", '50%')
.attr('dy', function(d) {
return fontsize * 1.2
})
.text(d => d)
}
svg {
border: 1px solid red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.6.1/d3.min.js"></script>
dy can actually be better suited to mimic line breaks.
Worth mentioning:
there are no block-like elements in svg:
the parent svg won't automatically grow or shrink according to it's content/child node dimensions ( so you need to calculate an appropriate height and with depending on your text length)
<text> elements need a x value of "50%" or "viewBox width/2" to be centered
I strongly recommend to test the best <text> properties "statically" (e.g. in a codepen or a static html)
Second example:
You need to set the same x value for both <text> and <tspan> elements to mimic a center aligned text block.
Red circles illustrate the desired horizontal/vertical center points.
demo();
function demo() {
var svg = d3
.select("body")
.append("svg")
.attr("width", 300)
.attr("height", 200)
.style("background", "#ececec");
add_grid(svg);
var data = [
{
text: ["B", "BB", "BBB", "BBBB"],
x: 50,
y: 50
},
{
text: ["AAAAA", "AAA", "A"],
x: 150,
y: 100
}
];
var fontsize = 20;
var txt = svg
.selectAll(".box")
.data(data)
.join("text")
.attr("text-anchor", "middle")
.attr("dominant-baseline", "text-bottom")
.attr("font-size", fontsize)
.attr("x", (d) => d.x)
.attr("y", (d) =>{
let textL = d.text.length;
let yOffset = d.y - (fontsize * 1.2 * textL/2);
return (yOffset);
});
var tspan = txt
.selectAll("tspan")
.data((d) => d.text)
.join("tspan")
.attr("class", "tspan")
.attr("x", function (d) {
let currentX = d3.select(this.parentNode).attr("x");
return currentX;
})
.attr("dy", function (d) {
return fontsize * 1.2;
})
.text((d) => d);
function add_grid(svg) {
var w = +svg.attr("width");
var step = 10;
var mygrid = function (d) {
return `M 0,${d} l ${w},0 M ${d},0 l 0,${w}`;
};
var grid = [];
for (var i = 0; i < w; i += step) {
grid.push(i);
}
svg
.append("g")
.selectAll(null)
.data(grid)
.enter()
.append("path")
.attr("d", (d) => mygrid(d))
.attr("fill", "none")
.attr("stroke", "green")
.attr("stroke-width", 0.5);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.6.1/d3.min.js"></script>
<svg width="300" height="200" style="background: rgb(236, 236, 236);">
<g>
<path fill="none" stroke="green" stroke-width="0.5" d="M0 0l300 0m-300 0l0 300m0-290l300 0m-290-10l0 300m-10-280l300 0m-280-20l0 300m-20-270l300 0m-270-30l0 300m-30-260l300 0m-260-40l0 300m-40-250l300 0m-250-50l0 300m-50-240l300 0m-240-60l0 300m-60-230l300 0m-230-70l0 300m-70-220l300 0m-220-80l0 300m-80-210l300 0m-210-90l0 300m-90-200l300 0m-200-100l0 300m-100-190l300 0m-190-110l0 300m-110-180l300 0m-180-120l0 300m-120-170l300 0m-170-130l0 300m-130-160l300 0m-160-140l0 300m-140-150l300 0m-150-150l0 300m-150-140l300 0m-140-160l0 300m-160-130l300 0m-130-170l0 300m-170-120l300 0m-120-180l0 300m-180-110l300 0m-110-190l0 300m-190-100l300 0m-100-200l0 300m-200-90l300 0m-90-210l0 300m-210-80l300 0m-80-220l0 300m-220-70l300 0m-70-230l0 300m-230-60l300 0m-60-240l0 300m-240-50l300 0m-50-250l0 300m-250-40l300 0m-40-260l0 300m-260-30l300 0m-30-270l0 300m-270-20l300 0m-20-280l0 300m-280-10l300 0m-10-290l0 300" />
</g>
<circle cx="50" cy="50" r="2%" fill="red"/>
<text text-anchor="middle" dominant-baseline="text-bottom" font-size="20" x="50" y="2" style="border: 1px solid red;">
<tspan class="tspan" x="50" dy="24">B</tspan>
<tspan class="tspan" x="50" dy="24">BB</tspan>
<tspan class="tspan" x="50" dy="24">BBB</tspan>
<tspan class="tspan" x="50" dy="24">BBBB</tspan>
</text>
<circle cx="150" cy="100" r="2%" fill="red"/>
<text text-anchor="middle" dominant-baseline="text-bottom" font-size="20" x="150" y="64" style="border: 1px solid red;">
<tspan class="tspan" x="150" dy="24">AAAAA</tspan>
<tspan class="tspan" x="150" dy="24">AAA</tspan>
<tspan class="tspan" x="150" dy="24">A</tspan>
</text>
</svg>

Create Tooltip for SVG <image> tag

I am attempting to create a tooltip that will appear when the user hover overs the icon, the tootltip will give information regarding the event. The tag is within an SVG image.
This is the code for the image:
<image class="tooltip-trigger" data-tooltip-text="Battles" id="Battles" x="100" y="200" width="30" height="30" xlink:href="battle.png" />
<g id="tooltip" visibility="hidden" >
<rect x="2" y="2" width="80" height="24" fill="black" opacity="0.4" rx="2" ry="2"/>
<rect width="80" height="24" fill="white" rx="2" ry="2"/>
<text x="3" y="6">Tooltip</text>
</g>
<script type="text/ecmascript"><![CDATA[
(function() {
var svg = document.getElementById('tooltip-svg-5');
var tooltip = svg.getElementById('tooltip');
var tooltipText = tooltip.getElementsByTagName('text')[0].firstChild;
var triggers = svg.getElementsByClassName('tooltip-trigger');
for (var i = 0; i < triggers.length; i++) {
triggers[i].addEventListener('mousemove', showTooltip);
triggers[i].addEventListener('mouseout', hideTooltip);
}
function showTooltip(evt) {
var CTM = svg.getScreenCTM();
var x = (evt.clientX - CTM.e + 6) / CTM.a;
var y = (evt.clientY - CTM.f + 20) / CTM.d;
tooltip.setAttributeNS(null, "transform", "translate(" + x + " " + y + ")");
tooltip.setAttributeNS(null, "visibility", "visible");
tooltipText.data = evt.target.getAttributeNS(null, "data-tooltip-text");
}
function hideTooltip(evt) {
tooltip.setAttributeNS(null, "visibility", "hidden");
}
})()
]]></script>
When loaded, the tooltip does not appear
Your code is working. The problem you may have is that the tooltip appears mostly outside the svg element. I've added overflow:visible; to the svg element and now you can see the tooltip.
(function() {
var svg = document.getElementById('tooltip-svg-5');
var tooltip = svg.getElementById('tooltip');
var tooltipText = tooltip.getElementsByTagName('text')[0].firstChild;
var triggers = svg.getElementsByClassName('tooltip-trigger');
for (var i = 0; i < triggers.length; i++) {
triggers[i].addEventListener('mousemove', showTooltip);
triggers[i].addEventListener('mouseout', hideTooltip);
}
function showTooltip(evt) {
var CTM = svg.getScreenCTM();
var x = (evt.clientX - CTM.e + 6) / CTM.a;
var y = (evt.clientY - CTM.f + 20) / CTM.d;
tooltip.setAttributeNS(null, "transform", "translate(" + x + " " + y + ")");
tooltip.setAttributeNS(null, "visibility", "visible");
tooltipText.data = evt.target.getAttributeNS(null, "data-tooltip-text");
}
function hideTooltip(evt) {
tooltip.setAttributeNS(null, "visibility", "hidden");
}
})()
svg{border:1px solid; overflow:visible;display:block;margin:auto}
<svg id="tooltip-svg-5" viewBox="0 -8 130 238" width="200">
<image class="tooltip-trigger" data-tooltip-text="Battles" id="Battles" x="100" y="200" width="30" height="30" xlink:href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/222579/pin.png" />
<g id="tooltip" visibility="hidden">
<rect x="2" y="2" width="80" height="24" fill="black" opacity="0.4" rx="2" ry="2"/>
<rect width="80" height="24" fill="white" rx="2" ry="2"/>
<text x="3" y="6">Tooltip</text>
</g>
</svg>
Another option would have been adding text-anchor="end" to the text and rewriting the rects accordingly.
Yet another potion would have been using an HTML element for the tooltip.

D3 svg text position right

I am wondering if there is a way to get svg text of an element to align to the right. It is currently working, positioned left and wonder if I am missing something simple.
svg.append("text")
.attr("class", "role-title")
.text(function (d) {return d.role;})
.call(wrapText, 190)
.attr('transform', function (d) {
if (d.place === 1) {
return 'translate(75,-40)'; ... // parameter for right align
<text class="role-title" transform="translate(-240, 50)">
<tspan x="0" dy="0em">Computer and Information</tspan>
<tspan x="0" dx="0" dy="1.1em">Research Scientist</tspan>
</text>

D3.js - Why are all my data being appended to the last appended group?

I'm pretty new to D3.js, and I've been able to create a simple animation that suits my needs from the Health and Wealth of Nations example. I had some trouble with adding labels to SVG circles, and when I investigated the source I found that while a group is being appended for each object in my dataset, all of the circles are being added to the last group only.
JSON data:
{"numSegments":4,
"movement":[
{"Id":"1",
"xValues":[[0,350],[1,400],[2,350],[3,300],[4,350]],
"yValues":[[0,690],[1,640],[2,590],[3,640],[4,690]]},
{"Id":"2",
"xValues":[[0,150],[1,150],[2,150],[3,150], [4, 150]],
"yValues":[[0,690],[1,490],[2,690],[3,490],[4, 690]]},
{"Id":"3",
"xValues":[[0,550],[1,650],[2,550],[3,650],[4,550]],
"yValues":[[0,690],[1,590],[2,490],[3,390],[4,490]]},
{"Id":"4",
"xValues":[[0,0],[1,200],[2,0],[3,200],[4,0]],
"yValues":[[0,0],[1,200],[2,0],[3,200],[4,0]]}
]
}
Relevant JavaScript:
var chart = d3.select(".chart")
.attr("width", width)
.attr("height", height)
var g = chart.append("g")
.selectAll(".node")
.data(interpolateData(0));
var sections = g.enter().append("g")
.attr("class", "node");
console.log(sections.size()); // this returns "4" as expected
var dot = sections.append("circle")
.attr("class", "dot")
.attr("fill", function (d) { return colour(key(d)); })
.attr("r", "10")
.call(position)
.sort(order);
dot.append("title")
.text(function (d) { return key(d); });
var text = sections.append("text")
.text(function (d) { return key(d); })
.call(positionText)
.sort(order);
SVG output:
<svg class="chart" width="700" height="700">
<g>
<g class="node"></g>
<g class="node"></g>
<g class="node"></g>
<g class="node">
<circle class="dot" fill="#fff" r="10">
<title>1</title>
</circle>
<circle class="dot" fill="#fff" r="10">
<title>2</title>
</circle>
<circle class="dot" fill="#fff" r="10">
<title>3</title>
</circle>
<circle class="dot" fill="#fff" r="10">
<title>4</title>
</circle>
<text>1</text>
<text>2</text>
<text>3</text>
<text>4</text>
</g>
</g>
If the size of sections is 4 as the console reports, is it not an array of the four <g> elements that were just appended? And if so, why does sections.append() add four elements to the last group instead of adding one element to each of the four groups?

d3js : How to select nth element of a group?

I create a group with 9 elements (circles) such as:
// JS
var data=[ 1,2,3,4,5,6,7,8,9 ];
var svg = d3.select("body").append("svg");
var circles = svg.append("g").attr("id", "groupOfCircles")
.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d){ return d*20;})
.attr("cy", function(d){ return d*10;})
.attr("r" , function(d){ return d;})
.attr("fill","green");
//xml
<svg>
<g id="groupOfCircles">
<circle cx="20" cy="10" r="1" fill="green"></circle>
<circle cx="40" cy="20" r="2" fill="green"></circle>
<circle cx="60" cy="30" r="3" fill="green"></circle>
<circle cx="80" cy="40" r="4" fill="green"></circle>
<circle cx="100" cy="50" r="5" fill="green"></circle>
<circle cx="120" cy="60" r="6" fill="green"></circle>
<circle cx="140" cy="70" r="7" fill="green"></circle>
<circle cx="160" cy="80" r="8" fill="green"></circle>
<circle cx="180" cy="90" r="9" fill="green"></circle>
</g>
</svg>
But How to select the nth element (i.e : the 3rd circle) of the group groupOfCircles while not knowing the circles' id or attributes values ?
I will later on loop over all elements via a for loop, and color each for one second.
Note: I tried things such as :
circles[3].attr("fill","red") // not working
d3.select("#groupOfCircles:nth-child(3)").attr("fill","red") // not working
..
The selector needs to be circle:nth-child(3) -- the child means that the element is the nth child, not to select the nth child of the element (see here).
You could also use:
// JS
var data=[ 1,2,3,4,5,6,7,8,9 ];
var svg = d3.select("body").append("svg");
var circles = svg.append("g").attr("id", "groupOfCircles")
.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d){ return d*20;})
.attr("cy", function(d){ return d*10;})
.attr("r" , function(d){ return d;})
.attr("fill","green");
d3.select("circle:nth-child(3)").attr("fill","red"); // <== CSS selector (DOM)
d3.select(circles[0][4]).attr("fill","blue"); // <== D3 selector (node)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
d3 v4 now supports using selection.nodes() to return an array of all elements of this selection. Then, we can change the attributes of nth element by d3.select(selection.nodes()[n]).attr(something)
// JS
var data=[ 1,2,3,4,5,6,7,8,9 ];
var svg = d3.select("body").append("svg");
var circles = svg.append("g").attr("id", "groupOfCircles")
.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d){ return d*20;})
.attr("cy", function(d){ return d*10;})
.attr("r" , function(d){ return d;})
.attr("fill","green");
circleElements = circles.nodes(); // <== Get all circle elements
d3.select(circleElements[6]).attr("fill","red");
<script src="https://d3js.org/d3.v4.min.js"></script>
You can also use your circles array to set the element's attribute:
d3.select(circles[3]).attr("fill","red");
// JS
var data=[ 1,2,3,4,5,6,7,8,9 ];
var svg = d3.select("body").append("svg");
var circles = svg.append("g").attr("id", "groupOfCircles")
.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d){ return d*20;})
.attr("cy", function(d){ return d*10;})
.attr("r" , function(d){ return d;})
.attr("fill","green");
var group = document.querySelector('#groupOfCircles');
var circleNodes = group.getElementsByTagName('circle');
d3.select(circleNodes[3]).attr("fill", "red");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
If you want to do it in d3 logic, the anonymous function always has an index parameter aside the data:
my_selection.attr("fill",function(d,i) {return i%3==0?"red":"green";});
http://jsfiddle.net/risto/os5fj9m6/
Here is another option by using a function as the selector.
circles.select(function (d,i) { return (i==3) ? this : null;})
.attr("fill", "red");
If the selector is a function it gets the datum (d) and the iterator (i) as parameter. It then returns either the object (this) if selected or null if not selected.

Resources