d3.js size node based on label text - javascript

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.

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.

In D3, how does one place text inside shapes that already have text attached to them?

I am currently working on the following d3 example where I'd like to place text inside of the arcs, similar to this example. However, whenever I try to append text to anything, the text just doesn't display. I've looked at the developer console, and it appears to be there, but it won't visually display on the screen. I used all the code provided in the first example, except I tried to add the following the the arc elements:
("the d3 element").enter().append("svg:text").text("???")
("the d3 element").enter().append("text").text("???")
("the d3 element").append("svg:text").text("???")
("the d3 element").append("text").text("???")
Aside from cutting off some of the styling changes, it seems like no matter where I put any of this code, it just doesn't want to work for me. I would appreciate and help!
"whenever I try to append text to anything, the text just doesn't display": Text cannot be appended to most svg elements. You can append text to the svg itself or a g, but you cannot append it to a path, rect, circle, etc.
One of the most common methods of dealing with this is to use g elements to place a shape and text while binding data to the g. Using a transform on theg will translate shape and text - great for things like circles and rectangles.
There are several other approaches you can use to overlay text on svg elements:
Use the positioning attributes of an element to set the x and y attributes of text so that you can place text over top of an element.
Use a path as a text path to place text (as in your example)
Use utility methods such as centroid (for arcs or geopaths for example)
Find the bounding box of elements and place elements using this information.
These options help place, but won't make sure that the text falls within the bounds of a shape - that is different complication.
For arcs, one option is to use a circular path as a text-path where the circle has a radius between that of the inner and outer radius of your arc - then place the text using a text offset that reflects the start angle - or make an arc for each piece of text. The general mechanism is shown below (note it can't use a circle element as svg textPaths must follow paths):
var svg=d3.select("body")
.append("svg")
.attr("height",400)
.attr("width",400)
.attr("transform","translate(200,200)")
var arc = d3.arc()
.innerRadius(50)
.outerRadius(100)
.startAngle(0)
.endAngle(2);
var arcText = d3.arc()
.innerRadius(75)
.outerRadius(75)
.startAngle(0)
.endAngle(2);
var arc = svg.append("path")
.attr("d",arc)
.attr("fill","steelblue")
var textPath = svg.append("path")
.attr("d",arcText)
.attr("id","textpath")
.attr("fill","none")
.attr("stroke","black");
var text = svg.append("text")
.append("textPath")
.attr("xlink:href","#textpath")
.text("title")
.attr("startOffset", "25%") // the bottom of the arc is from 50%-100%, the top from 0 to 50%
.style("text-anchor","middle")
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
The above mechanism is very similar to the example you link to - it appends text using the arcs as text paths. The example you reference uses text paths of the visible arc(donut) segments themselves - and offsets the x,y positions to move the text into the arc itself (as opposed to on the arc).
Note that I've used v4, as opposed to v3 as in the linked example.

Zoomable sunburst chart in percentages

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

How can I make x divs rather than a single div of x width using d3?

I am attempting to present some data vaguely similarly to what is done in https://xkcd.com/980, though on a much smaller scale.
What I am attempting to do is separate each of the horizontal bars (the divs created in the enter() and append()) into a series of smaller blocks, according to the average of the total that they make up (d/d3.sum(data)*100).
I am using the d3 library, but I haven't seen an example that has attached code nor that does quite the same thing. My own attempts have either failed or are, to my knowledge of JavaScript and d3, not possible.
I've adapted a simple bar chart to be labeled by percentage rather than count, here.
d3.select(".chart")
.selectAll("div")
.data(data)
.enter().append("div")
//can't enter and manipulate via loop?
.style("width", function(d) { return x(d) + "px"; })
//or perhaps, using %age, use that to loop and make div?
.text(function(d) { return Math.round(d/d3.sum(data)*100); });
I've also tried entering the div once it is created after assigning text, using d3's selectAll("div") followed by using the number in the array (.data(data[d])) and then entering and appending, but have been met with no luck.
Within the creation of the bars, where would I put a line creating inner div tags and how can I make sure that the correct amount are nested inside?

Adding more circles to SVG degrades performance drastically

I have a application where I draw a world map with D3 and I use latitude and longitude data from a different source to plot them on the map. Currently what I learnt from lot of google'ing is that i can plot points by appending "circle" to to the SVG, which works fine for first 15 to 20 seconds after my web page is opened, later everything gets too slow and sloppy.
I am not sure how to keep the performance of the page decent and not add a new DOM element for every single circle I append with SVG. Do I need to use some other technology to achieve this ? Please advice.
My code looks like below and I call this like 500 times every 5 seconds.
function draw_point(lat, lon, keyword) {
var x = projection([lon, lat])[0];
var y = projection([lon, lat])[1];
svg.append("circle")
.attr("cx", x)
.attr("cy", y)
.attr("r", 0.5)
.style("fill", "gold");
svg.append("text")
.text(keyword)
.attr("x", x)
.attr("y", y)
.style("fill", "gold")
.style("font-size", "10px")
.transition()
.duration(40)
.style("opacity", 0)
.remove();
}
To give a bit more context, I am trying to do something like this site http://tweetping.net/ In this page I see that new DOM element is not being added for every dot placed in the map, I am looking for something similar.
The page which you mentioned uses canvas element and not svg or d3.js. You might want to look into
fabricjs
paperjs
kinectjs
Additional clarification of #VivekKumarBansal's suggestion: The general rule is that SVG slows down as more elements are added, but making images larger or smaller doesn't affect speed. Canvas doesn't slow down as more elements are added, but increasing size does slow it down. d3.js can be used with Canvas, although it seems to be more common to use it with SVG.

Categories