Im learning d3 js layout and having difficulty in adding node to the tree layout.
Hoping for a way of adding dynamic node(children) after clicking the parent node.
My current implementation do add the node, but instead of updating it, it added new children and keep the child from before.
Can someone help me understand the problem and a correct way of approaching this.
Here is my code and my Fiddle (click on the root node):
HTML
<div id="body">
</div>
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
JS
var margin = {top: 100, right: 50, bottom: 100, left: 50},
width = 900 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var tree = d3.layout.tree()
.separation(function(a, b) { return a.children === b.children ? 1 : 1.2; })
.size([width, height]);
var svg = d3.select("body")
.attr("bgcolor", "#fff")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var dataOne = {
"name": "Mike and Marcia",
"children": [
{
"name": "Children",
"children": [
{ "name": "Mikael" }
]
}
]
};
var drawTree = function(source){
var nodes = tree.nodes(source);
var node = svg.selectAll(".node")
.data(nodes)
.enter()
.append("g");
var boxes = node.selectAll('g')
.data(nodes)
.enter()
.append('g')
.append('rect')
.attr("width", 50)
.attr("height", 40)
.attr("fill", "tan")
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y + 50; });
boxes.on("click", function(d,i){
clickOutside(d);
});
};
var clickOutside = function(value){
var newData = {
"name": "Mike and Marcia",
"children": [{
"name": "Children",
"children": [{
"name": "Mikael"
}]
},{
"name": "Pets",
"children": []
}]
};
drawTree(newData);
console.log(value);
}
drawTree(dataOne);
The problem is that you are drawing the new graph over the old graph.
That is the reason why you get the impression that its adding child to the old parent.
So the correct approach would be to
draw the graph
remove all nodes that is not required.
So 1st point
var nodedata = svg.selectAll(".node")
.data(nodes, function(d){ /* function which return the Unique id of each node */ return d.name;})
//make all the nodes.
nodedata.enter().append('g')
.attr("class", "node")
.append('rect')
.attr("width", 50)
.attr("height", 40)
.attr("fill", "tan")
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y + 50; })
.on("click", function(d,i){
clickOutside(d);
});
2nd point
Remove all nodes which are intersection of first set of passed data and the second set of passed data.
nodedata.exit().remove();
Last point add children to parent
You can change the JSON
var dataOne = {
"name": "Mike and Marcia",
"children": [
{
"name": "Children",
"children": [
{ "name": "Mikael" }
]
}
]
};
Add whatever structure JSON and pass it down your drawTree function
Working code here
Related
I am using D3.js to produce Sankey diagrams, and I'm not satisfied with the resulting connection between nodes, particularly when the total input of a node is different from its total output.
As an example:
// set the dimensions and margins of the data
var margin = { top: 10, right: 10, bottom: 10, left: 10 },
width = 300 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3.select("#graph").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Set the sankey diagram properties
var sankey = d3.sankey()
.nodeWidth(10)
.nodePadding(10)
.size([width, height]);
data = {
"nodes": [
{ "node": 0, "name": "A" },
{ "node": 1, "name": "B" },
{ "node": 2, "name": "C" }
],
"links": [
{ "source": 0, "target": 1, "value": 2 },
{ "source": 1, "target": 2, "value": 1 }
]
}
// Constructs a new Sankey generator with the default settings.
sankey
.nodes(data.nodes)
.links(data.links)
.layout(1);
// add in the links
svg.append("g")
.selectAll(".link")
.data(data.links)
.enter()
.append("path")
.attr("class", "link")
.attr("d", sankey.link())
.style("stroke-width", function (d) { return Math.max(1, d.dy); })
// add in the nodes
var node = svg.append("g")
.selectAll(".node")
.data(data.nodes)
.enter()
.append("g")
.attr("class", "node")
.attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; })
// add the rectangles for the nodes
node.append("rect")
.attr("height", function (d) { return d.dy; })
.attr("width", sankey.nodeWidth())
// add in the title for the nodes
node.append("text")
.attr("x", -6)
.attr("y", function (d) { return d.dy / 2; })
.attr("dy", ".35em")
.attr("text-anchor", "end")
.attr("transform", null)
.text(function (d) { return d.name; })
.filter(function (d) { return d.x < width / 2; })
.attr("x", 6 + sankey.nodeWidth())
.attr("text-anchor", "start")
<body>
<style>
.link {
fill: none;
stroke: #000;
stroke-opacity: .2;
}
</style>
<div id="graph"></div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/holtzy/D3-graph-gallery#master/LIB/sankey.js"></script>
<script src="test.js"></script>
</body>
This produces:
But without chaning the input data I would like:
Meaning the node size is determined by the link value, but the link size matches both the input and the output nodes.
I have been working off of this basic pie example to learn about writing pie charts in D3, but while I got the example to work when mimicking their data structure, I wanted to try it out with a JSON data structure that would be more native to how I would structure data for the visualizations. When switching the same data to this new structure I noticed that the black stroke appears and the annotations, but the slices aren't present and the annotation labels are referencing an index and object value.
I believe this is due to the .entries() method that converts it to a key-value data structure, but I'm curious if that is a necessary method to use in order to visualize the data points or if there is a simpler method to utilize the structure I have in place.
Working data structure:
var data = {
deep: 22.37484390963787,
light: 62.65183335225337,
rem: 14.973322738108752
}
JSON data structure:
var data = [
{ "label": "deep", "value": 22.37484390963787 },
{ "label": "light", "value": 62.65183335225337 },
{ "label": "rem", "value": 14.973322738108752 }
]
var data = [
{ "label": "deep", "value": 22.37484390963787 },
{ "label": "light", "value": 62.65183335225337 },
{ "label": "rem", "value": 14.973322738108752 }
]
// var data = {
// deep: 22.37484390963787,
// light: 62.65183335225337,
// rem: 14.973322738108752
// }
console.log(data)
var width = 480;
var height = 480;
var margin = 40;
var radius = Math.min(width, height) / 2 - margin;
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var color = d3.scaleOrdinal()
.domain(data)
.range(["#98abc5", "#8a89a6", "#7b6888"]);
var pie = d3.pie()
.value(function(d) { return d.value; });
var data_ready = pie(d3.entries(data));
console.log(data_ready);
var arcGenerator = d3.arc()
.innerRadius(0)
.outerRadius(radius);
svg.selectAll('viz')
.data(data_ready)
.enter()
.append('path')
.attr('d', arcGenerator)
.attr('fill', function(d){ return color(d.data.key)})
.attr("stroke", "black")
.style("stroke-width", "2px")
.style("opacity", 0.7);
svg.selectAll('viz')
.data(data_ready)
.enter()
.append('text')
.text(function(d){ return d.data.key + ', ' + d.data.value})
.attr("transform", function(d) { return "translate(" + arcGenerator.centroid(d) + ")"; })
.style("text-anchor", "middle")
.style("font-size", 17);
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>
The other way to achieve this is to not use d3.entries and pass your data directly. A couple of other tweaks are required where you get the color and label text (ie use d.data.label in place of d.data.key).
var data = [
{ "label": "deep", "value": 22.37484390963787 },
{ "label": "light", "value": 62.65183335225337 },
{ "label": "rem", "value": 14.973322738108752 }
]
// var data = {
// deep: 22.37484390963787,
// light: 62.65183335225337,
// rem: 14.973322738108752
// }
console.log(data)
var width = 480;
var height = 480;
var margin = 40;
var radius = Math.min(width, height) / 2 - margin;
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var color = d3.scaleOrdinal()
.domain(data)
.range(["#98abc5", "#8a89a6", "#7b6888"]);
var pie = d3.pie()
.value(function(d) { return d.value; });
var data_ready = pie(data);
console.log(data_ready);
var arcGenerator = d3.arc()
.innerRadius(0)
.outerRadius(radius);
svg.selectAll('viz')
.data(data_ready)
.enter()
.append('path')
.attr('d', arcGenerator)
.attr('fill', function(d){ return color(d.data.label)})
.attr("stroke", "black")
.style("stroke-width", "2px")
.style("opacity", 0.7);
svg.selectAll('viz')
.data(data_ready)
.enter()
.append('text')
.text(function(d){ return d.data.label + ', ' + d.data.value})
.attr("transform", function(d) { return "translate(" + arcGenerator.centroid(d) + ")"; })
.style("text-anchor", "middle")
.style("font-size", 17);
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>
You can't just change the format of the data without changing something else too. The simplest solution is to reformat your structure into the structure that d3 expected in the first place:
var formatted_data = data.reduce((acc,i) => {acc[i.label] = i.value; return acc;},{});
And then pass that to entries:
var data = [
{ "label": "deep", "value": 22.37484390963787 },
{ "label": "light", "value": 62.65183335225337 },
{ "label": "rem", "value": 14.973322738108752 }
]
// var data = {
// deep: 22.37484390963787,
// light: 62.65183335225337,
// rem: 14.973322738108752
// }
var formatted_data = data.reduce((acc,i) => {acc[i.label] = i.value; return acc;},{});
console.log(formatted_data)
var width = 480;
var height = 480;
var margin = 40;
var radius = Math.min(width, height) / 2 - margin;
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var color = d3.scaleOrdinal()
.domain(data)
.range(["#98abc5", "#8a89a6", "#7b6888"]);
var pie = d3.pie()
.value(function(d) { return d.value; });
var data_ready = pie(d3.entries(formatted_data));
console.log(data_ready);
var arcGenerator = d3.arc()
.innerRadius(0)
.outerRadius(radius);
svg.selectAll('viz')
.data(data_ready)
.enter()
.append('path')
.attr('d', arcGenerator)
.attr('fill', function(d){ return color(d.data.key)})
.attr("stroke", "black")
.style("stroke-width", "2px")
.style("opacity", 0.7);
svg.selectAll('viz')
.data(data_ready)
.enter()
.append('text')
.text(function(d){ return d.data.key + ', ' + d.data.value})
.attr("transform", function(d) { return "translate(" + arcGenerator.centroid(d) + ")"; })
.style("text-anchor", "middle")
.style("font-size", 17);
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>
I'm working on a D3 data visualization and I'm trying to get my first bar chart to disappear and then have a new one appear exactly where the last one was. My problem is the second one is getting overlayed right above the first one so everything gets muddled up. Any help would be greatly appreciated.
Here is my code:
var data = [{
"name": "text",
"value": 2,
},
{
"name": "text",
"value": 44,
},
{
"name": "text",
"value": 20,
},
{
"name": "text",
"value": 18,
},
{
"name": "text",
"value": 12,
},
{
"name": "text",
"value": 9,
},
{
"name": "text",
"value": 7,
},
{
"name": "text",
"value": 6,
},
{
"name": "text",
"value": 5,
},
{
"name": "text",
"value": 4,
},
];
var margin = {
top: 15,
right: 35,
bottom: 15,
left: 115
};
var width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
//var startBar2010 =
startBarChart.append("g").attr("class","bar2010");
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
//.attr("align","center")
.append("g")
.attr("transform", "translate(" + margin.left + "," +
margin.top + ")");
//.attr("transform", "translate(" + 200 + "," + (-500) + ")");
var x = d3.scale.linear()
.range([0, width])
.domain([0, d3.max(data, function (d) {
return d.value;
})]);
var y = d3.scale.ordinal()
.rangeRoundBands([height, 0], .1)
.domain(data.map(function (d) {
return d.name;
}));
//make y axis to show bar names
var yAxis = d3.svg.axis()
.scale(y)
//no tick marks
.tickSize(0)
.orient("left");
var gy = svg.append("g")
.attr("class", "y axis")
.transition().delay(delay*1).ease(d3.easeLinear).duration(1000)
.call(yAxis);
var bars = svg.selectAll(".bar")
.data(data)
.enter()
.append("g");
//append rects
bars.append("rect")
.attr("class", "bar")
.attr("x", -500)
.attr("y", -25)
.attr("width", 0)
.attr("height", 35)
.transition().delay(delay*1).ease(d3.easeLinear).duration(1000)
//.transition().delay(delay*1).duration(2500)
.attr("y", function (d) {
return y(d.name);
})
.attr("height", y.rangeBand())
.attr("x", 0)
.attr("width", function (d) {
return x(d.value);
})
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("text-decoration", "underline")
.text("Do I Work ?");
bars.append("text")
.attr("class", "label")
.attr("y", -42)
.attr("x", -520)
.transition().delay(delay*1).ease(d3.easeLinear).duration(1000)
//y position of the label is halfway down the bar
.attr("y", function (d) {
return y(d.name) + y.rangeBand() / 2 + 4;
})
//x position is 3 pixels to the right of the bar
.attr("x", function (d) {
return x(d.value) + 3;
})
.text(function (d) {
return d.value;
});
I thought the following code would clear everything for the follow up bar chart
bars.selectAll("g")
//.transition().delay(delay*3).duration(1000)
.transition().delay(delay*1.7).duration(1000)
.style("opacity",0)
.call(endall, function() {
bars.selectAll("g")
.remove();
});
Here is second bar chart data and chart:
var dataTwo = [{
"name": "text",
"value": 2,
},
{
"name": "text",
"value": 44,
},
{
"name": "text",
"value": 20,
},
{
"name": "text",
"value": 18,
},
{
"name": "text",
"value": 12,
},
{
"name": "text",
"value": 9,
},
{
"name": "text",
"value": 7,
},
{
"name": "text",
"value": 6,
},
{
"name": "text",
"value": 5,
},
{
"name": "text",
"value": 4,
},
];
var margin = {
top: 15,
right: 35,
bottom: 15,
left: 115
};
var width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
//var startBar2010 =
startBarChart.append("g").attr("class","bar2010");
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
//.attr("align","center")
.append("g")
.attr("transform", "translate(" + margin.left + "," +
margin.top + ")");
//.attr("transform", "translate(" + 200 + "," + (-500) + ")");
var x = d3.scale.linear()
.range([0, width])
.domain([0, d3.max(dataTwo, function (d) {
return d.value;
})]);
var y = d3.scale.ordinal()
.rangeRoundBands([height, 0], .1)
.domain(dataTwo.map(function (d) {
return d.name;
}));
//make y axis to show bar names
var yAxis = d3.svg.axis()
.scale(y)
//no tick marks
.tickSize(0)
.orient("left");
var gy = svg.append("g")
.attr("class", "y axis")
.transition().delay(delay*2.1).ease(d3.easeLinear).duration(1000)
.call(yAxis);
// Update with new values
var bars = svg.selectAll(".bar")
.data(dataTwo)
.enter()
.append("g");
//append rects
bars.append("rect")
.attr("class", "bar")
.attr("x", -500)
.attr("y", -25)
.attr("width", 0)
.attr("height", 35)
.transition().delay(delay*2.1).ease(d3.easeLinear).duration(1000)
//.transition().delay(delay*1).duration(2500)
//.selection().delay(delay*1).duration(2500)
.attr("y", function (d) {
return y(d.name)
})
.attr("height", y.rangeBand())
.attr("x", 0)
.attr("width", function (d) {
return x(d.value)
});
bars.append("text")
.attr("class", "label")
.attr("y", -42)
.attr("x", -520)
.transition().delay(delay*2.1).ease(d3.easeLinear).duration(1000)
//y position of the label is halfway down the bar
.attr("y", function (d) {
return y(d.name) + y.rangeBand() / 2 + 4;
})
//x position is 3 pixels to the right of the bar
.attr("x", function (d) {
return x(d.value) + 3;
})
.text(function (d) {
return d.value;
});
Just as the combination of enter().append() is used in D3.js to create DOM elements to reflect dataset elements that don't yet exist in the DOM, likewise the exit().remove() combination is used to remove those DOM elements.
What you will want to do, then, is to have a DOM element acting as a container which you use as the root of your D3 manipulations. Generally, we have a div to which we add the chart's SVG - but you should know which element this is, whether body, a div or something else. Everything that you add into that container, should be removed by 2 steps:
Implementing your exit().remove() as described below.
Clearing your entire dataset currently bound to that set of elements.
This is a useful description. Mike Bostock (D3.js's author) has a diagram clarifying exactly what enter, exit and update mean; "elements" here means "DOM elements", while "data" means "data array elements":
I am using http://d3js.org/d3.v3.min.js.
I have code that append svg to my body html element.
var svg = d3.select("body").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
This code append <svg> to <body>
I am new in javascript and i don't know how to write code that append this svg to my div: <div id="svg">
I try: var svg = d3.select(document.getElementById("svg")).append("svg")... but it not working.
Thank you for any help!
Edit:
There is a full code for testing
<style>
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 3px;
}
.node text { font: 12px sans-serif; }
.link {
fill: none;
stroke: #ccc;
stroke-width: 2px;
}
</style>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var treeData = [
{
"name": "Top Level",
"parent": "null",
"children": [
{
"name": "Level 2: A",
"parent": "Top Level",
"children": [
{
"name": "Son of A",
"parent": "Level 2: A"
},
{
"name": "Daughter of A",
"parent": "Level 2: A"
}
]
},
{
"name": "Level 2: B",
"parent": "Top Level"
}
]
}
];
// ************** Generate the tree diagram *****************
var margin = {top: 20, right: 120, bottom: 20, left: 120},
width = 960 - margin.right - margin.left,
height = 500 - margin.top - margin.bottom;
var i = 0;
var tree = d3.layout.tree()
.size([height, width]);
var diagonal = d3.svg.diagonal()
.projection(function (d) {
return [d.y, d.x];
});
// var svg = d3.select("div#tree").append("svg")
var svg = d3.select("#tree").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
root = treeData[0];
update(root);
function update(source) {
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Normalize for fixed-depth.
nodes.forEach(function (d) {
d.y = d.depth * 180;
});
// Declare the nodes…
var node = svg.selectAll("g.node")
.data(nodes, function (d) {
return d.id || (d.id = ++i);
});
// Enter the nodes.
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function (d) {
return "translate(" + d.y + "," + d.x + ")";
});
nodeEnter.append("circle")
.attr("r", 10)
.style("fill", "#fff");
nodeEnter.append("text")
.attr("x", function (d) {
return d.children || d._children ? -13 : 13;
})
.attr("dy", ".35em")
.attr("text-anchor", function (d) {
return d.children || d._children ? "end" : "start";
})
.text(function (d) {
return d.name;
})
.style("fill-opacity", 1);
// Declare the links…
var link = svg.selectAll("path.link")
.data(links, function (d) {
return d.target.id;
});
// Enter the links.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", diagonal);
}
</script>
Simply use d3.select("#svg").
I've recently created a D3 chart that uses JSON to populate it. The chart is made up of nodes that have the movies name next to it. Then the chart has links which connect each node together. However I also have a panel which I want to display the nodes information. For example if I clicked on the 'blu-ray' node, the panel will populate the node title 'blu-ray' and all the movies that are of the blu-ray format. These are all stored in the JSON array. How would I go about doing this? Close example to what I'm trying to achive is on http://jsfiddle.net/sXkjc/994/... except I want to populate the panels information by clicking the node instead of the buttons, can someone please help?
D3 Code:
<script>
//Setting the svg & the charts width etc
var diameter = 560;
var tree = d3.layout.tree()
.size([360, diameter / 2 - 120])
.separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; });
var diagonal = d3.svg.diagonal.radial()
.projection(function(d) { return [d.y, d.x / 180 * Math.PI]; });
var svg = d3.select("body").append("svg")
.attr("width", diameter)
.attr("height", diameter - 5)
.append("g")
.attr("transform", "translate(" + diameter / 2 + "," + diameter / 2 + ")");
//connecting the JSON file
d3.json("data.json", function(error, root) {
var nodes = tree.nodes(root),
links = tree.links(nodes);
//creating the links
var link = svg.selectAll(".link")
.data(links)
.enter().append("path")
.attr("class", "link")
.attr("d", diagonal);
//creating the circles
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; })
node.append("circle")
.attr("r", 4.5);
node.append("text")
.attr("dy", ".31em")
.attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
.attr("transform", function(d) { return d.x < 180 ? "translate(8)" : "rotate(180)translate(-8)"; })
.text(function(d) { return d.name; });
});
d3.select(self.frameElement).style("height", diameter - 150 + "px");
</script>
JSON:
{
"name": "Movies",
"children": [
{
"name": "Blu-Ray",
"children": [
{
"name": "Transformers",
"url": "www.my-media-website.com",
"dependsOn": ["Content API", "Search API", "Account API", "Picture API", "Facebook", "Twitter"],
"technos": ["PHP", "Silex", "Javascript", "NGINX", "Varnish"],
"host": { "Amazon": ["fo-1", "fo-2"] }
},
{
"name": "Saving Private Ryan",
"dependsOn": ["Content API", "Search API", "Account API"]
},
{
"name": "Star Trek Into Darkness",
"dependsOn": ["Content API"]
}
],
"dependsOn": ["Transformers", "Saving Private Ryan", "Star Trek Into Darkness"]
}
]}
Panel:
<div class="panel">
<h3>You have selected:</h3>
<p>Node Detail will go the p tags!</p>
</div>
You have to do two things:
1. Attach 'id' attribute to each of your node. This id should be unique. you can use id field from your json.
2. Write click handler for your nodes like following:
node.on('click', function(d) {
var clickedId = d3.select(this).attr("id");
/* your code to use clickedId to get the data of clicked node from your JSON */
});
This will solve your problem.