d3 path: Why are square brackets used in .data([...]) - d3.js

In d3 creating a path involves a sequence of method calls like this:
var path = svg.append("path")
.data([points])
.attr("d", d3.svg.line()
.tension(0) // Catmull–Rom
.interpolate("cardinal-closed"));
This above is excerpted from the following:
https://bl.ocks.org/mbostock/1705868
Given that points is itself an array, why is it necessary to enclose points inside square brackets in the method invocation .data([points])?
I believe this inner square brackets requirement obtains in d3 v3 and v4.

#davenewton's comment is close. The simply answer is that d3.line expects an array. By passing an array of arrays to .data d3's data-binding will call .attr with the first array in that array of arrays.
Examine this code:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script>
d3.select('body')
.append('div')
.data([1,2,3])
.attr('d', function(d, i){
console.log(d, i);
});
</script>
</body>
</html>
This is a strange use of the data-binding since it doesn't fulfill the enter, update, exit we usually use with data-binding (and that's why only the first iteration of binding get's called).
Finally, it should be noted that this code could be written as:
var path = svg.append("path")
.datum(points)
.attr("d", d3.svg.line()
.tension(0) // Catmull–Rom
.interpolate("cardinal-closed"));
Or even:
var path = svg.append("path")
.attr("d", d3.svg.line()
.tension(0) // Catmull–Rom
.interpolate("cardinal-closed")(points));
Since:
.append("path")
.data([array])
.attr("d", line)
and
.append("path")
.datum(array)
.attr("d", line)
and
.append("path")
.attr("d", line(points))
are all equivalent.

Related

Can I project one state from "d3js.org/us-10m.v1.json" and not the whole nation?

