D3.js treemap wrong rectangle size - d3.js

i'm currently working on an interactive D3.js Treemap, strongly inspired from https://observablehq.com/#d3/zoomable-treemap. However I got a problem with the size of inner rectangle on the composing part of "Maxillopoda" group as you can see in
https://jsfiddle.net/CharlotteAndre/rjy2pb4x/4/ the last "Halopitilus" group is larger than many groups even if I got a value of 1. The problem might be coming from this function
function position(group, root) {
group.selectAll("g")
.attr("transform", d => d === root ? `translate(0,-30)` : `translate(${x(d.x0)},${y(d.y0)})`)
.select("rect")
.attr("width", d => d === root ? width : x(d.x1) - x(d.x0))
.attr("height", d => d === root ? 30 : y(d.y1) - y(d.y0));
}

I found issue with your treemap building using value of nodes and sort function
let treemap = data => d3.treemap()
.tile(tile)(d3.hierarchy(data)
.sum(d => d.value)
.sort((a, b) => b.value - a.value))
When you are sorting based on sum of values, dont assign 0 value to parent nodes. I changed your data to remove the values associated with parents. Either you assign sum of children values or dont assign. This logic will mess up your hierarchy. Run the code below and let me know if this suffice on what you want. I have changed the width and height for better viewing.
let data = {
"name": "Crustacea",
"children": [
{
"name": "Maxillopoda",
"children": [
{
"name": "Maxillopoda_X",
"children": [
{
"name": "Maxillopoda_X_sp.",
"value": 448
}
]
},
{
"name": "Acartia",
"children": [
{
"name": "Acartia_longiremis",
"value": 6
},
{
"name": "Acartia_negligens",
"value": 6
},
{
"name": "Acartia_danae",
"value": 2
},
{
"name": "Acartia_pacifica",
"value": 1
}
]
},
{
"name": "Pleuromamma",
"children": [
{
"name": "Pleuromamma_scutullata",
"value": 10
},
{
"name": "Pleuromamma_borealis",
"value": 8
}
]
},
{
"name": "Calocalanus",
"children": [
{
"name": "Calocalanus_minutus",
"value": 143
},
{
"name": "Calocalanus_sp.",
"value": 61
},
{
"name": "Calocalanus_plumulosus",
"value": 12
},
{
"name": "Calocalanus_pavo",
"value": 19
}
]
},
{
"name": "Mecynocera",
"children": [
{
"name": "Mecynocera_clausi",
"value": 11
}
]
},
{
"name": "Oithona",
"children": [
{
"name": "Oithona_sp.",
"value": 18
}
]
},
{
"name": "Corycaeus",
"children": [
{
"name": "Corycaeus_speciosus",
"value": 9
}
]
},
{
"name": "Acrocalanus",
"children": [
{
"name": "Acrocalanus_monachus",
"value": 1
}
]
},
{
"name": "Subeucalanus",
"children": [
{
"name": "Subeucalanus_crassus",
"value": 5
}
]
},
{
"name": "Sapphirina",
"children": [
{
"name": "Sapphirina_sp.",
"value": 9
},
{
"name": "Sapphirina_scarlata",
"value": 3
},
{
"name": "Sapphirina_darwinii",
"value": 1
}
]
},
{
"name": "Paracalanus",
"children": [
{
"name": "Paracalanus_aculeatus",
"value": 1
}
]
},
{
"name": "Canthocalanus",
"children": [
{
"name": "Canthocalanus_pauper",
"value": 1
}
]
},
{
"name": "Temoropia",
"children": [
{
"name": "Temoropia_mayumbaensis",
"value": 5
}
]
},
{
"name": "Cosmocalanus",
"children": [
{
"name": "Cosmocalanus_darwinii",
"value": 2
}
]
},
{
"name": "Haloptilus",
"children": [
{
"name": "Haloptilus_longicornis",
"value": 1
}
]
},
{
"name": "Undinula",
"children": [
{
"name": "Undinula_vulgaris",
"value": 2
}
]
},
{
"name": "Centropages",
"children": [
{
"name": "Centropages_violaceus",
"value": 3
}
]
},
{
"name": "Euchaeta",
"children": [
{
"name": "Euchaeta_indica",
"value": 2
}
]
}
]
}
]
}
let svg_element = document.getElementById('treemap');
this.svg = d3.select(svg_element);
let margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 550 - margin.left - margin.right,
height = 550 - margin.top - margin.bottom;
// let height = 400;
// let width = 500;
let format = d3.format("");
let uids = new Map();
let uid = title => {
let counter = uids.has(title) ? uids.get(title) + 1 : 0;
uids.set(title, counter);
return `${title}-${counter}`;
}
let name = d => d.ancestors().reverse().map(d => d.data.name).join("/");
function tile(node, x0, y0, x1, y1) {
d3.treemapBinary(node, 0, 0, width, height);
for (const child of node.children) {
child.x0 = x0 + child.x0 / width * (x1 - x0);
child.x1 = x0 + child.x1 / width * (x1 - x0);
child.y0 = y0 + child.y0 / height * (y1 - y0);
child.y1 = y0 + child.y1 / height * (y1 - y0);
}
}
let treemap = data => d3.treemap()
.tile(tile)(d3.hierarchy(data)
.sum(d => d.value)
.sort((a, b) => b.value - a.value))
const x = d3.scaleLinear().rangeRound([0, width]);
const y = d3.scaleLinear().rangeRound([0, height]);
const svg = this.svg
.attr("viewBox", [0.5, -30.5, width, height + 30])
.style("font", "8px sans-serif");
let group = svg.append("g")
.call(render, treemap(data));
function render(group, root) {
const node = group
.selectAll("g")
.data(root.children.concat(root))
.join("g");
node.filter(d => d === root ? d.parent : d.children)
.attr("cursor", "pointer")
.on("click", (event, d) => d === root ? zoomout(root) : zoomin(d));
node.append("title")
.text(d => `${name(d)}\n${format(d.value)}`)
.style("font-size", "70%");
node.append("rect")
.attr("id", d => (d.leafUid = uid("leaf")))
.attr("fill", d => d === root ? "#edf3f7" : d.children ? "#0b9ba3" : "#0b9ba3")
.attr("stroke", "#edf3f7");
node.append("clipPath")
.attr("id", d => (d.clipUid = uid("clip")))
.append("use")
.attr("xlink:href", d => d.leafUid.href);
node.append("text")
.attr("clip-path", d => d.clipUid)
.attr("font-weight", d => d === root ? "bold" : null)
.selectAll("tspan")
.data(d => (d.data.name).split(/(?=[A-Z][^A-Z])/g).concat(format(d.value)))
.join("tspan")
.attr("x", 3)
.attr("y", (d, i, nodes) => `${(i === nodes.length - 1) * 0.3 + 1.1 + i * 0.9}em`)
.attr("fill-opacity", (d, i, nodes) => i === nodes.length - 1 ? 0.7 : null)
.attr("font-weight", (d, i, nodes) => i === nodes.length - 1 ? "normal" : null)
.text(d => d);
group.call(position, root);
}
function position(group, root) {
group.selectAll("g")
.attr("transform", d => d === root ? `translate(0,-30)` : `translate(${x(d.x0)},${y(d.y0)})`)
.select("rect")
.attr("width", d => d === root ? width : x(d.x1) - x(d.x0))
.attr("height", d => d === root ? 20 : y(d.y1) - y(d.y0));
}
// When zooming in, draw the new nodes on top, and fade them in.
function zoomin(d) {
const group0 = group.attr("pointer-events", "none");
const group1 = group = svg.append("g").call(render, d);
x.domain([d.x0, d.x1]);
y.domain([d.y0, d.y1]);
svg.transition()
.duration(750)
.call(t => group0.transition(t).remove()
.call(position, d.parent))
.call(t => group1.transition(t)
.attrTween("opacity", () => d3.interpolate(0, 1))
.call(position, d));
}
// When zooming out, draw the old nodes on top, and fade them out.
function zoomout(d) {
const group0 = group.attr("pointer-events", "none");
const group1 = group = svg.insert("g", "*").call(render, d.parent);
x.domain([d.parent.x0, d.parent.x1]);
y.domain([d.parent.y0, d.parent.y1]);
svg.transition()
.duration(750)
.call(t => group0.transition(t).remove()
.attrTween("opacity", () => d3.interpolate(1, 0))
.call(position, d))
.call(t => group1.transition(t)
.call(position, d.parent));
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.4.4/d3.min.js"></script>
<svg id="treemap"></svg>
UPDATE without changing data
let data = {
"name": "Crustacea",
"value": 0,
"children": [
{
"name": "Maxillopoda",
"value": 195,
"children": [
{
"name": "Maxillopoda_X",
"value": 0,
"children": [
{
"name": "Maxillopoda_X_sp.",
"value": 448
}
]
},
{
"name": "Acartia",
"value": 0,
"children": [
{
"name": "Acartia_longiremis",
"value": 6
},
{
"name": "Acartia_negligens",
"value": 6
},
{
"name": "Acartia_danae",
"value": 2
},
{
"name": "Acartia_pacifica",
"value": 1
}
]
},
{
"name": "Pleuromamma",
"value": 0,
"children": [
{
"name": "Pleuromamma_scutullata",
"value": 10
},
{
"name": "Pleuromamma_borealis",
"value": 8
}
]
},
{
"name": "Calocalanus",
"value": 9,
"children": [
{
"name": "Calocalanus_minutus",
"value": 43
},
{
"name": "Calocalanus_sp.",
"value": 61
},
{
"name": "Calocalanus_plumulosus",
"value": 12
},
{
"name": "Calocalanus_pavo",
"value": 19
}
]
},
{
"name": "Mecynocera",
"value": 0,
"children": [
{
"name": "Mecynocera_clausi",
"value": 11
}
]
},
{
"name": "Oithona",
"value": 0,
"children": [
{
"name": "Oithona_sp.",
"value": 18
}
]
},
{
"name": "Corycaeus",
"value": 0,
"children": [
{
"name": "Corycaeus_speciosus",
"value": 9
}
]
},
{
"name": "Acrocalanus",
"value": 0,
"children": [
{
"name": "Acrocalanus_monachus",
"value": 1
}
]
},
{
"name": "Subeucalanus",
"value": 0,
"children": [
{
"name": "Subeucalanus_crassus",
"value": 5
}
]
},
{
"name": "Sapphirina",
"value": 0,
"children": [
{
"name": "Sapphirina_sp.",
"value": 9
},
{
"name": "Sapphirina_scarlata",
"value": 3
},
{
"name": "Sapphirina_darwinii",
"value": 1
}
]
},
{
"name": "Paracalanus",
"value": 0,
"children": [
{
"name": "Paracalanus_aculeatus",
"value": 1
}
]
},
{
"name": "Canthocalanus",
"value": 0,
"children": [
{
"name": "Canthocalanus_pauper",
"value": 1
}
]
},
{
"name": "Temoropia",
"value": 0,
"children": [
{
"name": "Temoropia_mayumbaensis",
"value": 5
}
]
},
{
"name": "Cosmocalanus",
"value": 0,
"children": [
{
"name": "Cosmocalanus_darwinii",
"value": 2
}
]
},
{
"name": "Haloptilus",
"value": 0,
"children": [
{
"name": "Haloptilus_longicornis",
"value": 1
}
]
},
{
"name": "Undinula",
"value": 0,
"children": [
{
"name": "Undinula_vulgaris",
"value": 2
}
]
},
{
"name": "Centropages",
"value": 0,
"children": [
{
"name": "Centropages_violaceus",
"value": 3
}
]
},
{
"name": "Euchaeta",
"value": 0,
"children": [
{
"name": "Euchaeta_indica",
"value": 2
}
]
}
]
}
]
}
let svg_element = document.getElementById('treemap');
this.svg = d3.select(svg_element);
let margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 350 - margin.left - margin.right,
height = 198 - margin.top - margin.bottom;
// let height = 400;
// let width = 500;
let format = d3.format("");
let uids = new Map();
let uid = title => {
let counter = uids.has(title) ? uids.get(title) + 1 : 0;
uids.set(title, counter);
return `${title}-${counter}`;
}
let name = d => d.ancestors().reverse().map(d => d.data.name).join("/");
function tile(node, x0, y0, x1, y1) {
d3.treemapBinary(node, 0, 0, width, height);
for (const child of node.children) {
child.x0 = x0 + child.x0 / width * (x1 - x0);
child.x1 = x0 + child.x1 / width * (x1 - x0);
child.y0 = y0 + child.y0 / height * (y1 - y0);
child.y1 = y0 + child.y1 / height * (y1 - y0);
}
}
function identifyLeaves(obj) {
var hasNoChildren = (obj.children) ? false : true;
if(hasNoChildren)
return obj.value
}
let treemap = data => d3.treemap()
.tile(tile)(d3.hierarchy(data)
.sum((d) => identifyLeaves(d))
.sort((a, b) => identifyLeaves(b) - identifyLeaves(a)))
const x = d3.scaleLinear().rangeRound([0, width]);
const y = d3.scaleLinear().rangeRound([0, height]);
const svg = this.svg
.attr("viewBox", [0.5, -30.5, width, height + 30])
.style("font", "8px sans-serif");
let group = svg.append("g")
.call(render, treemap(data));
function render(group, root) {
const node = group
.selectAll("g")
.data(root.children.concat(root))
.data(root.children.concat(root))
.join("g");
node.filter(d => d === root ? d.parent : d.children)
.attr("cursor", "pointer")
.on("click", (event, d) => d === root ? zoomout(root) : zoomin(d));
node.append("title")
.text(d => `${name(d)}\n${format(d.value)}`)
.style("font-size", "70%");
node.append("rect")
.attr("id", d => (d.leafUid = uid("leaf")))
.attr("fill", d => d === root ? "#edf3f7" : d.children ? "#0b9ba3" : "#0b9ba3")
.attr("stroke", "#edf3f7");
node.append("clipPath")
.attr("id", d => (d.clipUid = uid("clip")))
.append("use")
.attr("xlink:href", d => d.leafUid.href);
node.append("text")
.attr("clip-path", d => d.clipUid)
.attr("font-weight", d => d === root ? "bold" : null)
.selectAll("tspan")
.data(d => (d.data.name).split(/(?=[A-Z][^A-Z])/g).concat(format(d.value)))
.join("tspan")
.attr("x", 3)
.attr("y", (d, i, nodes) => `${(i === nodes.length - 1) * 0.3 + 1.1 + i * 0.9}em`)
.attr("fill-opacity", (d, i, nodes) => i === nodes.length - 1 ? 0.7 : null)
.attr("font-weight", (d, i, nodes) => i === nodes.length - 1 ? "normal" : null)
.text(d => d);
group.call(position, root);
}
function position(group, root) {
group.selectAll("g")
.attr("transform", d => d === root ? `translate(0,-30)` : `translate(${x(d.x0)},${y(d.y0)})`)
.select("rect")
.attr("width", d => d === root ? width : x(d.x1) - x(d.x0))
.attr("height", d => d === root ? 30 : y(d.y1) - y(d.y0));
}
// When zooming in, draw the new nodes on top, and fade them in.
function zoomin(d) {
const group0 = group.attr("pointer-events", "none");
const group1 = group = svg.append("g").call(render, d);
x.domain([d.x0, d.x1]);
y.domain([d.y0, d.y1]);
svg.transition()
.duration(750)
.call(t => group0.transition(t).remove()
.call(position, d.parent))
.call(t => group1.transition(t)
.attrTween("opacity", () => d3.interpolate(0, 1))
.call(position, d));
}
// When zooming out, draw the old nodes on top, and fade them out.
function zoomout(d) {
const group0 = group.attr("pointer-events", "none");
const group1 = group = svg.insert("g", "*").call(render, d.parent);
x.domain([d.parent.x0, d.parent.x1]);
y.domain([d.parent.y0, d.parent.y1]);
svg.transition()
.duration(750)
.call(t => group0.transition(t).remove()
.attrTween("opacity", () => d3.interpolate(1, 0))
.call(position, d))
.call(t => group1.transition(t)
.call(position, d.parent));
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.4.4/d3.min.js" integrity="sha512-hnFpvCiJ8Fr1lYLqcw6wLgFUOEZ89kWCkO+cEekwcWPIPKyknKV1eZmSSG3UxXfsSuf+z/SgmiYB1zFOg3l2UQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<svg id="treemap"></svg>

Related

Updating nodes and links with labels in d3 force directed network graph is not removing the nodes properly

I am trying to create a d3 force directed network graph where based on a given network I can update the network by modifying the links and nodes and re-update them in the svg.
I tweaked the code so that a g element can be created which enclose each node circle so that I can add a text inside that same g element.
But now the labels are working perfectly but when I transition from graph 3 to graph 1 by clicking button 3 first then clicking button 1, the links that are redundant (a->d, a->f) are removed perfectly but the nodes that are redundant (e and f) stays in the svg.
I could not figure out whether it was a wrong selection or do I need some tweaking in the tick() function?
Here is the code:
var height = 200;
var width = 200;
const graph = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 }
]
}
const graph2 = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 },
{ "source": "a", "target": "d", "value": 1 }
]
}
const graph3 = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 },
{ "name": "e", "group": 4 },
{ "name": "f", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 },
{ "source": "a", "target": "d", "value": 1 },
{ "source": "f", "target": "a", "value": 1 }
]
}
var simulation = d3.forceSimulation()
.force("ct", d3.forceCenter(height / 2, width / 2))
.force("link", d3.forceLink().id(function(d) { return d.name; })
.distance(50).strength(2))
.force("charge", d3.forceManyBody().strength(-240))
// use forceX and forceY instead to change the relative positioning
// .force("centering", d3.forceCenter(width/2, height/2))
.force("x", d3.forceX(width / 2))
.force("y", d3.forceY(height / 2))
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("g").attr("class", "links");
svg.append("g").attr("class", "nodes");
function start(graph) {
var linkElements = svg.select(".links").selectAll(".link").data(graph.links);
linkElements.enter().append("line").attr("class", "link");
linkElements.exit().remove();
var nodeElements = svg.select(".nodes").selectAll(".node")
.data(graph.nodes, function(d) { return d.name })
.enter().append("g")
.attr("class", "node");
var circles = nodeElements.append("circle")
.attr("r", 8);
var labels = nodeElements.append("text")
.text(function(d) { return d.name; })
.attr("x", 10)
.attr("y", 10);
nodeElements.exit().remove();
simulation.nodes(graph.nodes);
simulation.force("link").links(graph.links);
simulation.alphaTarget(0.1).restart();
}
function tick() {
var nodeElements = svg.select(".nodes").selectAll(".node");
var linkElements = svg.select(".links").selectAll(".link");
nodeElements.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
linkElements.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.1).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
start(graph);
document.getElementById('btn1').addEventListener('click', function() {
start(graph);
});
document.getElementById('btn2').addEventListener('click', function() {
start(graph2);
});
document.getElementById('btn3').addEventListener('click', function() {
start(graph3);
});
.link {
stroke: #000;
stroke-width: 1.5px;
}
.node {
stroke-width: 1.5px;
}
text {
font-family: sans-serif;
font-size: 10px;
fill: #000000;
}
<body>
<div>
<button id='btn1'>1</button>
<button id='btn2'>2</button>
<button id='btn3'>3</button>
</div>
</body>
<script src="https://d3js.org/d3.v5.min.js"></script>
Here is the jsfiddle version of the code: https://jsfiddle.net/syedarehaq/myd0h5w1/
In the provided code, variable nodeElements contains the enter selection, rather than the whole data binding selection.
var nodeElements = svg.select(".nodes").selectAll(".node")
.data(graph.nodes, function(d) { return d.name })
.enter().append("g")
.attr("class", "node");
nodeElements declaration should not contain the .enter bit - it should be just like linkSelection variable declaration:
var nodeElements = svg.select(".nodes").selectAll(".node")
.data(graph.nodes, function(d) { return d.name })
Then, in order to append the new circles and texts only to the entering g elements, adapt as follows:
var enterSelection = nodeElements.enter().append("g")
.attr("class", "node");
var circles = enterSelection.append("circle")
.attr("r", 8);
var labels = enterSelection.append("text")
.text(function(d) { return d.name; })
.attr("x", 10)
.attr("y", 10);
The exit function call is now working as expected.
Demo in the snippet below.
var height = 200;
var width = 200;
const graph = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 }
]
}
const graph2 = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 },
{ "source": "a", "target": "d", "value": 1 }
]
}
const graph3 = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 },
{ "name": "e", "group": 4 },
{ "name": "f", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 },
{ "source": "a", "target": "d", "value": 1 },
{ "source": "f", "target": "a", "value": 1 }
]
}
var simulation = d3.forceSimulation()
.force("ct", d3.forceCenter(height / 2, width / 2))
.force("link", d3.forceLink().id(function(d) { return d.name; })
.distance(50).strength(2))
.force("charge", d3.forceManyBody().strength(-240))
// use forceX and forceY instead to change the relative positioning
// .force("centering", d3.forceCenter(width/2, height/2))
.force("x", d3.forceX(width / 2))
.force("y", d3.forceY(height / 2))
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("g").attr("class", "links");
svg.append("g").attr("class", "nodes");
function start(graph) {
var linkElements = svg.select(".links").selectAll(".link").data(graph.links);
linkElements.enter().append("line").attr("class", "link");
linkElements.exit().remove();
var nodeElements = svg.select(".nodes").selectAll(".node")
.data(graph.nodes, function(d) { return d.name })
var enterSelection = nodeElements.enter().append("g")
.attr("class", "node");
var circles = enterSelection.append("circle")
.attr("r", 8);
var labels = enterSelection.append("text")
.text(function(d) { return d.name; })
.attr("x", 10)
.attr("y", 10);
nodeElements.exit().remove();
simulation.nodes(graph.nodes);
simulation.force("link").links(graph.links);
simulation.alphaTarget(0.1).restart();
}
function tick() {
var nodeElements = svg.select(".nodes").selectAll(".node");
var linkElements = svg.select(".links").selectAll(".link");
nodeElements.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
linkElements.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.1).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
start(graph);
document.getElementById('btn1').addEventListener('click', function() {
start(graph);
});
document.getElementById('btn2').addEventListener('click', function() {
start(graph2);
});
document.getElementById('btn3').addEventListener('click', function() {
start(graph3);
});
.link {
stroke: #000;
stroke-width: 1.5px;
}
.node {
stroke-width: 1.5px;
}
text {
font-family: sans-serif;
font-size: 10px;
fill: #000000;
}
<body>
<div>
<button id='btn1'>1</button>
<button id='btn2'>2</button>
<button id='btn3'>3</button>
</div>
</body>
<script src="https://d3js.org/d3.v5.min.js"></script>

