d3 line chart labels overlap - d3.js

I've created a line chart based on the example found here:
http://bl.ocks.org/mbostock/3884955
However, with my data the line labels (cities) end up overlapping because the final values on the y-axis for different lines are frequently close together. I know that I need to compare the last value for each line and move the label up or down when the values differ by 12 units or less. My thought is to look at the text labels that are written by this bit of code
city.append("text")
.datum(function(d) { return {name: d.name, value: d.values[d.values.length - 1]}; })
.attr("transform", function(d) { return "translate(" + x(d.value.date) + "," + y(d.value.temperature) + ")"; })
.attr("x", 3)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
If the y(d.value.temperature) values differ by 12 or less, move the values apart until they have at least 12 units between them. Any thoughts on how to get this done? This is my first d3 project and the syntax is still giving me fits!

You're probably better off passing in all the labels at once -- this is also more in line with the general d3 idea. You could then have code something like this:
svg.selectAll("text.label").data(data)
.enter()
.append("text")
.attr("transform", function(d, i) {
var currenty = y(d.value.temperature);
if(i > 0) {
var previousy = y(data[i-1].value.temperature),
if(currenty - previousy < 12) { currenty = previousy + 12; }
}
return "translate(" + x(d.value.date) + "," + currenty + ")";
})
.attr("x", 3)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
This does not account for the fact that the previous label may have been moved. You could get the position of the previous label explicitly and move the current one depending on that. The code would be almost the same except that you would need to save a reference to the current element (this) such that it can be accessed later.
All of this will not prevent the labels from being potentially quite far apart from the lines they are labelling in the end. If you need to move every label, the last one will be pretty far away. A better course of action may be to create a legend separately where you can space labels and lines as necessary.

Consider using a D3 force layout to place the labels. See an example here: https://bl.ocks.org/wdickerson/bd654e61f536dcef3736f41e0ad87786
Assuming you have a data array containing objects with a value property, and a scale y:
// Create some nodes
const labels = data.map(d => {
return {
fx: 0,
targetY: y(d.value)
};
});
// Set up the force simulation
const force = d3.forceSimulation()
.nodes(labels)
.force('collide', d3.forceCollide(10))
.force('y', d3.forceY(d => d.targetY).strength(1))
.stop();
// Execute thte simulation
for (let i = 0; i < 300; i++) force.tick();
// Assign values to the appropriate marker
labels.sort((a, b) => a.y - b.y);
data.sort((a, b) => b.value - a.value);
data.forEach((d, i) => d.y = labels[i].y);
Now your data array will have a y property representing its optimal position.
Example uses D3 4.0, read more here: https://github.com/d3/d3-force

Related

How to move a point from d3 arc start to end without wrapping?

