How do I override d3.js chart function behavior? - javascript

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.

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.

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

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.

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.

D3.js - Data and Axis Not Synchronized During Pan

I created a scatter plot using D3.js that updates every 20 seconds. It also pans and zooms. The problem is the data lags behind the axis during the pan. I've looked for examples of a similar implementation but all I can find are ones that either do zoom/pan or do intervals, not both. I can't find the source of the problem. A simplified demo of my code can be found here: http://jsbin.com/yurik/1/edit. Any help is appreciated.
The synchronization issue comes from the fact that you are using a transition to move the circles and not using a transition to update the x-axis. Here's the relevant snippet from the draw function:
circles.transition()
.attr("cx", function(d) { return x(dateFn(d)) })
.attr("cy", function(d) { return yValueFn(d) });
svg.selectAll("g.x.axis").call(xAxis);
Because D3 has a default transition duration of 250 milliseconds, the circles are lagging behind the axis, which is updated instantly. You can synchronize the two by reducing the transition duration to 0 like this:
circles.transition().duration(0)
That should make the x-axis and circles move synchronously.

Categories