Root element is not showing its children in sunburst

I am trying to make a sunburst by following the 3-part tutorial on https://bl.ocks.org/denjn5/3b74baf5edc4ac93d5e487136481c601 My json contains sell information based on country and product division. I am trying to show in the first layer sell based on country and in the 2nd layer sell based on product division. My Json file looks like this:
{
"country": "All",
"shares":[
{
"country": "at",
"shares":[
{
"productdivision": "accessorie",
"label": 53222
},
{
"productdivision": "apparel",
"label": 365712
},
{
"productdivision": "footwear",
"label": 523684
}
]
},
{
"country": "be",
"shares":[
{
"productdivision": "accessorie",
"label": 57522
},
{
"productdivision": "apparel",
"label": 598712
},
{
"productdivision": "footwear",
"label": 52284
}
]
},
{
"country": "DE",
"shares":[
{
"productdivision": "accessorie",
"label": 56982
},
{
"productdivision": "apparel",
"label": 55312
},
{
"productdivision": "footwear",
"label": 67284
}
]
},
{
"country": "Fr",
"shares":[
{
"productdivision": "accessorie",
"label": 5862
},
{
"productdivision": "apparel",
"label": 45312
},
{
"productdivision": "footwear",
"label": 26284
}
]
}
]
}
This json file's name is kpiDrillDown2.json and I call it in my code with d3.json(). I have made slight changes to the code to work for my data. The code is as follows:
<html>
<head>
<style>
#import url('https://fonts.googleapis.com/css?family=Raleway');
body {
font-family: "Raleway", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
</style>
<script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<svg></svg>
<script>
//initialize variables
var width = 500;
var height = 500;
var radius = Math.min(width, height) / 2;
var color = d3.scaleOrdinal(d3.schemeCategory20b);
//setting up svg workspace
var g = d3.select('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
//formatting the data
var partition = d3.partition()
.size([2 * Math.PI, radius]);
function draw(nodeData){
debugger;
//finding the root node
var root = d3.hierarchy(nodeData)
.sum(function (d) { return d.label});
//calculating each arc
partition(root);
var arc = d3.arc()
.startAngle(function (d) { return d.x0; })
.endAngle(function (d) { return d.x1; })
.innerRadius(function (d) { return d.y0; })
.outerRadius(function (d) { return d.y1; });
g.selectAll('g')
.data(root.descendants())
.enter()
.append('g')
.attr("class", "node")
.append('path')
.attr("display", function (d) { return d.depth ? null : "none"; })
.attr("d", arc)
.style('stroke', '#fff')
.style("fill", function (d) { return color((d.parent ? d : d.parent).data.productdivision); })
g.selectAll(".node")
.append("text")
.attr("transform", function(d) {
return "translate(" + arc.centroid(d) + ")rotate(" + computeTextRotation(d) + ")"; })
.attr("dx", "-20")
.attr("dy", ".5em")
.text(function(d) { return d.parent ? d.data.productdivision : "" });
function computeTextRotation(d) {
var angle = (d.x0 + d.x1) / Math.PI * 90;
// Avoid upside-down labels
return (angle < 90 || angle > 270) ? angle : angle + 180;
}
}
d3.json('kpiDrillDown3.json', draw);
</script>
</body>
</html>
I put a debbuger in the draw functin to inspect root element. Root doesn't have any children. This is what I see in the console:
When I continue it gives me the error:"Cannot read property 'data' of null". As shown in console, root doesn't have children. My question is, do I need to change my json data format to make root recogninze the chilren, or am I doing something wrong. I am new to d3js and basically by getting the source code and modifying it, I am making my way through. This is the error in console:
I appreciate your help and thank you very much.
According to the API:
The specified children accessor function is invoked for each datum, starting with the root data, and must return an array of data representing the children, or null if the current datum has no children. If children is not specified, it defaults to:
function children(d) {
return d.children;
}
However, in your data structure, you don't have children, but shares instead.
So, the hierarchy should be:
var root = d3.hierarchy(data, function(d) {
return d.shares;
})
Pay attention to the fact that in the JSON of that tutorial you linked (just like in the API's example) the children's array is named children.
Here is a demo, look at the console (your browser's console, not the snippet one):
var data = {
"country": "All",
"shares": [{
"country": "at",
"shares": [{
"productdivision": "accessorie",
"label": 53222
},
{
"productdivision": "apparel",
"label": 365712
},
{
"productdivision": "footwear",
"label": 523684
}
]
},
{
"country": "be",
"shares": [{
"productdivision": "accessorie",
"label": 57522
},
{
"productdivision": "apparel",
"label": 598712
},
{
"productdivision": "footwear",
"label": 52284
}
]
},
{
"country": "DE",
"shares": [{
"productdivision": "accessorie",
"label": 56982
},
{
"productdivision": "apparel",
"label": 55312
},
{
"productdivision": "footwear",
"label": 67284
}
]
},
{
"country": "Fr",
"shares": [{
"productdivision": "accessorie",
"label": 5862
},
{
"productdivision": "apparel",
"label": 45312
},
{
"productdivision": "footwear",
"label": 26284
}
]
}
]
};
var root = d3.hierarchy(data, function(d) {
return d.shares;
})
.sum(function(d) {
return d.label
});
console.log(root)
<script src="https://d3js.org/d3.v4.min.js"></script>

D3 Tree layout visualization - Inherit child with multiple parents

I'm fresh bee for D3-visulization. Currently working with creating D3 tree layout visualization for data lineage. In a data lineage flow, a child node can be derived from more than one parent. Here is the example. In below example, a 'DevLead' may work with 2 managers.
var data = [
{ "name": "Director", "parent": "null", "depth": 0 },
{ "name": "Manager1", "parent": "Director", "depth": 1 },
{ "name": "Manager2", "parent": "Director", "depth": 1 },
{ "name": "DevLead", "parent": "Manager1", "depth": 2 },
{ "name": "DevLead", "parent": "Manager2", "depth": 2 }
];
Getting output refer below image attached.
I'd like to see 'DevLead' children should show only one, and there should be a derivation from 'Manager1' and 'Manager2'. Could any one help with this.
D3 Tree Layout does not exactly supports multiple parents
What Can you do?
Use network graph instead - downside is that node positioning is
hard
I had similar requirements and tried building network graph similar
with tree layout, but when there are many nodes, it gets messy ...
you can check it on
codepen
use hack on tree layout - draw additional link from other node
check this example
another hack using hidden nodes - jsfiddle
Also, I think, these links will help you further :
Family Tree in d3.js
d-tree library - data with multiple parents
If you go with first option, here, you can play with this snippet by removing and adding nodes in data
<!DOCTYPE html>
<html >
<head>
<meta charset="UTF-8">
<link rel="shortcut icon" type="image/x-icon" href="https://production-assets.codepen.io/assets/favicon/favicon-8ea04875e70c4b0bb41da869e81236e54394d63638a1ef12fa558a4a835f1164.ico" />
<link rel="mask-icon" type="" href="https://production-assets.codepen.io/assets/favicon/logo-pin-f2d2b6d2c61838f7e76325261b7195c27224080bc099486ddd6dccb469b8e8e6.svg" color="#111" />
<title>CodePen - A Pen by dato</title>
</head>
<body translate="no" >
<script src='https://d3js.org/d3.v3.min.js'></script>
<script>
var width = window.innerWidth - 20,
height = window.innerHeight - 20,
radius = 30;
var min_zoom = 0.1;
var max_zoom = 7;
var zoom = d3.behavior.zoom().scaleExtent([min_zoom, max_zoom])
var fill = d3.scale.category20();
var force = d3.layout.force()
.charge(-8000)
.linkDistance(200)
.size([width, height]);
force.drag().on("dragstart", dragstarted)
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var chart = svg.append('g');
var json = {
"nodes": [{
"name": "node0"
}, {
"name": "node1"
}, {
"name": "node2"
}, {
"name": "node3"
}, {
"name": "node4"
}, {
"name": "node5"
}, {
"name": "node6"
}, {
"name": "node7"
}, {
"name": "node8"
}, {
"name": "node9"
}, {
"name": "node10"
}, {
"name": "node11"
}, {
"name": "node12"
}, {
"name": "node13"
}, {
"name": "node14"
}, {
"name": "node15"
}, {
"name": "node16"
}, {
"name": "node17"
}, {
"name": "node18"
}, {
"name": "node19"
}, {
"name": "node20"
}, {
"name": "node21"
}, {
"name": "node22"
}, {
"name": "node23"
}, {
"name": "node24"
}, {
"name": "node25"
}, {
"name": "node26"
}, {
"name": "node27"
}, {
"name": "node28"
}, {
"name": "node29"
}, {
"name": "node30"
}, {
"name": "node31"
}, {
"name": "node32"
}, {
"name": "node33"
}, {
"name": "node34"
}, {
"name": "node35"
}, {
"name": "node36"
}, {
"name": "node37"
}, {
"name": "node38"
}, {
"name": "node39"
}, {
"name": "node40"
}, {
"name": "node41"
}, {
"name": "node42"
}, {
"name": "node43"
}, {
"name": "node44"
}, {
"name": "node45"
}, {
"name": "node46"
}, {
"name": "node47"
}, {
"name": "node48"
}, {
"name": "node49"
}, {
"name": "node50"
}, {
"name": "node51"
}, {
"name": "node52"
}, {
"name": "node53"
}, {
"name": "node54"
}, {
"name": "node55"
}, {
"name": "node56"
}, {
"name": "node57"
}, {
"name": "node58"
}, {
"name": "node59"
}, {
"name": "node60"
}, {
"name": "node61"
}, {
"name": "node62"
}, {
"name": "node63"
}, {
"name": "node64"
}, {
"name": "node65"
}, {
"name": "node66"
}, {
"name": "node67"
}, {
"name": "node68"
}, {
"name": "node69"
}, {
"name": "node70"
}, {
"name": "node71"
}, {
"name": "node72"
}, {
"name": "node73"
}, {
"name": "node74"
}, {
"name": "node75"
}, {
"name": "node76"
}, {
"name": "node77"
}, {
"name": "node78"
}, {
"name": "node79"
}, {
"name": "node80"
}, {
"name": "node81"
}, {
"name": "node82"
}, {
"name": "node83"
}, {
"name": "node84"
}, {
"name": "node85"
}, {
"name": "node86"
}, {
"name": "node87"
}, {
"name": "node88"
}, {
"name": "node89"
}, {
"name": "node90"
}, {
"name": "node91"
}, {
"name": "node92"
}, {
"name": "node93"
}, {
"name": "node94"
}, {
"name": "node95"
}, {
"name": "node96"
}, {
"name": "node97"
}, {
"name": "node98"
}, {
"name": "node99"
}],
"links": [ {
"source": 0,
"target": 1
}, {
"source": 0,
"target": 2
}, {
"source": 1,
"target": 3
}, {
"source": 1,
"target": 4
}, {
"source": 2,
"target": 5
}, {
"source": 2,
"target": 6
}, {
"source": 3,
"target": 7
}, {
"source": 3,
"target": 8
}, {
"source": 4,
"target": 9
}, {
"source": 4,
"target": 10
}, {
"source": 5,
"target": 11
}, {
"source": 5,
"target": 12
}, {
"source": 6,
"target": 13
}, {
"source": 6,
"target": 14
}, {
"source": 7,
"target": 15
}, {
"source": 7,
"target": 16
}, {
"source": 8,
"target": 17
}, {
"source": 8,
"target": 18
}, {
"source": 9,
"target": 19
}, {
"source": 9,
"target": 20
}, {
"source": 10,
"target": 21
}, {
"source": 10,
"target": 22
}, {
"source": 11,
"target": 23
}, {
"source": 11,
"target": 24
}, {
"source": 12,
"target": 25
}, {
"source": 12,
"target": 26
}, {
"source": 13,
"target": 27
}, {
"source": 13,
"target": 28
}, {
"source": 14,
"target": 29
}, {
"source": 14,
"target": 30
}, {
"source": 15,
"target": 31
}, {
"source": 15,
"target": 32
}, {
"source": 16,
"target": 33
}, {
"source": 16,
"target": 34
}, {
"source": 17,
"target": 35
}, {
"source": 17,
"target": 36
}, {
"source": 18,
"target": 37
}, {
"source": 18,
"target": 38
}, {
"source": 19,
"target": 39
}, {
"source": 19,
"target": 40
}, {
"source": 20,
"target": 41
}, {
"source": 20,
"target": 42
}, {
"source": 21,
"target": 43
}, {
"source": 21,
"target": 44
}, {
"source": 22,
"target": 45
}, {
"source": 22,
"target": 46
}, {
"source": 23,
"target": 47
}, {
"source": 23,
"target": 48
}, {
"source": 24,
"target": 49
}, {
"source": 24,
"target": 50
}, {
"source": 25,
"target": 51
}, {
"source": 25,
"target": 52
}, {
"source": 26,
"target": 53
}, {
"source": 26,
"target": 54
}, {
"source": 27,
"target": 55
}, {
"source": 27,
"target": 56
}, {
"source": 28,
"target": 57
}, {
"source": 28,
"target": 58
}, {
"source": 29,
"target": 59
}, {
"source": 29,
"target": 60
}, {
"source": 30,
"target": 61
}, {
"source": 30,
"target": 62
}, {
"source": 31,
"target": 63
}, {
"source": 31,
"target": 64
}, {
"source": 32,
"target": 65
}, {
"source": 32,
"target": 66
}, {
"source": 33,
"target": 67
}, {
"source": 33,
"target": 68
}, {
"source": 34,
"target": 69
}, {
"source": 34,
"target": 70
}, {
"source": 35,
"target": 71
}, {
"source": 35,
"target": 72
}, {
"source": 36,
"target": 73
}, {
"source": 36,
"target": 74
}, {
"source": 37,
"target": 75
}, {
"source": 37,
"target": 76
}, {
"source": 38,
"target": 77
}, {
"source": 38,
"target": 78
}, {
"source": 39,
"target": 79
}, {
"source": 39,
"target": 80
}, {
"source": 40,
"target": 81
}, {
"source": 40,
"target": 82
}, {
"source": 41,
"target": 83
}, {
"source": 41,
"target": 84
}, {
"source": 42,
"target": 85
}, {
"source": 42,
"target": 86
}, {
"source": 43,
"target": 87
}, {
"source": 43,
"target": 88
}, {
"source": 44,
"target": 89
}, {
"source": 44,
"target": 90
}, {
"source": 45,
"target": 91
}, {
"source": 45,
"target": 92
}, {
"source": 46,
"target": 93
}, {
"source": 46,
"target": 94
}, {
"source": 47,
"target": 95
}, {
"source": 47,
"target": 96
}, {
"source": 48,
"target": 97
}, {
"source": 48,
"target": 98
}, {
"source": 49,
"target": 99
},{
"source": 0,
"target": 99
}]
}
var link = chart.selectAll("line")
.data(json.links)
.enter()
.append("line")
.attr("stroke", function(d) {
return 'blue'
})
var node = chart.selectAll("circle")
.data(json.nodes)
.enter().append("circle")
.attr("r", radius - .75)
.style("fill", function(d) {
return fill(d.group);
})
.style("stroke", function(d) {
return d3.rgb(fill(d.group)).darker();
})
.on('mouseover', d => console.log(d))
.call(force.drag);
function dragstarted() {
d3.event.sourceEvent.stopPropagation();
}
zoom.on("zoom", function(d) {
var evt = d3.event;
debugger;
/*
var dcx = (window.innerWidth/2-d.x*zoom.scale());
var dcy = (window.innerHeight/2-d.y*zoom.scale());
*/
var dcx = evt.translate[0]
var dcy = evt.translate[1]
zoom.translate([dcx, dcy]);
chart.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");;
});
force
.nodes(json.nodes)
.links(json.links)
.on("tick", tick)
.start();
svg.call(zoom)
function tick(e) {
console.log(e)
var k = 6 * e.alpha;
// Push sources up and targets down to form a weak tree.
link
.each(function(d,i) {
d.source.y -= k * 60, d.target.y += k * 100;
/*
if(i%2==1){
d.source.x -= 0.4/k
}else{
d.source.x += 0.4/k
}
*/
})
.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});
node
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
}
</script>
</body>
</html>
use hack on tree layout - draw additional link from other node
check this example
If a hack is the solution you're looking for the implementation below might be helpful. It's a d3 collapsable tree based on Rob Schmueckers Blog Multiple Parent Nodes D3.js example which can handle multiple parent links and the basic tree events. It's typically split into index.html building the html structure, a stylesheet style.css and the actual script tree.js
index.html :
Define a div with id tree_view later holding the tree. Call tree.js to create the tree.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>D3 collapsable multiple parents tree</title>
<link rel="stylesheet" type="text/css" href="style.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
</head>
<body>
<div id="tree_view"></div> <!-- div holding tree -->
<script src="tree.js"></script> <!-- script to create tree -->
</body>
</html>
style.css :
#tree_view {
width: 100%;
height: 100%;
margin-top: 30px;
}
/* basic node */
.node {
cursor: pointer;
text-anchor: start;
}
/* rectangle node */
.node rect {
stroke: gray;
stroke-width: 1.5px;
}
/* node text */
.node text {
font: 12px sans-serif;
}
/* standard links (.link) and multiparent links (.mpLink) */
.link, .mpLink {
fill: none;
stroke: #ccc;
}
tree.js : (full code below)
Basically you need to define a new link object for each additional link you want to add. Each link needs a source and target node. The backup nodes are necessary for event handling:
let link = new Object();
link.source = pairNode1;
link.target = pairNode2;
link._source = pairNode1; // backup source
link._target = pairNode2; // backup target
additionalLinks.push(link)
Now you can handle all additional links in the update process (updateTree(source)):
// ======== add additional links (mpLinks) ========
let mpLink = svg.selectAll("path.mpLink")
.data(additionalLinks);
mpLink.enter().insert("path", "g")
.attr("class", "mpLink")
.attr("x", nodeWidth / 2)
.attr("y", nodeHeight / 2)
.attr("d", function (d) {
var o = { x: source.x0, y: source.y0 };
return diagonal({ source: o, target: o });
});
mpLink.transition()
.duration(duration)
.attr("d", diagonal)
.attr("stroke-width", 1.5)
mpLink.exit().transition()
.duration(duration)
.attr("d", function (d) {
let o = { x: source.x, y: source.y };
return diagonal({ source: o, target: o });
})
.remove();
and define the on event behavior in method click(d).
The full tree.js code:
// plot properties
let root;
let tree;
let diagonal;
let svg;
let duration = 750;
let treeMargin = { top: 0, right: 20, bottom: 20, left: 20 };
let treeWidth = window.innerWidth - treeMargin.right - treeMargin.left;
let treeHeight = window.innerHeight - treeMargin.top - treeMargin.bottom;
let treeDepth = 5;
let maxTextLength = 90;
let nodeWidth = maxTextLength + 20;
let nodeHeight = 36;
let scale = 1;
// tree data
let data = [
{
"name": "Root",
"parent": "null",
"children": [
{
"name": "Level 2: A",
"parent": "Top Level",
"children": [
{
"name": "A1",
"parent": "Level 2: A"
},
{
"name": "A2",
"parent": "Level 2: A"
}
]
},
{
"name": "Level 2: B",
"parent": "Top Level"
}
]
}
];
// additional (multiparent) links data array
let additionalLinks = []
/**
* Initialize tree properties
* #param {Object} treeData
*/
function initTree(treeData) {
// init
tree = d3.layout.tree()
.size([treeWidth, treeHeight]);
diagonal = d3.svg.diagonal()
.projection(function (d) { return [d.x + nodeWidth / 2, d.y + nodeHeight / 2]; });
svg = d3.select("div#tree_view")
.append("svg")
.attr("width", treeWidth + treeMargin.right + treeMargin.left)
.attr("height", treeHeight + treeMargin.top + treeMargin.bottom)
.attr("transform", `translate(${treeMargin.left},${treeMargin.top})scale(${scale},${scale})`);
root = treeData[0];
root.x0 = treeHeight / 2;
root.y0 = 0;
// fill additionalLinks array
let pairNode1 = tree.nodes(root).filter(function(d) {
return d['name'] === 'Level 2: B';
})[0];
let pairNode2 = tree.nodes(root).filter(function(d) {
return d['name'] === 'A2';
})[0];
let link = new Object();
link.source = pairNode1;
link.target = pairNode2;
link._source = pairNode1; // backup source
link._target = pairNode2; // backup target
additionalLinks.push(link)
// update
updateTree(root);
d3.select(self.frameElement).style("height", "500px");
// add resize listener
window.addEventListener("resize", function (event) {
resizeTreePlot();
});
}
/**
* Perform tree update. Update nodes and links
* #param {Object} source
*/
function updateTree(source) {
let i = 0;
let nodes = tree.nodes(root).reverse();
let links = tree.links(nodes);
nodes.forEach(function (d) { d.y = d.depth * 80; });
// ======== add nodes and text elements ========
let node = svg.selectAll("g.node")
.data(nodes, function (d) { return d.id || (d.id = ++i); });
let nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function (d) { return `translate(${source.x0},${source.y0})`; })
.on("click", click);
nodeEnter.append("rect")
.attr("width", nodeWidth)
.attr("height", nodeHeight)
.attr("rx", 2)
.style("fill", function(d) { return d._children ? "#ace3b5": "#f4f4f9"; });
nodeEnter.append("text")
.attr("y", nodeHeight / 2)
.attr("x", 13)
.attr("dy", ".35em")
.text(function (d) { return d.name; })
.style("fill-opacity", 1e-6);
let nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function (d) { return `translate(${d.x},${d.y})`; });
nodeUpdate.select("rect")
.attr("width", nodeWidth)
.style("fill", function(d) { return d._children ? "#ace3b5": "#f4f4f9"; });
nodeUpdate.select("text").style("fill-opacity", 1);
let nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function (d) { return `translate(${source.x},${source.y})`; })
.remove();
nodeExit.select("rect")
.attr("width", nodeWidth)
.attr("rx", 2)
.attr("height", nodeHeight);
nodeExit.select("text")
.style("fill-opacity", 1e-6);
// ======== add links ========
let link = svg.selectAll("path.link")
.data(links, function (d) { return d.target.id; });
link.enter().insert("path", "g")
.attr("class", "link")
.attr("x", nodeWidth / 2)
.attr("y", nodeHeight / 2)
.attr("d", function (d) {
var o = { x: source.x0, y: source.y0 };
return diagonal({ source: o, target: o });
});
link.transition()
.duration(duration)
.attr("d", diagonal)
link.exit().transition()
.duration(duration)
.attr("d", function (d) {
let o = { x: source.x, y: source.y };
return diagonal({ source: o, target: o });
})
.remove();
// ======== add additional links (mpLinks) ========
let mpLink = svg.selectAll("path.mpLink")
.data(additionalLinks);
mpLink.enter().insert("path", "g")
.attr("class", "mpLink")
.attr("x", nodeWidth / 2)
.attr("y", nodeHeight / 2)
.attr("d", function (d) {
var o = { x: source.x0, y: source.y0 };
return diagonal({ source: o, target: o });
});
mpLink.transition()
.duration(duration)
.attr("d", diagonal)
.attr("stroke-width", 1.5)
mpLink.exit().transition()
.duration(duration)
.attr("d", function (d) {
let o = { x: source.x, y: source.y };
return diagonal({ source: o, target: o });
})
.remove();
nodes.forEach(function (d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
/**
* Handle on tree node clicked actions
* #param {Object} d node
*/
function click(d) {
// update regular links
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
// update additional links
additionalLinks.forEach(function(link){
let sourceVisible = false;
let targetVisible = false;
tree.nodes(root).filter(function(n) {
if(n["name"] == link._source.name){
sourceVisible = true;
}
if(n["name"] == link._target.name){
targetVisible = true;
}
});
if(sourceVisible && targetVisible){
link.source = link._source;
link.target = link._target;
}
else if(!sourceVisible && targetVisible
|| !sourceVisible && !targetVisible){
link.source = d;
link.target = link.source;
}
else if(sourceVisible && !targetVisible){
link.source = link._source;
link.target = link.source;
}
});
// define more links behavior here...
updateTree(d);
}
/**
* Update tree dimension
*/
function updateTreeDimension() {
tree.size([treeWidth, treeHeight]);
svg.attr("width", treeWidth + treeMargin.right + treeMargin.left)
.attr("height", treeHeight + treeMargin.top + treeMargin.bottom)
.attr("transform", `translate(${treeMargin.left},${treeMargin.top})scale(${scale},${scale})`);
}
/**
* Resize the tree using current window dimension
*/
function resizeTreePlot() {
treeWidth = 0.9 * window.innerWidth - treeMargin.right - treeMargin.left;
treeHeight = (treeDepth + 2) * nodeHeight * 2;
updateTreeDimension();
updateTree(root);
}
// plot tree
initTree(data);
updateTree(root);
Here you can find the full demo. You can also fork the project.

How to give color to edges of d3js graph?

I am trying to give color to the edges but the desire outcome is not according to my desire.The color scheme changes every time I change the JSON file.
<%# Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm1.aspx.cs" Inherits="D3js_edges_connected_by_nodes_id.WebForm1" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<script type="text/javascript" src="http://d3js.org/d3.v2.min.js"></script>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.8/jquery.min.js"></script>
<title>Weighted Citation Graph</title>
<style>
path.link {
fill: none;
stroke: #666;
stroke-width: 1.5px;
}
circle {
fill: #ccc;
stroke: #333;
stroke-width: 1.5px;
}
text {
font: 10px sans-serif;
pointer-events: none;
}
text.shadow {
stroke: #fff;
stroke-width: 3px;
stroke-opacity: .8;
}
body {
background-color: white;
margin: 0px;
}
.graphContainer {
text-shadow: -1px -1px 0 white, 1px -1px 0 white, -1px 1px 0 white, 1px 1px 0 white;
}
</style>
<script>
function load_graph(text) {
var color = d3.scale.category20();
try{
//var data = JSON.parse(text);
} catch (e) {
window.alert("sometext: "+e);
}
var data = { "nodes": [{ "id": 127230, "name": "Optimization of query evaluation algorithms", "citation": 26, "group": 7 }, { "id": 127254, "name": "Flow algorithms for parallel query optimization", "citation": 22, "group": 7 }, { "id": 127380, "name": "Randomized approximation algorithms for query optimization problems on two processors", "citation": 14, "group": 7 }, { "id": 127438, "name": "Optimization algorithms for simultaneous multidimensional queries in OLAP environments", "citation": 12, "group": 7 }, { "id": 127063, "name": "Query optimization in database systems", "citation": 230, "group": 7 }, { "id": 127158, "name": "Query optimization in a memory-resident domain relational calculus database system", "citation": 41, "group": 7 }, { "id": 129760, "name": "An Overview of TQuel", "citation": 22, "group": 7 }, { "id": 129867, "name": "ADVISORS", "citation": 10, "group": 7 }, { "id": 129872, "name": "Tellabs and THRIP through the Telkom Centre of Excellence at Rhodes University.", "citation": 10, "group": 7 }, { "id": 127412, "name": "Optimal service ordering in decentralized queries over web services", "citation": 13, "group": 7 }, { "id": 130856, "name": "Queries over Web Services", "citation": 10, "group": 7 }, { "id": 130959, "name": "Exploiting Parallelism to Accelerate Keyword Search On Deep-web Sources", "citation": 10, "group": 7 }, { "id": 131199, "name": "Contents lists available at ScienceDirect Future Generation Computer Systems", "citation": 10, "group": 7 }, { "id": 131211, "name": "Flow Algorithms for Parallel Query Optimization", "citation": 10, "group": 7 }, { "id": 127373, "name": "Multi-query Optimization for On-Line Analytical Processing", "citation": 14, "group": 7 }, { "id": 133379, "name": "Concise descriptions of subsets of structured sets", "citation": 21, "group": 7 }], "links": [{ "source": 127230, "target": 127063, "name": "Most Similar", "value": 100, "grouo": "#1A4876" }, { "source": 127230, "target": 127158, "name": "71 %", "value": 71, "grouo": "#1F75FE" }, { "source": 127230, "target": 129760, "name": "Nothing Matched", "value": 10, "grouo": "#EE204D" }, { "source": 127230, "target": 129867, "name": "Nothing Matched", "value": 10, "grouo": "#EE204D" }, { "source": 127230, "target": 129872, "name": "Nothing Matched", "value": 10, "grouo": "#EE204D" }, { "source": 127230, "target": 127063, "name": "Most Similar", "value": 100, "grouo": "#1A4876" }, { "source": 127230, "target": 127158, "name": "71 %", "value": 71, "grouo": "#1F75FE" }, { "source": 127230, "target": 129760, "name": "Nothing Matched", "value": 10, "grouo": "#EE204D" }, { "source": 127230, "target": 129867, "name": "Nothing Matched", "value": 10, "grouo": "#EE204D" }, { "source": 127230, "target": 129872, "name": "Nothing Matched", "value": 10, "grouo": "#EE204D" }, { "source": 127254, "target": 127412, "name": "5 %", "value": 5, "grouo": "#1F75FE" }, { "source": 127254, "target": 130856, "name": "2 %", "value": 2, "grouo": "#1F75FE" }, { "source": 127254, "target": 130959, "name": "Least Similar", "value": 10, "grouo": "#ACE5EE" }, { "source": 127254, "target": 131199, "name": "Nothing Matched", "value": 10, "grouo": "#EE204D" }, { "source": 127254, "target": 131211, "name": "Most Similar", "value": 100, "grouo": "#1A4876" }, { "source": 127254, "target": 127412, "name": "5 %", "value": 5, "grouo": "#1F75FE" }, { "source": 127254, "target": 130856, "name": "2 %", "value": 2, "grouo": "#1F75FE" }, { "source": 127254, "target": 130959, "name": "Least Similar", "value": 10, "grouo": "#ACE5EE" }, { "source": 127254, "target": 131199, "name": "Nothing Matched", "value": 10, "grouo": "#EE204D" }, { "source": 127254, "target": 131211, "name": "Most Similar", "value": 100, "grouo": "#1A4876" }, { "source": 127438, "target": 127373, "name": "Most Similar", "value": 100, "grouo": "#1A4876" }, { "source": 127438, "target": 133379, "name": "3 %", "value": 3, "grouo": "#1F75FE" }, { "source": 127438, "target": 127373, "name": "Most Similar", "value": 100, "grouo": "#1A4876" }, { "source": 127438, "target": 133379, "name": "3 %", "value": 3, "grouo": "#1F75FE" }] };
// used to store the number of links between two nodes.
// mLinkNum[data.links[i].source + "," + data.links[i].target] = data.links[i].linkindex;
var mLinkNum = {};
// sort links first
// sortLinks();
data.links.sort(function (a, b) {
if (a.source > b.source) { return 1; }
else if (a.source < b.source) { return -1; }
else {
if (a.target > b.target) { return 1; }
if (a.target < b.target) { return -1; }
else { return 0; }
}
})
// set up linkIndex and linkNumer, because it may possible multiple links share the same source and target node
setLinkIndexAndNum();
var w = 1345,
h = 1000;
//var w = 3000,
// h = 3000;
var force = d3.layout.force()
.size([w, h])
.linkDistance(200)
.charge(-800)
.on("tick", tick);
var svg = d3.select(".graphContainer").append("svg:svg")
.attr("width", w)
.attr("height", h);
var color = d3.scale.category10()
var edges = [];
data.links.forEach(function (e) {
var sourceNode = data.nodes.filter(function (n) {
return n.id === e.source;
})[0],
targetNode = data.nodes.filter(function (n) {
return n.id === e.target;
})[0];
edges.push({
source: sourceNode,
target: targetNode,
name: e.name,
value: e.value,
linkindex: e.linkindex,
grouo: e.grouo
});
});
console.log(edges)
force
.nodes(data.nodes)
.links(edges)
.start();
var path = svg.append("svg:g")
.selectAll("line")
.data(edges)
.enter().append("svg:path")
.attr("class", "link")
.style("stroke-width", function (d, i) {
console.log(d.value)
return Math.sqrt(d.value);
}).style('stroke', function (d) {
return color(d.grouo);
});
//path.append("title").text(function (d) { return d.name });
var circle = svg.append("svg:g")
.selectAll("circle")
.data(force.nodes())
.enter().append("svg:circle")
.attr("r", function (d) {
return (Math.sqrt(d.citation));
})
.style("fill", function (d) {
return color(d.group);
})
.call(force.drag);
circle.append("title").text(function (d) { return d.name });
//circle.on("click", function () {
// d3.select(this)
// .attr("r", function (d) {
// return (Math.sqrt(d.citation) * 2);
// })
// .style("fill", "lightsteelblue");
//});
circle.on("click", function (d) {
var thisNode = d.id
var connected = data.links.filter(function (e) {
return e.source === thisNode || e.target === thisNode
});
circle.attr("opacity", function (d) {
return (connected.map(d => d.source).indexOf(d.id) > -1 || connected.map(d => d.target).indexOf(d.id) > -1) ? 1 : 0.1
});
path.attr("opacity", function (d) {
return (d.source.id == thisNode || d.target.id == thisNode) ? 1 : 0.1
});
})
circle.on("dblclick", function (d) {
var thisNode = d.id
var connected = data.links.filter(function (e) {
return e.source === thisNode || e.target === thisNode
});
circle.attr("opacity", function (d) {
return (connected.map(d => d.source).indexOf(d.id) > -1 || connected.map(d => d.target).indexOf(d.id) > -1) ? 1 : 1
});
path.attr("opacity", function (d) {
return (d.source.id == thisNode || d.target.id == thisNode) ? 1 : 1
});
})
var text = svg.append("svg:g")
.selectAll("g")
.data(force.nodes())
.enter().append("svg:g");
console.log('test');
//A copy of the text with a thick white stroke for legibility.
//text.append("svg:text")
//.attr("x", 8)
//.attr("y", ".31em")
//.attr("class", "shadow")
//.text(function (d) {
//return d.name;
//});
text.append("svg:text")
.attr("x", 8)
.attr("y", ".31em")
.text(function (d) {
// return d.name;
});
// Use elliptical arc path segments to doubly-encode directionality.
function tick() {
path.attr("d", function (d, i) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = 75 * d.linkindex; //linknum is defined above
var output = "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
//console.log(d)
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
});
// Add tooltip to the connection path
path.append("svg:title")
.text(function (d, i) {
return d.name;
});
circle.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
text.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
// sort the links by source, then target
function sortLinks1() {
data.links.sort(function (a, b) {
if (a.source > b.source) {
return 1;
} else if (a.source < b.source) {
return -1;
} else {
if (a.target > b.target) {
return 1;
}
if (a.target < b.target) {
return -1;
} else {
return 0;
}
}
});
}
//any links with duplicate source and target get an incremented 'linknum'
function setLinkIndexAndNum1() {
for (var i = 0; i < data.links.length; i++) {
if (i != 0 &&
data.links[i].source == data.links[i - 1].source &&
data.links[i].target == data.links[i - 1].target) {
data.links[i].linkindex = data.links[i - 1].linkindex + 1;
console.log(data.links[i].linkindex)
} else {
data.links[i].linkindex = 1;
console.log(data.links[i].linkindex)
}
// save the total number of links between two nodes
if (mLinkNum[data.links[i].target + "," + data.links[i].source] !== undefined) {
mLinkNum[data.links[i].target + "," + data.links[i].source] = data.links[i].linkindex;
} else {
mLinkNum[data.links[i].source + "," + data.links[i].target] = data.links[i].linkindex;
}
}
}
function setLinkIndexAndNum() {
for (var i = 0; i < data.links.length; i++) {
if (i != 0 &&
data.links[i].source == data.links[i - 1].source &&
data.links[i].target == data.links[i - 1].target) {
data.links[i].linkindex = data.links[i - 1].linkindex + 1;
}
else {
data.links[i].linkindex = 1;
};
};
}
}
</script>
</head>
<body>
<form id="form1" runat="server">
<div>
<script src="//d3js.org/d3.v3.min.js"></script>
<%--<textarea runat="server" id="textarea" cols="80" rows="20"></textarea>
<asp:Label ID="Label1" runat="server" Text="Label"></asp:Label>--%>
</div>
<div id="graphContainer" class="graphContainer"></div>
</form>
</body>
</html>
Can anybody help me out. thanks!
If you look at the data variable I have given different colors for each edge according to description of "name" attribute.
The reason you see different colours is because the colour scale d3.scale.category10() - like all d3 ordinal scales (at least in v3) - assigns its colours on a first come first served basis.
https://github.com/d3/d3-3.x-api-reference/blob/master/Ordinal-Scales.md#_ordinal
So if you changed your json so a node with group 'A' is encountered first, then it and subsequent group 'A' nodes will be given the first colour in the scale. If previously it was a node with group 'B' that was encountered first all the nodes with group 'B' would have got that colour instead.
One way to avoid this is to run through your data, collate the groups, order them, and pass them as a domain to your colour scale before setting the colours for individual nodes. That way A, B etc would be in the same order each time.
However, this would not help in situations where a change in your data meant particular groups were missing altogether, you'd still see a shift in the colour assignment. For that you'd need to supply a fixed list of all possible groups to assign to the colour scale, even if those groups weren't all in your current data.
PS. You assign color to the category20 colour scale, and then later on reassign it to category10. That doesn't have any effect on why you're seeing what you're seeing though as you don't use color till afterwards.
PPS. Another thing I've noticed is that your link colours, as defined by the .grouo data, are already hexadecimal colour strings. I suspect you don't mean to pass them to the color scale and mean to use them as they are i.e. style ("stroke", function(d) { return d.grouo; ]);

Json d3 access each object

{
"name": "Max",
"value": 107,
"children": [
{
"name": "Don",
"value": 60,
"children" [
{"name": "CC", "value": 25},
{"name": "Jim", "value": 35}
]
},
{
"name": "David",
"value": 47,
"children": [
{"name": "Jeff", "value": 32},
{"name": "Buffy", "value": 15}
]
}
]
}
How can I access the inner most child name with d3?
I tried doing :
.text(function (d) { return d.children ? null : d.name; });
But it didn't work....
When I do
.text(function (d) { return d.name });
it only shows the name of the outer loop --> Don, David.
d3.json('flare.json', function (data) {
var canvas = d3.select('p1')
.append('svg')
.attr('width', 800)
.attr('height', 800)
var color = d3.scale.category20c();
var data1 = data.children;
canvas.selectAll('text')
.data(data1)
.enter()
.append('text')
.attr('x', function (d) { return 2; })
.attr('y', function (d, i) { return i * 15; })
.attr('fill', 'black')
.style('font-size', '12px')
.text(function (d) { return d.children ? null: d.name; })
Data I had before ↓ ↓
{
"name": "Don",
"value": 75,
"children" [
{"name": "CC", "value": 25},
{"name": "Jim", "value": 35}
]
}
When the data was in this single nested format, my code worked perfectly, but when I did double nest on it, it no longer works
You need a recursive function for this --
function getNames(d) {
return d.children ? d.children.map(getNames) : d.name;
}
This will return a nested list with the names of the elements that have no children.

Resources