I am attempting to use d3.js to move a point along an arc from 0 to PI, say, without the point moving back along the innerRadius as seen here http://bl.ocks.org/mbostock/1705868.
I removed innerRadius hoping (unsuccessfully) that would work (http://jsfiddle.net/klin/23c5476v/). I had also tried setting the innerRadius with the same value as outerRadius.
Fragment I changed (changes marked with //) ...
var path = svg.append("svg:path")
.datum({endAngle: Math.PI}) //
.attr("d", d3.svg.arc()
// .innerRadius(h / 4) // Hoping removal would prevent inner transition
.outerRadius(h / 3)
.startAngle(0)
);//.endAngle(Math.PI));
Entire code ...
var svg = d3.select("body").append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(" + w / 2 + "," + h / 2 + ")");
var path = svg.append("svg:path")
.datum({endAngle: Math.PI}) //
.attr("d", d3.svg.arc()
// .innerRadius(h / 4) // Hoping removal would prevent inner transition
.outerRadius(h / 3)
.startAngle(0)
);//.endAngle(Math.PI));
var circle = svg.append("svg:circle")
.attr("r", 6.5)
.attr("transform", "translate(0," + -h / 3 + ")");
function transition() {
circle.transition()
.duration(5000)
.attrTween("transform", translateAlong(path.node()))
.each("end", transition);
}
transition();
// Returns an attrTween for translating along the specified path element.
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}
The problem I think is that the arc shape has area, so the path must be closed, while the line shape does not. Eventually I'd like to be able to separately animate object movement along a series of consecutive arcs similar to the answer to Interpolating along consecutive paths with D3.js, but first I need to avoid the loop back movement.
Is a simple solution maybe to not use d3's arc generator, but instead use another where the end point actually is the terminus of the path?
Paul is right.
You can do next
var arc = d3.svg.arc(); //plus params
$path.attr('d',function(){
var d = arc();
return d.split('L')[0]; //will return half of arc without lines
});

D3: How do I chain transitions for nested data?

I'm attempting to make a visualization with a multistage animation. Here's a contrived fiddle illustrating my problem (code below).
In this visualization the boxes in each row should turn green when the entire group has finished moving to the right column. IOW, when the first row (containing 3 boxes) is entirely in the right column, all the boxes should turn from black to green, but the second row, having only partially moved to the right column at this point, would remain black until it, too, is completely in the right column.
I'm having a hard time designing this transition.
Basic chaining without a delay immediately turns each box green once its finished moving (this is how it's working currently). Not good enough.
On the other hand creating a delay for the chain is difficult, since the effective delay per group is based on the number of boxes it has and I don't think this count is available to me.
It's like I need the transition to happen at mixed levels of granularity.
How should I go about doing this?
The fiddle (code below)
var data = [
["x", "y", "z"],
["a", "b", "c", "d", "e"]
];
var svg = d3.select("svg");
var group = svg.selectAll("g").data(data)
.enter()
.append("g")
.attr("transform", function(d, i) {
return "translate(0, " + (40 * i) + ")";
});
var box = group.selectAll("rect")
.data(function(d) { return d; });
box.enter()
.append("rect")
.attr("width", 30)
.attr("height", 30)
.attr("x", function(d, i) { return 60 + 30 * i; })
.transition()
.delay(function(d, i) { return 250 + 500 * i; })
.attr("x", function(d, i) { return 300 + 30 * i; })
.transition()
.attr("style", "fill:green");
// I probably need a delay here but it'd be based off the
// number of elements in the nested data and I don't know
// how to get that count
.attr("style", "fill:green");
I manage to get the effect you want, it's a little tricky though. You can customize the behavior of a transition at the begining and end of a transition. If you add a function to the end of the transition that detects if the transitioned element is the last in the group, you select all the rectangles in the group and apply the change to them.
box.enter()
.append("rect")
.attr("width", 30)
.attr("height", 30)
.attr("x", function(d, i) { return 60 + 30 * i; })
.transition()
.delay(function(d, i) { return 250 + 500 * i; })
.attr("x", function(d, i) { return 300 + 30 * i; })
.each('end', function(d, i) {
var g = d3.select(d3.select(this).node().parentNode),
n = g.selectAll('rect')[0].length;
if (i === n - 1) {
g.selectAll('rect').attr('fill', 'green');
}
});
More details in the transitions here, a working fiddle here.

Unique symbols for each data set in d3 Scatterplot

I am having trouble using d3's symbol mechanism to specify a unique symbol for each set of data. The data's like this:
[[{x: 1, y:1},{x: 2, y:2},{x: 3, y:3}], [{x: 1, y:1},{x: 2, y:4},{x: 3, y:9}], etc.]
The part of the code that writes out the symbols looks like this:
I create a series group for each vector of points. Then:
series.selectAll("g.points")
//this selects all <g> elements with class points (there aren't any yet)
.data(Object) //drill down into the nested Data
.enter()
.append("g") //create groups then move them to the data location
.attr("transform", function(d, i) {
return "translate(" + xScale(d.x) + "," + yScale(d.y) + ")";
})
.append("path")
.attr("d", function(d,i,j){
return (d3.svg.symbol().type(d3.svg.symbolTypes[j]));
}
);
Or at least that's how I'd like it to work. The trouble is that I can't return the function d3.svg.symbol() from the other function. If I try to just put the function in the "type" argument, then data is no longer scoped correctly to know what j is (the index of the series).
right, but I don't want a unique symbol for each datapoint, I want a unique symbol for each series. The data consists of multiple arrays (series), each of which can have an arbitrary number of points (x,y). I'd like a different symbol for each array, and that's what j should give me. I associate the data (in the example, two arrays shown, so i is 0 then 1 for that) with the series selection. Then I associate the data Object with the points selection, so i becomes the index for the points in each array, and j becomes the index of the original arrays/series of data. I actually copied this syntax from somewhere else, and it works ok for other instances (coloring series of bars in a grouped bar chart for example), but I couldn't tell you exactly why it works...
Any guidance would be appreciated.
Thanks!
What is the question exactly? The code that you give answers your question. My bad, j does return a reference to the series. Simpler example.
var data = [
{id: 1, pts: [{x:50, y:10},{x:50, y:30},{x:50, y:20},{x:50, y:30},{x:50, y:40}]},
{id: 2, pts: [{x:10, y:10},{x:10, y:30},{x:40, y:20},{x:30, y:30},{x:10, y:30}]}
];
var vis = d3.select("svg");
var series = vis.selectAll("g.series")
.data(data, function(d, i) { return d.id; })
.enter()
.append("svg:g")
.classed("series", true);
series.selectAll("g.point")
.data(function(d, i) { return d.pts })
.enter()
.append("svg:path")
.attr("transform", function(d, i) { return "translate(" + d.x + "," + d.y + ")"; })
.attr("d", function(d,i, j) { return d3.svg.symbol().type(d3.svg.symbolTypes[j])(); })
The only difference is that I added parenthesis after d3.svg.symbol().type(currentType)() to return the value rather than the function. D3js uses chaining, jquery style. This let you use symbol().type('circle') to set a value and symbol().type() to get it. Whenever accessors are used, what is returned is a reference to a function that has methods and attributes. Keep in mind that, in Javascript functions are first class objects - What is meant by 'first class object'?. In libraries that use that approach, often, there is an obvious getter for retrieving meaningful data. With symbol, you have to use symbol()().
The code beyond the symbol functionality can be seen at: https://github.com/mbostock/d3/blob/master/src/svg/symbol.js
d3.svg.symbol = function() {
var type = d3_svg_symbolType,
size = d3_svg_symbolSize;
function symbol(d, i) {
return (d3_svg_symbols.get(type.call(this, d, i))
|| d3_svg_symbolCircle)
(size.call(this, d, i));
}
...
symbol.type = function(x) {
if (!arguments.length) return type;
type = d3_functor(x);
return symbol;
};
return symbol;
};
Just in case you haven't. Have you tried?
.append("svg:path")
.attr("d", d3.svg.symbol())
as per https://github.com/mbostock/d3/wiki/SVG-Shapes.

Changing number displayed as svg text gradually, with D3 transition

I am looking for a simple way to gradually change the value of a number displayed as svg text with d3.
var quality = [0.06, 14];
// qSVG is just the main svg element
qSVG.selectAll(".txt")
.data(quality)
.enter()
.append("text")
.attr("class", "txt")
.text(0)
.transition()
.duration(1750)
.text(function(d){
return d;
});
Since text in this case is a number i hope there is an easy way to just increment it to the end of the transition.
Maybe someone of you has an idea.
Cheers
It seems d3JS already provides a suitable function called "tween"
Here is the important part of the code example.
.tween("text", function(d) {
var i = d3.interpolate(this.textContent, d),
prec = (d + "").split("."),
round = (prec.length > 1) ? Math.pow(10, prec[1].length) : 1;
return function(t) {
this.textContent = Math.round(i(t) * round) / round;
};
});​
http://jsfiddle.net/c5YVX/280/
You can increment them over a given time interval from any start to any end value regardless their number precision.
Its implemented for SVG text but of course works the same for standard html text.
If you only need the plain tween function for rounded numbers, it gets a bit more leightweight.
.tween("text", function(d) {
var i = d3.interpolate(this.textContent, d),
return function(t) {
this.textContent = Math.round(i(t));
};
});​

What is the proper way to both rotate and translate text in d3?

I have an array with two strings and I want them to align with two circles (see example: http://bl.ocks.org/3028447)
I'm currently doing this:
.attr("transform", function(d, i) { return "translate(" + x(i)+",0) rotate(-45," + x(1)+"," + 0+") "; })
I was sure there was a simpler way to do it, something like this:
.attr("transform", function(d, i) { return "translate(" + x(i)+",0) rotate(-45) "; })
but when I use that I get this (http://bl.ocks.org/3028512), and I don't understand why.
You've combined your transform with x and y attributes:
.attr("y", 0)
.attr("x", 60)
These get applied before the transform (i.e., before the rotation), hence the text is not in the same position as the circles. Sometimes this technique is useful; the x moves the text parallel to the text’s baseline. So, if you wanted to position the text slightly outside the circle, you could change the x value to 6 rather than 60.

Resources