I was trying to plot a time-pressure line chart.
The data is an array of objects, named "res"
[
{Time: ,
Psi:
},
...
]
I defined the x, y axis, and line function like these
var x = d3.scaleTime().domain(d3.extent(res, d => d.Time)).range([0, width]),
y = d3.scaleLinear().domain([0,d3.max(res, d=>d.psi)]).range([height, 0]),
var line = d3.line()
.x(function(d) { return x(d.Time) })
.y(function(d) { return y(d.psi) });
Every thing was very standard set-up.
When I insert line element to the chart, I found two ways to insert them.
Method 1, with "datum"
svg.append("path")
.datum(res) //"datum"
.attr("class", "line")
.attr("d", line);
Method 2, with by line(res)
svg.insert("path")
.attr("class", "line")
.attr("d", line(res)); //line(res), like a function
Both methods work, just wondering are there any difference between these two methods?
Thanks,
The difference between the two methods is that by method one, you have assigned res as the "datum" object of the node. That means that if you were to store it in a variable - or I think even if you would re-select them (not sure though) - you should be able to reliably access the current value using .attr('...', function(d) { });. That can be useful if you want to do stuff to it, like animations or styling, and the value might update often - so it's a hassle to carry it around.
Other than that, there is no real difference. One of the things I like to use .datum() for is when I have a container for every shape and I want to add a node to every container, then it might be useful to use container.select('text').datum((d) => d) to feed the datum object from the container to its text child.
Related
As a newbie to D3.js, this is a tricky problem that bothers me for a while. I am plotting a series of bars using the SVG rect elements, the associated object is (defined outside d3.csv() function)
var bars = g.selectAll(".bar rect")
.data(data)
.enter()
.append("rect")
.attr("class", "bar rect");
.attr("x", function(d) { return x(d.date)})
.attr("y", function(d) { return y(max(d.open, d.close));})
.attr("width", 2*tickWidth/3)
.attr("height", function(d) { return y(min(d.open, d.close)) - y(max(d.open, d.close));})
.attr("fill", function(d) { return d.open > d.close ? "red" : "green" ;});
However, the data is loaded using d3.csv() function from a file. The other settings, such as x, y, etc are rather standard as in this example. Now, because I wanted to add one more line on this chart and the range of y axis is changed accordingly. For this, I need to re-map the range for the y axis.
Here comes the problem. First of all, it appears a bit difficult for me to get the data from the above bar chart. I used the data() method to acquire the data of the above bars object (inside d3.csv() function while plotting the other line) but it gave me an empty array. Second, I am not sure how I can associated the newly set y range to the bars object without calling again the data() method, since each time I fetch the newly mapped y range to the bar object, it is plotted again.
I definitely lack a good understanding of the d3's philosophy of separating style and data. Your help will be highly appreciated.
I've managed to draw a barchart (it's inverted I know :)) for each year of data by reading in the CSV data and then using d3.nest() to group the data for each date per year, see block here or blockbuilder here.
However I'm am now trying to append notes from my annotations array to each chart and I'm stuck.
One approach I'm trying is to selectAll(".charts") and to append my nested annotations array i.e. annotationsPerYear. But I'm finding it difficult to join my annotationsPerYear key with my charts and then to iterate and append the notes for each year onto the correct chart. Any ideas?
You can rely on nested selections for this. You're already using nested selection with this bit:
svg.selectAll(".bar")
.data(function(d) {return d.values;})
The above binds different data to each of the 3 SVGs created earlier. It does this by calling the function(d) 3 times, and returns a different d.values each time.
You can do a similar thing to bind and create a different set of annotations in each SVG. You need a function (passed to data()) that returns the applicable annotations per chart, but this time you don't have something pre-computed like d.values. Instead, you have to extract the applicable annotations out of annotations array, using filter():
svg.selectAll(".annotation")
.data(function(d) {
// return annotations whose key matches d's key
return annotations.filter(function(a) { return a.key == d.key; });
})
Then you can use enter() and append() as you've done for the bars to create the text and position it. I'm not sure how you intend to lay it out, but altogether you want something like this:
svg.selectAll(".annotation")
.data(function(d) {
return annotations.filter(function(a) { return a.key == d.key; });
})
.enter()
.append("text")
.attr("class", "annotation")
.attr("x", function(d) { return x(d.xPos); })
.attr("y", function(d) { return d.yPos; })
.text(function(d) { return d.note; })
See:
Updated block
Updated blockbuilder
I overlay a leaflet map with the d3 library. The points get displayed, as well as the map. However, the Colorbrewer does not work...
Its supposed to color the dots on the map according to their value, instead, they stay black. I could hardcode that with something like if value == 0.1 but thats not what I want...
Thats my code, the structure of cities.json can be seen here, the colorbrewer is this one
...
// add colorbrewer
var colorScale = d3.scale.quantize()
.domain([extent[0], extent[1]])
.range(colorbrewer.YlGn[n]);
// uses d3 data join method
// for each data point a "path" is created
var feature = g.selectAll("path")
.data(collection.features)
.enter()
.append("path")
.style("fill", function(d) {
colorScale(d.properties.pop_max);
});
...
Any ideas what is going wrong?!
There are negative values in my d.properties.pop_max. Could that be the problem?
You're missing a return in the fill function.
...
...
.style("fill", function(d) {
// add a 'return' here.
return colorScale(d.properties.pop_max);
});
Also, you can just write .domain(extent) directly when initializing your colorScale since d3.extent returns a two element [min, max] array
var colorScale = d3.scale.quantize()
.domain(extent) // instead of .domain([extent[0], extent[1]])
.range(colorbrewer.YlGn[n]);
In Mike Bostock’s ‘Towards Reusable Charts’ why the initial data link to <p> element is made with datum(data):
d3.csv("sp500.csv", function(data) {
var formatDate = d3.time.format("%b %Y");
d3.select("#example")
.datum(data)
.call(timeSeriesChart()
.x(function(d) { return formatDate.parse(d.date); })
.y(function(d) { return +d.price; }));
});
while further link of data to <svg> element inside the chart() function is made with data([data]):
// Select the svg element, if it exists.
var svg = d3.select(this).selectAll("svg").data([data]);
// Otherwise, create the skeletal chart.
var gEnter = svg.enter().append("svg").append("g");
gEnter.append("path").attr("class", "area");
gEnter.append("path").attr("class", "line");
gEnter.append("g").attr("class", "x axis");
In his own answer Mike says that these two approaches are interchangeable, except the former doesn’t compute a join. So why to use data([data]) here?
Also I don’t quite get what happens if there is already an <svg> element as the comment line suggests. For me the enter selection is empty in this case and no further append will work…
I must be misunderstanding something…
Thanks for your help!
The advantage of using .data([data]) in the second case is that if there's no SVG, the handling of the enter selection adds it. If the SVG exists, the code is exactly equivalent to .datum(data) -- the data bound to the element is changed and nothing else happens.
I'm learning D3.js and trying to get my head around data keys used with streamgraphs. I would like to adapt the official streamgraph example:
...so that each path has an explicit data key, and so that the mouseover logs the data key.
The official example adds paths as follows:
var area = d3.svg.area()
.x(function(d) { console.log('x', d.data); return d.x * w / mx; })
.y0(function(d) { return h - d.y0 * h / my; })
.y1(function(d) { return h - (d.y + d.y0) * h / my; });
vis.selectAll("path")
.data(data0)
.enter().append("path")
.style("fill", function() { return color(Math.random()); })
.attr("d", area);
I tried adapting the code as follows, but I'm not sure how to change the structure of data0 (currently an array of arrays) to achieve what I want:
vis.selectAll("path")
.data(data0, function(d) { return d.name }) // Add key function
.enter().append("path")
.style("fill", function() { return color(Math.random()); })
.attr("d", area)
.on("mouseover", function (d,i) {
console.log("mouseover", d.name); // Log name property on mouseover
});
As it stands, without my having made any changes to the structure of data0, it unsurprisingly does not work. How can I add a name property to data0 without also messing up the area and .data() functions?
UPDATE: To be a bit clearer: the D3 docs say that the area function is expecting a two-dimensional array. So if I change data0 from a two-dimensional array, to an array of objects, each with a name key and a data key, how can I also change what I pass to area?
The data in the example doesn't have a "name" property, so you would need to add that to the data to use it. The data keys you refer to are used when merging/updating data, i.e. you have drawn some paths already and then update (some of them). The .data() function will try to figure out what data is updated and what data is new. If that doesn't work for you, you can use the data key to help it, i.e. in your case tell it that things with the same name are the same data.
If what you mean by data keys are "data legends", then you might want to take a look at the following examples where I've completely separated the placement of magnitudes, legend bullets and legend text in different areas of the charts.
Multiple D3 Pie Charts Mixed In With HTML Layout Constructs
Multiple D3 Horizontal Bar Charts Mixed In With HTML Layout Constructs
In each of the examples, you'll clearly see how the data is labeled, structured, passed in, and used.
I also tied them together through mouseover and mouseout events so that mousing over or out of any element causes all elements in a set to change color simultaneously.
I hope this helps.
Frank