Zoomable sunburst chart in percentages - javascript

I'm new to D3 and I hope you can help out. I'm working on a sunburst chart divided into inner and outer layers, where inner layer represents a group and outer layers represent subgroups. Here's a working example for reference: jsfiddle.net/9gpL308y/1/ (and here's the original fiddle I used as a starting point: jsfiddle.net/j9WnB/64/)
Currently, each inner layer adds up to a number based on its categories' values in the outer layer and categories behave the same with their subcategories. What I need is for each layer to display as percentage (out of 100 that all groups on the same layer should add up to) and scale to appropriate size. Take this image for example:
image of the wanted chart
Working example would be extremely helpful. I found some topics on this problem but I couldn't get it to work with provided advice. To be honest I still don't understand fully what's going on in here (I was never good at geometry).
tl;dr: How to make sunburst chart from the fiddle above display data as percentage and have arcs scale appropriately?
Thanks.

There is a little to do with geometry in this case (except positioning your label, but this is what arc.centroid provides). To get your percentages you just need to divide an extent of the child node by the extent of its parent.
var center = arc.centroid(d);
g.append("text")
.attr("text-anchor", "middle")
.attr("transform", "translate(" + center + ")")
.text(function(_) {
if (d.parent == null)
{
return;
}
var percentage = 100 * d.dx / d.parent.dx; // that's it!
return d3.format(",.2f")(percentage) + "%";
})
Working example here: http://jsfiddle.net/9gpL308y/2/
For educational purposes you can dump all the nodes of the partition layout and then find how do they match visual picture. You can also read about those parent, dx, etc. at the documentation on partition layout: https://github.com/d3/d3-3.x-api-reference/blob/master/Partition-Layout.md

Related

Cannot get labels to work with D3-tile projection

