Reputation: 25
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!
Upvotes: 2
Views: 155
Reputation: 4241
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>
-->
Upvotes: 1