Two alternative D3.js scale syntaxes - javascript

Say we have a y-scale that converts the data domain to rangebands that are 20px for each data element.
var y = d3.scale.ordinal()
.domain(data)
.rangeBands([0, 20 * data.length]);
I would normally use this scale to determine the y-coordinate of something in this manner:
svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("y", y)
But in some tutorials, I've seen an alternative syntax.
.attr("y", function(d) { return y(d); })
I'm not sure what's going on here, and I would like to understand it before I move on with learning D3. I know y is a function, so the d within parenthesis would be an argument of that function. But we already specified the data input in the y function. I would love to read an explanation of what we're actually doing with y(d). What are the pros and cons of the two alternatives?

I think that's a minor difference that's maybe left over from copying and pasting other attrs that are set with an anonymous function (as in the second case). There should be no reason to wrap y in an anonymous function.

Related

d3.js insert line path by "datum" or line (data)

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.

Updating x axis d3js

I'm trying to update my x axis in a D3js bar chart (is partially working) depending on a user filter, the bars are actually changing but is not doing it well. I don't really know where is the problem and I need some help.
in this part of the code I'm updating the bar chart
function update(selectedGroup) {
svg.selectAll("rect").remove()
var groups = d3.map(dataFilter, function(d){return(d.group)}).keys();
x.domain(groups);
var dataFilter = result.filter(function(d){return d.group==selectedGroup});
console.log(dataFilter);
var rectG=rectangulos(dataFilter)
}
the complete bar chart
how is working now:
the result should be something like this
I have an live example here
There is a relatively straightforward reason you are seeing this behavior.
When the domain of the scale x is all the groups x.bandwidth() is small. But when the domain of x is only one value, x.bandwidth() is large. In both cases, the first band starts in the same location.
Next we have a nested scale here xSubgroup - the range of this is equal to x.bandwidth(). When the domain of x changes, we need to update the range of xSubgroup. If we don't do this, the bars will still be very thin and start at the beginning of the axis (as the bars' bandwidth aren't changing even if the group's bandwidth does). You don't update the sub scale's range, but we need to do that:
x.domain(groups);
xSubgroup.range([0, x.bandwidth()])
With this we get the update we're looking for.
But the axis labels remain unchanged. Updating a scale doesn't update the axis unless we explicitly do so. I'll break up your method chaining and store a reference for the g holding the axis:
var xAxis = svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
xAxis.selectAll("text")
...
Now we can update the axis, I'm carrying forward the text styling as well. You can simplify the code by using an update function to do all the entering/exiting/updating of axes and data - here we have some duplication in that both the initial set up and the update function have overlap.
To update the axis we use:
// Call the axis again to update
xAxis.call(d3.axisBottom(x))
xAxis.selectAll("text")
.style("text-anchor", "end")
.attr("font-size", "55px")
.attr("y", "-7")
.attr("x", "-7")
.attr("transform", "rotate(-90)");
Which gives the desired behavior if I understand correctly, updated plunkr

D3: Are "parabolic" scales possible?

I'm working on a chart for which each observation has a value between [-100,100], and I want to plot each point's position on a scale. The challenge is that the vast majority of the points have values in one region of the scale (the distribution is essentially Gaussian with mean 0).
In the past, when I've needed to plot something like a Zipf probability density distribution, I've used log scales to spread out the points in the congested region. Now my situation is similar, except that I have two distributions for which I need to spread out the points (the positive scale from [0, max] and the mirrored negative scale from [0, min]).
I know I could create one scale for positive values and one for negative values, but I'm wondering if it's possible to achieve this layout with only one scale. It seems that something like a parabolic scale could help out here (if that exists). Is it possible to achieve something like this in D3?
Before explaining my proposed solution, some considerations about the comments in this question: you cannot use a log scale in your situation. This is an easy mathematical principle: Log(0) is minus infinity. Actually, this is explicitly stated in the docs:
As log(0) = -∞, a log scale domain must be strictly-positive or strictly-negative; the domain must not include or cross zero.
That being said, let's go to the proposed solution.
You could create your own scale (it's not that complicated). However, here, I'll use a interpolate function, based on this excellent answer (not a duplicate, though, because you want the opposite) and this code from Mike Bostock.
Using a linear scale, we set an interpolator:
var xScale = d3.scaleLinear()
.domain([-100, 100])
.interpolate(easeInterpolate(d3.easeQuadInOut));
Then, we use an easing in the easeInterpolate function:
function easeInterpolate(ease) {
return function(a, b) {
var i = d3.interpolate(a, b);
return function(t) {
return i(ease(t));
};
};
}
Here I'm using d3.easeQuadInOut, which I think suits you, but you can change this for another one, or even creating your own.
Have a look at this demo. I'm creating 50 circles, evenly spaced from -100 to +100 (-100, -96, -92, -88... until +100). You can see that they are moved away from the center. If you use this scale with your data, you'll avoid the crowded data points around zero:
var data = d3.range(51).map(function(d) {
return -100 + (d * 4)
});
var svg = d3.select("body")
.append("svg")
.attr("width", 600)
.attr("height", 100);
var xScale = d3.scaleLinear()
.domain([-100, 100])
.range([20, 580])
.interpolate(easeInterpolate(d3.easeQuadInOut));
svg.append("g")
.attr("transform", "translate(0,70)")
.call(d3.axisBottom(xScale));
svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) {
return xScale(d)
})
.attr("cy", 50)
.attr("r", 4)
.attr("fill", "teal")
function easeInterpolate(ease) {
return function(a, b) {
var i = d3.interpolate(a, b);
return function(t) {
return i(ease(t));
};
};
}
<script src="https://d3js.org/d3.v4.min.js"></script>
In case you ask, that last tick is not 80100. That's just the 80 tick overlapping with the 100 tick (the same thing happens with the -80 and the -100).
Also, it is worth noting that there is nothing wrong in using transformed scales, and that even if it does deform or skew the chart, it's perfect valid and does not lead to misinterpretations, as long as you inform the users about the transformation.

Rescale a bar chart when y axis range changes in d3.js

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.

d3.js: How to add a data key?

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

Categories