I'm a complete noop to D3 and partly SVG, so I got a few basic questions.
First off, my code in question can be viewed at http://dotnetcarpenter.github.io/d3-test/ and I've used Simple Pie Chart example with D3.js and Pie Chart Update, II as examples to get a running start.
As you can see, the animation gets skewed in the end when the low path values switch to the higher values. This is obviously not what I want. I think I'm getting the order of calculations wrong but I'm not sure what to do. I'm using the code from the last example:
function change() {
//...
path.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
}
// where arcTween is
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
Another issue is placing labels on the sectors. I've put the update stuff in the change function and is able to read out and only render them if the value is between 0 and 100. I can't however place them in any way. Looking at the first example, I figure that I could do something like this:
text.data(data)
.text(setText)
.attr("transform", function (d) {
// we have to make sure to set these before calling arc.centroid
d.innerRadius = 0;
d.outerRadius = radius;
return "translate(" + arc.centroid(d) + ")";
})
.attr("text-anchor", "middle") //center the text on it's origin
Where text is a d3 selection and arc is: d3.svg.arc().outerRadius(radius)
But I get "Unexpected value translate(NaN,NaN) parsing transform attribute." warning in Firefox and the labels are written on top of each other.
I appreciate any help and hints. Thanks!
I finally figured it out.
Maintain sector order throughout an animation.
You'd be forgiven for thinking that object contancy had something do with it. I did. But it turns out to be much simpler than that.
Every pie chart is by default sorted by value. If you don't want to sort by value but e.g. by data list order, you just have to disable sorting.
var pie = d3.layout.pie() // get a pie object structure
.value(function(d) { // define how to get your data value
return d.value; // (based on your data set)
})
.sort(null); // disable sort-by-value
Positioning labels according to your chart
Basically, you need to calculate your label positions depending on the type of chart or graph, your trying to connect them to. In my case, it's a pie chart. So if I want d3 to help with the calculations, I need to tell centroid the inner and outer radius and, most importantly to my issue, the start and end angles. The latter was missing from my code. Getting these values is as simple as, calling our pie layout above with our dataset and then do a transform.
Note that you don't have to call .data() again if you created the SVG with d3 and already supplied your data wrapped in .pie() structure. That is, that you didn't select any existing SVG from your page.
var svg = d3.select("svg")
// do stuff with your svg
var pie = d3.layout.pie()
// set stuff on your layout
var text = svg.selectAll("text")
.data(pie(dataset)) // where dataset contains your data
.attr("transform", function(d) {
return "translate(" + arc.centroid(d) + ")";
});
I have to give credit to Philip Pedruco for helping me along the way.
Bonus info
Use viewBox if you want to position your SVG cross browser, not transform/translate.
// move pie to center
d3.select("svg").attr("viewBox", -radius + ","+ -radius +"," + size + "," + size)
Related
I'm trying to update my x axis in a D3js bar chart (is partially working) depending on a user filter, the bars are actually changing but is not doing it well. I don't really know where is the problem and I need some help.
in this part of the code I'm updating the bar chart
function update(selectedGroup) {
svg.selectAll("rect").remove()
var groups = d3.map(dataFilter, function(d){return(d.group)}).keys();
x.domain(groups);
var dataFilter = result.filter(function(d){return d.group==selectedGroup});
console.log(dataFilter);
var rectG=rectangulos(dataFilter)
}
the complete bar chart
how is working now:
the result should be something like this
I have an live example here
There is a relatively straightforward reason you are seeing this behavior.
When the domain of the scale x is all the groups x.bandwidth() is small. But when the domain of x is only one value, x.bandwidth() is large. In both cases, the first band starts in the same location.
Next we have a nested scale here xSubgroup - the range of this is equal to x.bandwidth(). When the domain of x changes, we need to update the range of xSubgroup. If we don't do this, the bars will still be very thin and start at the beginning of the axis (as the bars' bandwidth aren't changing even if the group's bandwidth does). You don't update the sub scale's range, but we need to do that:
x.domain(groups);
xSubgroup.range([0, x.bandwidth()])
With this we get the update we're looking for.
But the axis labels remain unchanged. Updating a scale doesn't update the axis unless we explicitly do so. I'll break up your method chaining and store a reference for the g holding the axis:
var xAxis = svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
xAxis.selectAll("text")
...
Now we can update the axis, I'm carrying forward the text styling as well. You can simplify the code by using an update function to do all the entering/exiting/updating of axes and data - here we have some duplication in that both the initial set up and the update function have overlap.
To update the axis we use:
// Call the axis again to update
xAxis.call(d3.axisBottom(x))
xAxis.selectAll("text")
.style("text-anchor", "end")
.attr("font-size", "55px")
.attr("y", "-7")
.attr("x", "-7")
.attr("transform", "rotate(-90)");
Which gives the desired behavior if I understand correctly, updated plunkr
I'm studying transitions in D3js, trying to get them working with a simple pie chart that is derived from a Mike Bostock example.
I want to transition the data in the pie from data to data2, but the chart does not update.
This is the transition statement:
path.selectAll("path").data(pie(data2)).transition().duration(2000);
What am I missing?
EDIT
I've got it working with the below. Now I'd like to understand why this is working. I understand the .attr("d",arc) part, but why do I need to selectAll("path")?
path.selectAll("path").data(pie(data2)).transition().duration(2000).attr("d",arc);
END EDIT
The complete code (JSFiddle here):
var width = 200,
height = 200,
radius = Math.min(width, height) / 2;
var color = d3.scale.category20();
var data = [2, 19, 18, 99, 100];
var data2 = [100, 1200, 20, 88, 12];
var pie, arc, svg, path, data;
var chartCanvas = d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height);
var path = chartCanvas
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
function setupPieChart() {
pie = d3.layout.pie()
.value(function (d) {
return d;
})
.sort(null);
arc = d3.svg.arc()
.innerRadius(radius - 100)
.outerRadius(radius - 20)
path
.selectAll("path")
.data(pie(data))
.enter()
.append("path")
.attr("fill", function (d, i) {
return color(i);
})
.attr("d", arc)
.each(function (d) {
this._current = d;
}); // store the initial angles
console.log(path);
}
setupPieChart();
console.log(path);
path.selectAll("path").data(pie(data2)).transition().duration(2000);
.transition starts a transition for the attributes change(s) declared after it. You don't set / do anything after .transition, so there is nothing to interpolate over. In the original example from Mike, you'll see he sets the d attribute after starting the transition, specifying a custom interpolator (arcTween)
I immediately see you are missing some important part of the update process. You copied the original code, but you forgot the update part :-). I can tell, because I see in your code you store the initial angles.
Look again at the code here and try to understand the function arcTween. More information can be found here. You need an arc tween function to calculate the new angles based on the initial angles (which is why you stored the initial angles in the first place :-).
I won't do the fiddle at the moment, cause in my experience, you learn more if you try to understand the arc tween function (as I did here . This is a link to a personal project of mine, but feel free to copy code as you see fit).
You need to .selectAll("path") as those are the actual elements that will update. When doing d3, try to think of the chart elements as following: Elements that are not visible yet (which is enter collection), elements that are visible now (which can be seen as the update collection) and elements that can be removed (the exit collection).
You need to see those elements based on the data you want to visualize. D3 is data driven documents, so everything is in relation to the data you want to show on the screen: if you have data but no elements yet, you do an "enter" of elements. So you do a selection of elements that are not in the DOM yet, but will soon be, because you will bind them to the data you have.
If you already have elements on the screen, and the number of elements matches the data you need to show (for example: var data = [20 ,30 , 40], you got 3 pieces of data here for 3 div's on the screen, you got a matching amount), then d3 will update your selection (hence the update collection or update selection) so the elements properties match the data.
If you have more elements on the screen then there is data to show, then you can do an exit of elements (again, hence the exit selection or collection). I hope that made it a bit more clear and that it made sense as well :-)
I'm using Mike Bostock's example as a template and building on it. My bar chart here.
After transition to stacked version, I am unable to get the y position of the bars. Bars of higher height overshadow the smaller ones. Most likely because of the valueOffset attribute of the stack. I am stuck on this issue for few days now.
Changes from Mike's example:
removed group labels in stacked chart
new y-axis y2 on linear scale. The domain for this axis is from 0 to the maximum of all the sums of values in each year which is 141.
defined new stack stack_year for relative positions of the bars.
Relevant code:
// y2 definition
y2.domain([0, d3.max(dataByGroup_year, function(d) { return d.year_wise_sum; })]).range([height, 0]);
// calculates sum of all wins per year
dataByGroup_year.forEach(function(d) {
var order = d.values.map(function(d) { return d.value; });
d.year_wise_sum = d3.sum(order);
});
function transitionStacked() {
var t = svg.transition().duration(750),
g = t.selectAll(".group").attr("transform", "translate(0," + y0(y0.domain()[0]) + ")");
g.selectAll("rect").attr("x", function(d) { return x(d.year); })
.attr("y", function(d) { return height - y2(d.valueOffset); })
.attr("height", function(d) { return height - y2(d.value); });
g.selectAll(".group-label").text("");
}
y0 is the ordinal scale used for multiple charts. y1 is the linear scale used for each chart in multiple charts.
Full HTML code at github
Data used: input file. I disabled tips for each bar.
Update: JSFIDDLE
Any help is much appreciated! Thank you
There were a number of issues here, which I've fixed up in this fiddle: http://jsfiddle.net/henbox/wL9x6cjk/4/
Part of the problems was the data itself (as per my comment above). There were some repeated values, which was causing issues when calculating the valueOffset correctly (using the d3.layout.stack)
I've also made some changes to how the y and attribute for each rect are calculated in the transitionStacked function. I changed what you had:
.attr("y", function(d) {
return height - y2(d.valueOffset);
})
to:
.attr("y", function (d) {
return y2(d.value + d.valueOffset) - height;
})
Note that you need to sum the d.value and d.valueOffset, before applying the scaling, to calculate the top left corner position of the rect. Additionally, you don't need to recalculate the x attribute value since this doesn't change between the two chart views, so I removed it
I also removed the call to stack_year(dataByGroup_year);. You don't need to build the stack layout here, just to calculate the maximum sum per year.
Finally I also tidied up the y-axis positioning a bit so there's enough space for the x-axis labels, and simplified the positioning of group elements in the stacked view. I also moved the x-axis to be appended to svg rather than group, which simplified positioning of elements
This D3 example served as my starting point:
http://bl.ocks.org/kerryrodden/7090426
I wanted to change data that feeds the diagram, and I made following new example:
http://jsfiddle.net/ZGVK3/
One can notice at least two problems:
Legend is wrong. This is because it still contains 'hardcoded' names from original example.
All nodes are colored black. This is because the color scheme is also 'hardcoded' only for node names from original example.
How to improve the original example (or my jsfiddle, it doesn't matter) so that legend and coloring are self-adjusted to the data that feeds the diagram?
You can use an ordinal scale to map colors to the different node names. Implementing it would only require a few minor changes to your existing code.
Step 1. Create an ordinal scale for the colors
Instead of having colors be simply a list of color names, hard-coded to specific names, use d3.scale.ordinal(), and set the .range() to be an array of the colors you want to use. For example:
var colors = d3.scale.ordinal()
.range(["#5687d1","#7b615c","#de783b","#6ab975","#a173d1","#bbbbbb"]);
This would create an ordinal scale that uses the same colors as the original visualization. Since your data would require more colors, you would want to add a few more to your range, otherwise colors will be repeated.
As a shortcut, you can use d3.scale.category20() to let d3 choose a range 20 categorical colors for you.
Now when setting the fill colors for your path element arcs and also your breadcrumbs, you would simply use colors(d.name) instead of colors[d.name].
Step 2. Use your data to construct the domain of the scale
The .domain() of this scale will be set once we have the data, since it will depend on a list of the unique names contained in the data. To do this, we can loop through the data, and create an array of the unique names. There are probably several ways to do this, but here's one that works well:
var uniqueNames = (function(a) {
var output = [];
a.forEach(function(d) {
if (output.indexOf(d.name) === -1) {
output.push(d.name);
}
});
return output;
})(nodes);
This creates an empty array, then loops through each element of the nodes array and if the node's name doesn't already exist in the new array, it is added.
Then you can simply set the new array to be the domain of the color scale:
colors.domain(uniqueNames);
Step 3. Use the scale's domain to build the legend
Since the legend is going to depend on the domain, make sure the drawLegend() function is called after the domain is set.
You can find the number of elements in the domain (for setting the height of the legend) by calling colors.domain().length. Then for the legend's .data(), you can use the domain itself. Finally, to set the fill color for the legend boxes, you call the color scale on d since each element in the domain is a name. Here's what those three changes to the legend look like in practice:
var legend = d3.select("#legend").append("svg:svg")
.attr("width", li.w)
.attr("height", colors.domain().length * (li.h + li.s));
var g = legend.selectAll("g")
.data(colors.domain())
.enter().append("svg:g")
.attr("transform", function(d, i) {
return "translate(0," + i * (li.h + li.s) + ")";
});
g.append("svg:rect")
.attr("rx", li.r)
.attr("ry", li.r)
.attr("width", li.w)
.attr("height", li.h)
.style("fill", function(d) { return colors(d); });
And that's about it. Hope that helps.
Here's the updated JSFiddle.
I have implemented a realtime graph with javascript and d3.js. The data is generated randomly and it changes based on the random number. I want to fill the area under the line chart but I do not know how to fill it since the data is moving! The following code are correct for static charts but how I can use it for dynamic moving data
//Css part
.area {
fill: lightsteelblue;
stroke-width: 0;
}
//script
var area = d3.svg.area()
.x(function(d, i) { return x(i); })
.y0(height)
.y1(function(d, i) { return y(d); });
svg.append("path")
.datum(data)
.attr("class", "area")
.attr("d", area);
And that is how my data is generated:
var n = 100,
random = d3.random.normal(0, 50),
data = d3.range(n).map(random);
Thanks,
In order to move the area in real time, you will have to do quite a bit of work. Fortunately Mike Bostock wrote a very good tutorial for path transitions with d3.js.
The key code is:
// push a new data point onto the back
data.push(random());
// redraw the line, and then slide it to the left
path
.attr("d", area)
.attr("transform", null)
.transition()
.ease("linear")
.attr("transform", "translate(" + x(-1) + ")");
// pop the old data point off the front
data.shift();
Also note that you will certainly have to use selections at one point, to do so you can have a look at the following tutorial: A Bar Chart, Part 2.
Add to that the example of area chart that you already use and you are nearly done! The only difference is that you write
Now, you can also get inspiration from the following question: Smooth update to x axis in a D3 line chart?
Finally, here is a jsFiddle that provides you a working example of what you are looking for.