From this code at (https://bl.ocks.org/mbostock/4090848) I can access us.objects.states, .nation, or .counties but I just want to know if I can say: us.objects.states.geometries.id(Missouri FIPS code is 29)
I just want to get the state of Missouri or is there a way to narrow this down.
Or if you can give help or direction on how to manipulate this line:
.data(topojson.feature(us, us.objects.states).features)
I've read the API reference on the topojson.feature function but I am still confused on how to use it.
<svg width="960" height="600"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<script>
var svg = d3.select("svg");
var path = d3.geoPath();
d3.json("https://d3js.org/us-10m.v1.json", function(error, us) {
if (error) throw error;
svg.append("g")
.attr("class", "states")
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path);
svg.append("path")
.attr("class", "state-borders")
.attr("d", path(topojson.mesh(us, us.objects.states, function(a, b) { return a !== b; })));
});
</script>
The simplest change would be to filter for your chosen state(s):
.data(topojson.feature(us, us.objects.states).features.filter(function(d) {
return d.id == 29;
}))
We still return an array to .data() as required, and we draw the map, just with only the chosen state. (I've removed the mesh for borders below, the state appears just out of view, you'll need to scroll):
var svg = d3.select("svg");
var path = d3.geoPath();
d3.json("https://d3js.org/us-10m.v1.json", function(error, us) {
if (error) throw error;
svg.append("g")
.attr("class", "states")
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features.filter(function(d) {
return d.id == 29;
}))
.enter().append("path")
.attr("d", path);
});
<svg width="960" height="600"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
Which brings us to the next question, how to position the state. There are plenty of options and answers on SO on how to do so, but I'll note that this file is already projected and its coordinate system is in pixels, the file is designed for a 960x600 pixel map. This is why you haven't had to use a projection so far, there is no need for a transformation or projection. But unless we are happy with the empty space where all the other states would normally be drawn, we need to fix this.
The links in the text above go into more detail, but since we're here I'll just quickly demonstrate how to apply an identity projection and use the fitSize method to showcase Missouri (I just use selection.append() and .datum() as I'm just adding one path):
var svg = d3.select("svg");
var path = d3.geoPath();
d3.json("https://d3js.org/us-10m.v1.json", function(error, us) {
if (error) throw error;
var feature = topojson.feature(us, us.objects.states)
.features
.filter(function(d) { return d.id == 29; })[0]; // get a geojson object (not an array of objects)
var projection = d3.geoIdentity()
.fitSize([960,600],feature); // scale and translate the map so the feature is centered
path.projection(projection);
svg.append("path")
.datum(feature)
.attr("d", path); // draw the path.
});
<svg width="960" height="600"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>

Multiple Lines from Multiple Data D3js

I would expect the following code to plot two different lines on the same svg but it only returns two empty path>
<body>
<svg width="960" height="500"></svg>
<script>
data = [
{ Name: "line1", color: "blue", Points: [{x:0, y:5 }, {x:25, y:7 }, {x:50, y:13}] },
{ Name: "line2", color: "green", Points: [{x:0, y:10}, {x:25, y:30}, {x:50, y:60}] }
];
var line = d3.line()
.x(function(d, i) {return d.x})
.y(function(d, i) {return d.y})
var lines = d3.select("svg")
.selectAll("path")
.data(data)
.enter()
.append("path")
.attr("d", line);
</script>
</body>
I cannot find multiple line charts example that have a data structure similar to mine.
The issue is that you are passing the whole object from your array of data to the line() function, which is expecting an array of points. One alternative is to change the calling function to pass in only the Points array, something like this (untested):
.enter()
.append("path")
.attr("d", function(d) { return line(d.Points); })
In fact you need to access the Points field of each element of the data array:
Within:
var lines = d3.select("svg")
.selectAll("path")
.data(data)
.enter()
.append("path")
.attr("d", line);
replace
.data(data)
by
.data(data.map( function(d) { return d.Points }))

In D3.js I am confused by behavior of exit()

Today I am learning about D3.js
I started by studying this content:
https://bost.ocks.org/mike/circles/
Most of it seems easy to understand and follow.
But I have a problem getting exit() to work.
I do understand the idea that I can use exit() to force elements in my DOM to synch with values in a JS array.
So I wrote some code to demonstrate this idea and my code fails.
I want to know how I can rewrite my JS so that exit() will force elements in my DOM to synch with values in a JS array.
<html>
<body>
Ref:
<a href='https://bost.ocks.org/mike/circles/' target='x'>
https://bost.ocks.org/mike/circles/
</a>
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script>
// I should create 3 circles
var svg1 = d3.select('body')
.append('svg')
.attr('id','svg1')
.attr('width',800).selectAll("circle")
.data([0, 1, 2])
.enter()
.append("circle")
.attr("cy", 60)
.attr("cx", function(d, i) { return i * 100 + 30 })
.attr("r", function(d) { return 5+5*d })
// So far so good. I see 3 circles
// Now I should remove some.
var mycircles_a = svg1.selectAll("circle")
.data([99, 88])
// I should ask D3 to make the data-array sync with the circle-elements:
mycircles_a.exit().remove()
// Above call fails for some reason; I still see 3 circles!
// I should see 2 circles because mycircles_a is bound to an array with only 2 values.
'bye'
</script>
</body>
</html>
In your example svg1 is, itself, an "enter" selection.
Your code works just fine if you break the chain, making svg1 just a selection that creates the SVG:
var svg1 = d3.select('body')
.append('svg')
.attr('id','svg1')
.attr('width',800);
svg1.selectAll("circle")
.data([0, 1, 2])
.enter()
.append("circle")
.attr("cy", 60)
.attr("cx", function(d, i) { return i * 100 + 30 })
.attr("r", function(d) { return 5+5*d })
var mycircles_a = svg1.selectAll("circle")
.data([99, 88])
mycircles_a.exit().remove()
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
You're not saving a reference to svg1 correctly.
https://jsfiddle.net/y008c61L/
var svg1 = d3.select('body')
.append('svg')
.attr('id','svg1')
.attr('width',800);
svg1.selectAll("circle")
//...

What is the maximum value for the .duration() command in d3.js?

I am going through D3 Tips & Tricks and i'm on this graph: http://bl.ocks.org/d3noob/7030f35b72de721622b8.
I am playing around with the different axises to get them to re-render and re-size dynamically upon a JavaScript function has been called via a button. I want to re-render the x axis so that it takes longer to fully load than the re-generated line element.
// Select the section we want to apply our changes to
var svg = d3.select("body").transition().delay(500).style("stroke", "green");
// Make the changes
svg.select(".line") // change the line
.duration(750)
.style("stroke", "red")
.attr("d", valueline(data));
svg.select(".x.axis") // change the x axis
.duration(1750)
.call(xAxis);
svg.select(".y.axis") // change the y axis
.duration(1000000000)
.call(yAxis);
});
In theory, I suppose that .duration() command can take the highest integer value that JavaScript accepts as a .millisecond. Is that correct? I am keen to know if there is a limit here as to the longest possible duration I can make.
I just coded up a quick example with Number.MAX_VALUE and d3 doesn't complain:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script>
var svg = d3.select('body')
.append('svg')
.attr('width', 500)
.attr('height', 500);
var c = svg.append('circle')
.attr('transform', 'translate(20,20)')
.attr('r', 20)
.style('fill', 'steelblue');
c.transition()
.duration(Number.MAX_VALUE)
.ease(d3.easeLinear)
.tween("attr.transform", function() {
var self = d3.select(this),
i = d3.interpolateTransformSvg('translate(20,20)', 'translate(400,400)');
return function(t) {
console.log(i(t));
self.attr('transform',i(t));
};
});
</script>
</body>
</html>
Some quick calculations tell me though that it'll take 1x10306 iterations of the transition loop to move my circle 1px, so assuming the transition loop is firing every 17 milliseconds, that's around 5.34 × 10296 years before I see any movement....

d3.data skipping the first row of data

I have the following code that works great, except when I iterate through my data set, the first row (the 0 index) is getting skipped.
svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x",function(d){
console.log(data);
console.log(d);
return xScale(d.year-1980);
})
Note the console.log(data) returns my full data set, including the first row so the data is there!
But console.log(d) shows all rows after and including my second row of data - it drops the first row.
Any suggestions are welcome.
I had the same issue with similar code, and fixed it based on Lars Kothoff's comment.
In my case it made sense to change the selectAll to work on a g element, more like so:
svg.selectAll("g")
.data(data);
.enter()
.append("g")
.append("rect")
.attr("x",function(d) { return xScale(d.year-1980); });
You could also differentiate the rects with a class:
svg.selectAll("rect.year")
.data(data)
.enter()
.append("rect")
.attr("x",function(d){ return xScale(d.year-1980); })
.classed("year");
Yeah, it seems if there is already an element, it gets "skipped" by the .enter()
<html>
<head>
<title>D3 Test</title>
</head>
<body>
</body>
<script type='text/javascript' src='https://d3js.org/d3.v4.min.js'></script>
<script type='text/javascript'>
var theData = ["James", "Sue", "Ben"]
var p = d3.select("body").selectAll("p")
.data(theData)
.enter()
.append('p')
.text(function (d,i) {
return " i =" + i + "data =" + d;
});
</script>
</html>
Produces
i =0data =James
i =1data =Sue
i =2data =Ben
But if you add a p element in there, it'll skip it.
...[previous code]
<body>
<p>Here is the first p tag that "James gets matched too"</p>
</body>
...[previous code]

Resources