I am trying to implement an update function for my D3 sunburst diagram where i can change the data that is displayed. I am able to successfully add or remove nodes.
However, i can't seem to be able to modify the existing data.
For example, if half of my chart is removed, i would like for the other half to fill the space leaved by the delete. Same thing goes when new data get added, i would like for the existing data to shrink and take less space
Here is my update function :
function update(newData) {
root = newData;
node = root;
g = svg.datum(root)
.selectAll("g")
.data(partition.nodes(root));
var newG = g.enter()
.append("g");
g.exit().selectAll("path").transition().duration(5000).attrTween("d", arcTween(0)).remove();
path = g.selectAll("path").transition().duration(5000).attr("d", arc);
newG.append("path")
.attr("d", arc);
};
Here is how the chart is built :
function render(data) {
root = data;
node = root;
width = $(".burst-chart-container").height();
height = ($(".burst-chart-container").width() / 2);
radius = Math.min(width, height) / 2;
x = d3.scale.linear().range([0, 2 * Math.PI]);
y = d3.scale.linear().range([0, radius]);
rad = Math.min(width, height) / Math.PI - 25;
partition = d3.layout.partition().sort(null).value(function (d) { return d.size; });
arc = d3.svg.arc()
.startAngle(function (d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
.endAngle(function (d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
.innerRadius(function (d) { return Math.max(0, y(d.y)); })
.outerRadius(function (d) { return Math.max(0, y(d.y + d.dy)); });
svg = d3.select(element[0]).append('svg')
.attr("width", width).attr("height", height)
.attr("class", "svgDashboard")
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2) + ")");
g = svg.datum(root).selectAll("g")
.data(partition.nodes)
.enter()
.append("g")
path = g.append("path")
.attr("d", arc)
}
I do know that path.attr("d",arc) should update the visual, but it doesn't work in my case.
I think that it has something to do with the partition layout who dosen't tell the existing arcs that they need to change, or the way that I do the selection to update the data, but I might be wrong.
Any help would be appreciated.
I found out that the path and text data were never updated. My update did only change g element data. To correct it, I simply take the parent data and put into into his child after I updated it.
g = svg.datum(root)
.selectAll("g")
.data(partition.nodes(root));
//Add this part to update child elements
g.selectAll("path").each(function () {
d3.select(this).datum(d3.select(this.parentNode).datum());
});
g.selectAll("text").each(function () {
d3.select(this).datum(d3.select(this.parentNode).datum());
});
With those changes, i am able to call my attrTween function which update the visual with the new data.
Related
Not able to wrap labels of semi donut pie chart which is developed using d3 js.
I tried using CSS by giving word-wrap, max-width etc., but it's not working.
How can I warp the text labels if the labels are having more than three to fours words?
Here is my fiddle: https://jsfiddle.net/SampathPerOxide/hcvuqjt2/22/
var width = 400;
var height = 300; //this is the double because are showing just the half of the pie
var radius = Math.min(width, height) / 2;
var labelr = radius + 30; // radius for label anchor
//array of colors for the pie (in the same order as the dataset)
var color = d3.scale.ordinal()
.range(['#2b5eac', '#0dadd3', '#ffea61', '#ff917e', '#ff3e41']);
data = [{
label: 'CDU',
value: 10
},
{
label: 'SPD',
value: 15
},
{
label: 'Die Grünen',
value: 8
},
{
label: 'Die Mitte',
value: 1
},
{
label: 'Frei Wähler',
value: 3
}
];
var vis = d3.select("#chart")
.append("svg") //create the SVG element inside the <body>
.data([data]) //associate our data with the document
.attr("width", width) //set the width and height of our visualization (these will be attributes of the <svg> tag
.attr("height", height)
.append("svg:g") //make a group to hold our pie chart
.attr('transform', 'translate(' + (width / 2) + ',' + (height / 2) + ')'); //move the center of the pie chart from 0, 0 to radius, radius
var arc = d3.svg.arc() //this will create <path> elements for us using arc data
.innerRadius(79)
// .outerRadius(radius);
.outerRadius(radius - 10) // full height semi pie
//.innerRadius(0);
var pie = d3.layout.pie() //this will create arc data for us given a list of values
.startAngle(-90 * (Math.PI / 180))
.endAngle(90 * (Math.PI / 180))
.padAngle(.02) // some space between slices
.sort(null) //No! we don't want to order it by size
.value(function(d) {
return d.value;
}); //we must tell it out to access the value of each element in our data array
var arcs = vis.selectAll("g.slice") //this selects all <g> elements with class slice (there aren't any yet)
.data(pie) //associate the generated pie data (an array of arcs, each having startAngle, endAngle and value properties)
.enter() //this will create <g> elements for every "extra" data element that should be associated with a selection. The result is creating a <g> for every object in the data array
.append("svg:g") //create a group to hold each slice (we will have a <path> and a <text> element associated with each slice)
.attr("class", "slice"); //allow us to style things in the slices (like text)
arcs.append("svg:path")
.attr("fill", function(d, i) {
return color(i);
}) //set the color for each slice to be chosen from the color function defined above
.attr("d", arc); //this creates the actual SVG path using the associated data (pie) with the arc drawing function
const textEl = arcs.append("svg:text")
.attr("class", "labels") //add a label to each slice
.attr("fill", "grey")
.attr("transform", function(d) {
var c = arc.centroid(d),
xp = c[0],
yp = c[1],
// pythagorean theorem for hypotenuse
hp = Math.sqrt(xp * xp + yp * yp);
return "translate(" + (xp / hp * labelr) + ',' +
(yp / hp * labelr) + ")";
})
.attr("text-anchor", "middle"); //center the text on it's origin
textEl.append('tspan')
.text(function(d, i) {
return data[i].label;
});
textEl.append('tspan')
.text(function(d, i) {
return data[i].value;
})
.attr('x', '0')
.attr('dy', '1.2em');
arcs.append("svg:text")
.attr("class", "labels")//add a label to each slice
.attr("fill", "grey")
.attr("transform", function(d) {
var c = arc.centroid(d),
xp = c[0],
yp = c[1],
// pythagorean theorem for hypotenuse
hp = Math.sqrt(xp*xp + yp*yp);
return "translate(" + (xp/hp * labelr) + ',' +
(yp/hp * labelr) + ")";
})
.attr("text-anchor", "middle") //center the text on it's origin
.text(function(d, i) { return d.data.value; })
.text(function(d, i) { return d.data.label; });
//tooltip
arcs.on("mouseover", function(d) {
d3.select("#tooltip")
.style("left", `${d3.event.clientX}px`)
.style("top", `${d3.event.clientY}px`)
.classed("hidden", false);
d3.select("#tooltip-data")
.html(`Label: ${d.data.label}<br>Value: ${d.data.value}`);
});
arcs.on("mouseout", function(d) {
d3.select("#tooltip")
.classed("hidden", true);
});
So, I'm basically trying to make a multilevel circular partition (aka sunburst diagram) with D3.js (v4) and a JSON data.
I placed some labels, which must have different angles depending of their levels (circles) on the partition :
- Level < 3 must be curved and "follow" the arc radius.
- level == 3 must be straight and perpendicular of the arc radius.
I didn't use textPath tags, because I'm not really experienced in SVG and it looks overly complicated to me, and I don't really know how to use it.
here's my code (without the JSON but this is a really classical one, I can add a part of it if it is needed):
var width = 800;
var height = 800;
var radius = 400;
var formatNumber = d3.format(",d");
var x = d3.scaleLinear().range([0, 2 * Math.PI]);
var y = d3.scaleSqrt().range([0, radius]);
var arc = d3.arc()
.startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x0))); })
.endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x1))); })
.innerRadius(function(d) { return setRadius("inner", d.data.level); })
.outerRadius(function(d) { return setRadius("outer", d.data.level); });
var svg = d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width/2 + "," + (height/2) + ")");
var hierarchy = d3.hierarchy(dataset)
.sum(function(d) { return d.size; });
var partition = d3.partition();
svg.selectAll("path")
.data(partition(hierarchy).descendants())
.enter().append("path")
.attr("id", function(d, i){ return "path" + i; })
.attr("d", arc)
.attr("stroke", "white")
.attr("stroke-width", "1px")
.style("fill", function(d) { return (d.data.color) ? d.data.color : 'black'; });
svg.selectAll("text")
.data(partition(hierarchy).descendants())
.enter().append("text")
.attr("transform", function(d){ return setLabelPosition(d); })
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.attr("font-size", "18px")
.attr("fill", function(d){ return d.data.textcolor; })
.text(function(d){ if(parseInt(d.data.level) > 0 && parseInt(d.data.level) < 4){ return (d.data.name).toUpperCase(); }});
d3.select(self.frameElement)
.style("height", height + "px");
function setRadius(side, level){
var result = 0;
var innerValues = [0, 120, 180, 240, 365];
var outerValues = [0, 180, 240, 365, 400];
if(!side){
throw error;
}
if(side === "inner"){
result = innerValues[level];
}
if(side === "outer"){
result = outerValues[level];
}
return result;
};
function setLabelPosition(d){
var result = '';
var angle = 0;
var centroid = arc.centroid(d);
if(parseInt(d.data.level) === 3){
angle = (180/Math.PI * (arc.startAngle()(d) + arc.endAngle()(d))/2 - 90);
if(angle > 90){
angle = angle - 180;
}
result = "translate(" + centroid + ")rotate(" + angle + ")";
} else {
angle = (180/Math.PI * (arc.startAngle()(d) + arc.endAngle()(d))/2);
result = "translate(" + centroid + ")rotate(" + angle + ")";
}
return result;
};
And the result :
My problem is, how to curve these level 1 & 2 labels (like the one which have a red border), but keep my lvl 3 labels as they currently are.
It's really a pain in the head, and I did many search (on Google and SO) but I didn't find any satisfying answer.
A solution without using a textPath will be awesome if possible, but any advice is welcome.
Many thanks guys and sorry for my English (as you can probably see it's not my birth language).
PS : This is D3.js v4.
I've recently began trying to teach myself D3, and I'm to get my head round the enter, update, exit paradigm.
Below I have an example of some progress circles I'm trying to work with;
http://plnkr.co/edit/OoIL8v6FemzjzoloJxtQ?p=preview
Now, as the aim here is to update the circle path without deleting them, I believe I shouldn't be using the exit function? In which case, I was under the impression that I could update my data source inside a new function and then call for the path transition, and I would get my updated value. However, this is not the case.
I was wondering if someone could help me out and show me where I'm going wrong?
var dataset = [{
"vendor-name": "HP",
"overall-score": 45
}, {
"vendor-name": "CQ",
"overall-score": 86
}];
var dataset2 = [{
"vendor-name": "HP",
"overall-score": 22
}, {
"vendor-name": "CQ",
"overall-score": 46
}];
var width = 105,
height = 105,
innerRadius = 85;
var drawArc = d3.svg.arc()
.innerRadius(innerRadius / 2)
.outerRadius(width / 2)
.startAngle(0);
var vis = d3.select("#chart").selectAll("svg")
.data(dataset)
.enter()
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
vis.append("circle")
.attr("fill", "#ffffff")
.attr("stroke", "#dfe5e6")
.attr("stroke-width", 1)
.attr('r', width / 2);
vis.append("path")
.attr("fill", "#21addd")
.attr('class', 'arc')
.each(function(d) {
d.endAngle = 0;
})
.attr('d', drawArc)
.transition()
.duration(1200)
.ease('linear')
.call(arcTween);
vis.append('text')
.text(0)
.attr("class", "perc")
.attr("text-anchor", "middle")
.attr('font-size', '36px')
.attr("y", +10)
.transition()
.duration(1200)
.tween(".percentage", function(d) {
var i = d3.interpolate(this.textContent, d['overall-score']),
prec = (d.value + "").split("."),
round = (prec.length > 1) ? Math.pow(10, prec[1].length) : 1;
return function(t) {
this.textContent = Math.round(i(t) * round) / round + "%";
};
});
function updateChart() {
vis = vis.data(dataset2)
vis.selectAll("path")
.transition()
.duration(1200)
.ease('linear')
.call(arcTween);
vis.selectAll('text')
.transition()
.duration(1200)
.tween(".percentage", function(d) {
var i = d3.interpolate(this.textContent, d['overall-score']),
prec = (d.value + "").split("."),
round = (prec.length > 1) ? Math.pow(10, prec[1].length) : 1;
return function(t) {
this.textContent = Math.round(i(t) * round) / round + "%";
};
});
}
function arcTween(transition, newAngle) {
transition.attrTween("d", function(d) {
var interpolate = d3.interpolate(0, 360 * (d['overall-score'] / 100) * Math.PI / 180);
return function(t) {
d.endAngle = interpolate(t)
return drawArc(d);
};
});
}
Any help/advice is much appreciated!
Thanks all
You need to refresh your data through the DOM - svg > g > path :
// SET DATA TO SVG
var svg = d3.selectAll("svg")
.data(selectedDataset)
// SET DATA TO G
var g = svg.selectAll('g')
.data(function(d){return [d];})
// SET DATA TO PATH
var path = g.selectAll('path')
.data(function(d){ return [d]; });
Storing the d3 DOM data bind object for each step you can have control of the enter(), extit(), and transition() elements. Put changing attributes of elements in the transition() function:
// PATH ENTER
path.enter()
.append("path")
.attr("fill", "#21addd")
.attr('class', 'arc')
// PATH TRANSITION
path.transition()
.duration(1200)
.ease('linear')
.attr('d', function(d){ console.log(d);drawArc(d)})
.call(arcTween);
http://plnkr.co/edit/gm2zpDdBdQZ62YHhDbLb?p=preview
I'm trying to create a wordcloud with the D3 pack layout with a horizontal arrangement.
Instead of limiting the width, I am limiting the height.
The pack layout automatically disposes the circles with the larger one in the center and the others around him. If the height is limited, instead of expanding the circles disposition horizontally, it reduces the size of each circle.
How can I stop the layout from resizing the circles and start adding them to the sides if there is no more space around the larger one.
I want something like this:
http://imgur.com/7MDnKHF
But I'm only achieving this:
http://jsfiddle.net/v9xjra6c/
This is my current code:
var width,
height,
diameter,
padding,
format,
pack,
svg,
node;
var initSizes = function() {
var dimensions = { width: 900, height: 288 };
width = dimensions.width;
height = dimensions.height;
diameter = Math.min(width, height);
padding = 12;
format = d3.format(',d');
};
var initLayout = function() {
pack = d3.layout.pack()
.sort(null)
.size([width, height])
.padding(padding);
};
var createSVG = function() {
svg = d3.select('.chart-container').append('svg')
.attr('width', width)
.attr('height', height)
.attr('class', 'bubble');
};
var createBubbles = function() {
var dataset = pack.nodes(DATA);
node = svg.selectAll('.node')
.data(dataset.filter(function(d) { return !d.children; }))
.enter().append('g')
.attr('class', 'node')
.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; });
node.append('title')
.text(function(d) { return d.name + ': ' + format(d.value); });
node.append('circle')
.attr('r', function(d) { return d.r; });
node.append('text')
.attr('dy', '.3em')
.style('text-anchor', 'middle')
.text(function(d) { return d.name.substring(0, d.r / 3); });
};
initSizes();
initLayout();
createSVG();
createBubbles();
Thanks!
Your solution would be like merging this Example1 + Example2
So from Example 1 I have taken the mechanism to restrict the circles with in the bounds, such that it does not go beyond the svg height and width:
function tick(e) {
node
.each(cluster(10 * e.alpha * e.alpha))
.each(collide(.5))
//max radius is 50 restricting on the width
.attr("cx", function(d) { return d.x = Math.max(50, Math.min(width - 50, d.x)); })
//max radius is 50 restricting on the height
.attr("cy", function(d) { return d.y = Math.max(50, Math.min(height - 50, d.y)); }); }
Creating a scale for making radius
//so now for your data value which ranges from 0 to 100 you will have radius range from 5 to 500
var scale = d3.scale.linear().domain([0,100]).range([5, 50]);
Make the data as per Example2
var nodes = data.map(function(d){
var i = 0,
r = scale(d.value),
d = {cluster: i, radius: r, name: d.name};
if (!clusters[i] || (r > clusters[i].radius)) {clusters[i] = d;}
return d
});
Finally result will be looking like this
Note: You can reduce the height in the code and the circles will rearrange as per the space available.
Note: You can also play around the cluster to group similar nodes as in example in my case I have made a single group cluster.
Hope this helps!
I'm making a simple tool to display a set of values that are manipulated by the user. I want all the values to start at 0 and when the data is manipulated, to grow from there.
I have everything setup except that I get errors in the console when I start all my values at 0.
Is this possible?
Here's the code I have at the moment (which is working if the values are greater than 0):
var width = this.get('width');
var height = this.get('height');
var radius = Math.min(width, height) / 2;
var color = this.get('chartColors');
var data = this.get('chartData');
var arc = d3.svg.arc()
.outerRadius(radius)
.innerRadius(0);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.count; });
var id = this.$().attr('id');
var svg = d3.select("#"+id)
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var g = svg.selectAll("path")
.data(pie(data));
g.enter()
.append("path")
.attr("d", arc)
.each(function(d){ this._current = d; })
.style("fill", function(d, i) { return color[i]; })
.style("stroke", "white")
.style("stroke-width", 2);
The problem is a conceptual one -- if everything is 0, how are you going to draw a pie chart? You could however start with an empty data set and add new data as it becomes greater than zero. That leaves the problem of animating the growth of a pie chart segment from 0 to its desired size.
For this, you can animate the end angle of the pie chart segments starting at the start angle. The easiest way to do this is to copy the corresponding data object and tween the angle:
.each(function(d) {
this._current = JSON.parse(JSON.stringify(d));
this._current.endAngle = this._current.startAngle;
})
.transition().duration(dur).attrTween("d", arcTween);
Random example here.