D3js transition between two path cause parsing error - javascript

I try to make a simple transition / interpolation between two path / shapes (designed in illustrator).
I have d3 in my project, so I use it; but I could try something else if I can figure out how to do.
I Define a few variables (including the two path) :
var width = 300,
height = 300;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var path = svg.append("path"),
d0 = "M12,2c5.514,0,10,4.486,10,10c0,5.514-4.486,10-10,10C6.486,22,2,17.514,2,12C2,6.486,6.486,2,12,2z M12,0 C5.372,0,0,5.372,0,12c0,6.629,5.372,12,12,12s12-5.371,12-12C24,5.372,18.628,0,12,0z",
d1 = "M12,19.938c5.514,0,10-13.452,10-7.938c0,5.514-4.486,10-10,10C6.486,22,2,17.514,2,12C2,6.486,6.486,19.938,12,19.938z M12,0C5.372,0,0,5.372,0,12c0,6.629,5.372,12,12,12s12-5.371,12-12C24,5.372,18.628,0,12,0z";
Then I start to loop on the transition :
loop();
function loop() {
path
.attr("d", d0)
.transition()
.duration(5000)
.attr("d", d1)
.transition()
.delay(5000)
.attr("d", d0)
.each("end", loop);
}
And the weird things happen !
I have a long list of errors in my console : Error: Problem parsing d="M12,10.9402992c5.514,0,10-4.4542992,101.0597007999999999c0,5.514-4.486,10-10,10C6.486,22,2,17.514,2,12C2,6.486,6.486,10.9402992,12,10.9402992z M12,0C5.372,0,0,5.372,0,12c0,6.629,5.372,12,12,12s12-5.371,12-12C24,5.372,18.628,0,12,0z"
Which is not a valid svg. As far as I understand what this is about, svg path with float numbers are non valid (but I do have floats in my base path and they work ; so I'm not so sure).
It appears that I should try to round all the numbers all along the transition.
But the fact I really don't understand is why it works on the last 75% of the loop and not on the first 25%.
Here is a fiddle to see the fail in action : http://jsfiddle.net/vQabH/
And why it works on this one : http://jsfiddle.net/9bC6M/
(which I made from this example : http://bl.ocks.org/mbostock/3081153 )

The problem is that in your paths, negative values are not separated by space or comma. For example, there is c5.514,0,10,4.486,10,10 (3 pairs of values, separated by comma), but c0,5.514-4.486,10-10,10 (again 3 pairs of values, but not all separated with commas). This messes up the D3 transition.
D3 simply doesn't know how to interpolate between strings like that. For example, you're getting a parse error for c5.514,0,10-4.4542992,101.0597007999999999 (2 pairs of values and a spurious number).
It works fine when you separate the negative values from the rest, see here. Technically, you could argue that it should still work as it's a valid SVG path, so you may want to open an issue about this on the D3 website.

Related

Can't reference data objects in d3js

I can't wrap my head around manipulating the data object in d3js. I'm planning to create a chart composed of horizontal bars to hold data elements. Each data element is a circle. I figured out how to insert circles into the different bars, but I'm stuck on how to equally space the circles in each bar. For example, if the width is 800 and there are 8 circles, the x attribute should be 100*i.
Here's a link to my project: https://plnkr.co/edit/fHrdJsItEqA5qc35iUxG?p=preview
I think the problem is how to reference the data object in this block of code. Anyways, I would like to equally space the circles using scaleBand which I defined as variable x earlier in my code:
var x = d3.scaleBand()
.range([0,width]);
I think the solution would look something like this: .attr("x",x.domain(data.map(function(d,i){return d[i]})); x.bandwidth(), but obviously data is not the right object.
Selecting each bar and inserting circles:
bar.selectAll("rect")
.data(function(d,i){console.log(data_group[i].values.length);return data_group[i].values})
.enter().append("circle")
.attr("class","circle")
.attr("width", width)
//.attr("x",) //how to equally space these circle elements???
.attr("height",20)
.attr("y", y.bandwidth())
console.log(y.bandwidth());
As always, I would really appreciate your help.
There are a number of issues with your code that are preventing it from working, including:
You aren't setting a domain for your x scale.
You are attempting to place <circle>s inside of <rect>s but you cannot nest shapes in SVGs. You should place both inside of a <g>.
A <circle>'s position is set using the cx and cy attributes (and you also need to provide it an r radius attribute).
To address your question, you will need to determine how you want your items laid out. Because you are referencing the index in your question, I will use that.
You are breaking your data into nested groups where each one has a values array. You are rendering a <circle> for each datum in that array, so you will want to determine the length of the longest values array.
var longest = data_group.reduce(function(acc, curr) {
return curr.values.length > acc ? curr.values.length : acc;
}, -Infinity);
Once you have the length of the longest values array, you can set the domain for your x scale.
You are using d3.scaleBand (d3.scalePoint would probably work better here), which is an ordinal scale. Ordinal scales work on discrete domains, which means that you will need to have a domain value for each possible input (the indices). For this, you will need to generate an array of the possible indices from 0 to longest-1.
var domainValues = d3.range(longest);
Now that you have the input domain values, you can set them for the x scale.
x.domain(domainValues);
Then, for each <circle>, you will set its cx value using the index of the circle in its group and the x scale.
.attr('cx', function(d,i) { return x(i); })
As I mentioned in the beginning, there are other errors in your code, so just fixing this won't get it running correctly, but it should push you in the right direction.

Jittering geo paths using D3.js

I'm trying to add 'jitter' or add random noise to a D3.js map that contains line features. Note, this is slightly different from this other example because it involves geo paths. Additionally, while I'd like to use a custom transformation to do this, I don't think I can because I need to be able to use a standard transformation (from WGS84 to NY State Plane). I think the jittering function should either be based on a modified path function, or be a separate function which takes a path as input.
var projection = d3.geo.conicConformal()
.parallels([40 + 40 / 60, 41 + 2 / 60])
.rotate([74, -40 - 10 / 60]);
var path = d3.geo.path()
.projection(projection);
Note that I don't really want to modify the input data at all (i.e., the jittering should be on the paths, not the input geodata). Note also that the jittering can be totally random (i.e., it does not have to be the same every time). My initial thought is to wrap the data in a jitter function, or to wrap the path function in a jitter function. Either way, I'm not really sure where to start on this? Any suggestions? Even a link to the relevant API item would be awesome!
svg.selectAll("path")
.data(jitter(lines.features)) // Wrap data in jitter function... or...
.enter().append("path")
.attr("class", "line")
.attr("d", function(d) { return jitter(path(d)); }) // Jitter path directly
A (simplified) jsfiddle is available here for reference.

d3 enter().append() not appending all elements of my array

I'm working on my first mid-scale d3 project right now after having run through the tutorials. I understand scales, enter, update, and exit, so I'm pretty confused about a problem I'm running into.
I have an array of JSON objects with two fields, year and number. I am creating a bar chart with this array with the following code:
var bar = chart.selectAll('g')
.data(yearData)
.enter().append('g')
.attr('transform', function(d, i) {
console.log(i);
return 'translate(' + i * barWidth + ',0)'; });
My confusion stems from the fact that the console.log statement in this code block outputs 27 as its first value. In other words, d3 is skipping elements 0 - 26 of my array. Why could this be??
Thanks for your help.
This is most likely because you already have g elements on your page (e.g. from adding an axis). These are selected and matched with data, so the enter selection doesn't contain everything you expect.
One solution is to assign a class to these elements and select accordingly:
var bar = chart.selectAll('g.bar')
.data(yearData)
.enter().append('g')
.attr("class", "bar")
// ...
Much more detail on this in the second half of this tutorial.

Lines not showing in d3.js multi-line-chart with time axis

Hello nice people of SO,
I'm pretty new to javascript, JSON and D3.js and I'm trying to build a multi-line time series chart. My data comes as JSON from my server like this:
{
"t": ["2014-08-16T16:15:00", "2014-08-16T16:20:00", "2014-08-16T16:25:00", ...],
"todd": [0,0,1,2,3,2,1,0,0, ...],
"pete": [3,2,1,0,4,4,0,0,0, ...]
}
This I store in an array called 'dataset'. The datetimes are subsequently parsed. 't', 'todd' and 'pete' arrays are of equal length.
Ultimately I want the time (t) values to be used for the x-axis, the other two as y values over time. Parsing the data works, however I can't get it to display properly. What works so far is:
A: display my values one after the other without a reference to time, so I get two nice, colored lines but the x-axis holds no information, as it just displays 0 to N.
OR
B: have the x-axis show as a timeline based on the "t"-values (this tells me that the parsing works as it should). However, I can't get my two lines to show.
In Variant A I was simply returning the index as x value like so:
var line = d3.svg.
.x(function(d,i) {
return i;
})
...
Now I'm trying to return the datetime from my "t"-array:
var line = d3.svg.
.x(function(d,i) {
return dataset.t[i];
})
...
However, nothing is displayed. I get no errors at all in the console. I tried console logging the value of 'dataset.t[i]' but nothing shows. It's as if the code isn't even running so here is the for loop that calls the above code:
for (var key in dataset) {
if (dataset.hasOwnProperty(key) && key!="t") {
data=dataset[key];
svg.append("svg:path").attr("d", line(data))
.style("stroke", color(key))
.style("stroke-width", 2);
//BELOW IS IRRELEVANT TO MY QUESTION, JUST FOR COMPLETENESS...
svg.append("rect")
.attr("x", w - 65)
.attr("y", n*25)
.attr("width", 10)
.attr("height", 10)
.style("fill", color(key));
svg.append("text")
.attr("x", w - 50)
.attr("y", n * 25 + 10)
.attr("height",30)
.attr("width",100)
.style("fill", color(key))
.text(key);
n+=1; // counter up
}
};
I can tell this is running as the 'rect' and 'text' elements are added to the chart.
The full code is here: http://pastebin.com/tbS7rD7p
By now I can't see straight anymore so I'm really looking forward to your ideas. Many thanks in advance!
Cheers, Ben
Thanks, in code veritas, for your input. I'm in the process of optimizing the code in the way you suggested. However, it seems you do not need the .data() method with lines as you allocate the data via .x() and .y() methods for these (if I understand correctly).
Anyway, in the end the solution was to change:
.x(function(d,i) {return dataset.t[i];})
to
.x(function(d,i) {return x(dataset.t[i]);})
which is mentioned in pretty much every tutorial but which I, wisely ^^, chose to ignore because it seemed pointless to me. If anybody can comment on that and enlighten me, I'd be grateful.
Cheers!

Improving D3 Sequence Sunburst Example

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.

Categories