I am trying to add some simple country labels to a D3 vector map which is layered on top of a D3-tile raster map. The labels are being created in as expected, but I am not able to project them properly on the map. The projection in D3-tile is a bit messed up (by which I mean it doesn't work like on a 'normal' vector map, and I don't understand it).
I have created a jsfiddle where I create the maps and then try to project them so that they move around with user interaction.
Bit of code that fails to achieve this is here:
d3.selectAll(".country_labels")
.attr("transform", function(d) {return "translate(" + path.centroid(d) + ")"})
UPDATE
I suspect my issue on this question is similar to the one I raised earlier today on here. I also note that a similar-ish question was raised here too.
I have made some progress and put together this new fiddle. The labels are now all on the map, but floating around the gulf of guinea, close to geocoordinates [0,0]. To me, this means they may have been projected properly but that the zoom has not functioned as expected. The issue here is that there are three separate types of coordinates in this script:
Geocoordinates - these are the starting point and always fixed
The 'd3-tile' coordinates. The ones that fit within a single pixel, and therefore always very close to zero
Pixel coordinates - these correspond to the actual coordinates on the screen
This is similar to your other question, just it is on the forward projection & zoom rather than the inverts. (I started writing this before the update, but had to run, I'll continue with your original code).
As with the paths, you append your labels as expected:
country_labels.selectAll("text")
.data(collection.features)
.enter().append("text")
.attr("x", function(d){return path.centroid(d)[0];})
.attr("y", function(d){return path.centroid(d)[1];})
.attr("dx", -40)
.text(function(d){ return d.properties.name })
.style("fill", "#aeaeaf")
.style("font-size", "15px")
There is one concern here, as the projection of most d3-tile examples, including yours, use a d3-projection scale of 1/tau, the world is projected within the space of 1 pixel, so the dx value is equal to 40 worlds, this won't work when applying the zoom, so let's drop that part
Now you are appending the features more or less just like the paths, but the issue is in the zoom handling:
d3.selectAll(".country_labels")
.attr("transform", function(d) {return "translate(" + path.centroid(d) + ")"})
The paths are given a similar treatment:
vector
.attr("transform", "translate(" + [transform.x, transform.y] + ")scale(" + transform.k + ")")
.style("stroke-width", 1 / transform.k);
But there are a couple differences here:
you are applying a different transform (scale and translate) to the paths as compared to the text: for the text there is no reference to the current zoom transform, instead, you only use the projection, which is anchored at 0,0 with all features lying within an area of one pixel (and anchored at 0,0 will have its baseline at y=0, the text will be largely out of view). If you inspect the svg, you'll see the text, just in the wrong spot.
The paths have a reduced stroke width as one zooms in (as we are zooming the svg, the stroke width itself increases), the same would apply for text, so even if the text was correctly positioned, it would be very very large (more than most any screen holding the browser).
One way we can address this is we apply the zoom transform on the x/y coordinates of the text, not the element itself (which would scale the text size as well, this way we don't need to resize the text at all):
country_labels.selectAll("text")
.attr("x", function(d){return transform.apply(path.centroid(d))[0];})
.attr("y", function(d){return transform.apply(path.centroid(d))1;})
Like with the inversion from svg pixel to lat/long, we go through the same motions, but in reverse order: apply the projection, then apply the zoom.
Here's an updated fiddle.
However, I have bad news - the labels are positioned exactly where you are telling them to be positioned now. But they aren't where you want them to be (how's the saying go, the best thing about programming is that the code does exactly what you tell it, the worst thing about programming is that the code does exactly what you tell it?).
You are using path centroids to place labels, this works sometimes for some features, but it doesn't work all the time. Take the United States for example, the centroid of the US using a Mercator projection isn't in the United States because it is between Alaska and the lower 48 states (sorry Hawaii, you don't have much pull here). The centroid of Canada is partly in the Arctic Ocean, and in many datasets (not this one surprisingly), France is labelled in the middle of the Atlantic because of French Guiana, when using centroids as the text anchor.
You can improve the visual appearance slightly by using .style("text-anchor","middle"), which at least centers labels where the are (very useful for smaller or equitorial countries), but ultimately centroid placement isn't ideal.
I'll just finish with: Annotations are the bane of cartography.
But, there is hope, here's one of the more promising futures I've seen.

d3js Star Chart - responsive legend

I am working on a d3.js chart - This current model creates a legend segment as a g element. I'd like to ensure the legend stacks below if there is not enough room in the container.
//desktop - ample space
//mobile - not enough space
I've cleaned up the legend part -- you able to clean up the code base and add some more comments here. One of the old features I had - is if there wasn't enough room in the chart the legend stacks underneath - like responsive design - I used to create 2 svg parts but apparently with d3 it should only be 1 svg - http://jsfiddle.net/h066yn5u/13/
see if the chart can be more dynamic with different sizes - I think I had to add a padding of 10 to the radius to give it a little gap between the edges.. maybe though its a case of adding a transform on the svg itself to add that padding
var arcchart = chart.append("g")
.attr("class", "starchart")
.attr("transform", "translate("+(r+10)+"," + h / 2 + ")");
var legend = chart.append("g")
.attr("class", "legend")
.attr("transform", "translate(" + ((r + 10) * 2) + "," + (h / 4) + ")");
a version where it splits the chart into two svgs
http://jsfiddle.net/h066yn5u/14/
There are multiple ways to solve this issue.
Split into 2 svg containers: d3.js is not bound to just one svg container. You can split up the legend and the chart into 2 seperate svg containers and let the HTML handle the flow of the page
Use foreignObject: If you don't want to do that. You can try to use tag. Remember, that this is not supported by ie11 (and edge either afaik)
Calculate everything by hand: calculate the width of your legend (and including text), the width of the chart and get the available width for your whole container. If the whole container width is too small, push the legend below and of course adjust the svg height and width accordingly.

Trying to analyse why this transition is not happening

I have the following adapted d3.js visual and I'm unable to work out why the transition does not fire. The ars should rotate around to different sizes when the radio button is clicked.
It seems that clicking the radio button is changing the titles of the arc (if I hover the cursor over each)
Is this section of code to blame?
// check if svg group already exists
var svg = d3.select("#sunGroup");
if (svg.empty()) {
var svg = d3.select("#sunBurst")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr({
'transform': "translate(" + width / 2 + "," + height * .52 + ")",
id: "sunGroup"
});
}
Here is the full (not) working example:
https://plnkr.co/edit/hpvREU?p=preview
This is the transition I'm trying to hold onto: plnkr.co/edit/NnQUAp?p=preview
What I trying to do is move the logic at line 128 (starting d3.selectAll("input").on("change", function change() {...) out of this function
An easy fix to your problem is to remove all children of the SVG whenever you switch data types:
d3.select("#sunBurst").selectAll('*').remove()
That way, you are binding the new data to new elements. You can also remove the svg.empty() and d3.select('#sunGroup') code. This simple fix lets you switch between pie charts, and is in the spirit of the code you currently have. Here's the users pie chart.
However, this may not be the best way to do what you're trying to achieve. As a reference, see Mike Bostock's General Update Pattern series (link is to first in the series) for how to update your SVG.

d3.js size node based on label text

Im have a force directed graph with a bunch of various nodes. Each node has either a single word, or double word label.
I have the svg circle and text label inside a group element. I plan on styling the label so it overlaps the node. My issue is that the majority of the time, the text will overflow the node.
Is there anyway to change the radius of the node based on its label size?
Let's say that nodes contains your g groups, that you've already bound data to them, and that the label text string is in a property name. You probably used this string when you told d3 to add the text elements to the g groups. You can use the same bound data to configure the circles when you add them. Something like this should work:
nodes.append("circle")
.attr("r", function(d) {return d.name.length * 2.5;})
... more stuff here.
The second line is the key. I'm just setting the circle radius based on the length of the label text. I used 2.5 as the multiplier based on trial and error with the default san-serif in 10pt type.
In theory, it would be nice to have some systematic method for determining how much each character takes up, on average, and use that to determine the multiplier in the second line. (Even with fixed-width fonts, there's a lot of variation in how much space is used for different fonts with the same point size.) If it were me, that would be more work than it was worth. I would probably just set a variable containing the multiplier near the top of the script and try to remember to change it when I changed fonts.
EDIT: It might be possible to use one of the functions getBBox() or getBoundingClientRect() on the text object (probably referencing it as this) to figure out the size of the text.
Try using getComputedTextLength().
From Mike Bostock:
http://bl.ocks.org/mbostock/1846692
node.append("text")
.text(function(d) { return d.name; })
.style("font-size", function(d) { return Math.min(2 * d.r, (2 * d.r - 8) / this.getComputedTextLength() * 24) + "px"; })
This allows you to fill up the circle with text without overflowing. I'm not totally sure where the numbers come from there—perhaps someone can better explain.
Alternatively, you could use getBBox() as in this example (and the other answer by Mars), though you'd need to also do some calculations for the circle. You can do that using .attr("text-anchor", "middle") and some geometry.
Hope this helps.

How do I override d3.js chart function behavior?

There is a popular library in d3.js that provides a bullet chart implementation. I am in the process of making a small amendment to the default behaviour of this chart.
A bullet chart consists of one or more markers, parallel lines to identify targets, and I wish to programatically make one of the markers slightly shorter in length than the other. I'm pretty close to the solution and I have shared my work at Bullet Fiddle. To see my issue you have to hit the Year and Quarter buttons and notice how that the line shrinks and grows and then shrinks again. I want it to remain at the shorter length.
It seems that the default behaviour of the bullet is trigged at each update which is causing it to reset the marker to it's original size. I would love some help to understand how I can override this default behaviour and keep the line at a custom size.
This is a sample of the code that I use to update the marker:
d3.selectAll(".bullet .marker.s1").attr("y1", 10).attr("y2", 35);
This is the code in the library that is causing your changes to be overwritten:
marker.transition()
.duration(duration)
.attr("x1", x1)
.attr("x2", x1)
.attr("y1", height / 6) // the y1 and y2 values get updated here
.attr("y2", height * 5 / 6);
So the easiest way would just be to fork the library and delete the couple of lines. Otherwise, I didn't see that the marker definitions get exposed as part of the chart for you to remove the transition.

Categories