Display y-axis as percentages? - javascript

I'm making a chart, and I want my y-axis tick labels to display as percentages (instead of decimals). How can I do this?
My current code looks something like
yAxis
.append("text")
.attr("class", "ticklabel")
.attr("x", 0)
.attr("y", y)
.attr("dy", ".5em")
.attr("text-anchor", "middle")
.text(y.tickFormat(5))
.attr("font-size", "10px")
I noticed that d3 has a format specifier, but I'm not sure how to use these in conjunction with tickFormat.

It looks like you authoring the axis by hand. I recommend using the d3.svg.axis component instead, which does the rendering for you. For example:
http://bl.ocks.org/1166403
If you want to format ticks as percentages, then use d3.format's % directive:
var formatPercent = d3.format(".0%");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickFormat(formatPercent);
You can instead use the p directive if you want to round to the percentage to significant digits.
To answer your question more directly, you use d3.format instead of the scale's tickFormat. Scales provide a default tickFormat for convenience, but if you want different behavior, then you use a custom d3.format rather than the scale's default one.

Related

D3 chart zooms the plot but not the axis - callback.apply is not a function

I've got a chart based on this tutorial
It's working very all apart from that when I zoom, the plot works but not the x-axis.
The problem lies on my zoom function
const zoomed = (event) => {
xScale.range(
[margin.left, width - margin.right].map((d) =>
event.transform.applyX(d)
)
);
svg
.selectAll(".bars rect")
.attr("x", (d) => xScale(d.name))
.attr("width", xScale.bandwidth());
svg.selectAll(".x-axis").call(xAxis);
};
The last line svg.selectAll(".x-axis").call(xAxis); throws the error: callback.apply is not a function.
if I log svg.selectAll(".x-axis") to the console I get the corresponding nodes with the call method attached so I don't know why's not working. I'm using d3 7.6.1 so this method might not work in the same way but after researching, I couldn't find an answer.
Here's a sandbox
When you zoom, you want to recreate the x scale inside the g.x-axis element. To do that you need to call an axis generator on that element like that:
axisGenerator(d3.selectAll(".x-axis"))
which is equivalent to execute:
d3.selectAll(".x-axis").call(axisGenerator)
In your code the parameter to call() is not an axis generator. The xAxis variable is a d3 selection. So you first need to create an axis generator:
const xAxis = d3.axisBottom(xScale).tickSizeOuter(0);
And then initialize the x axis by creating a group element and call the axis generator on it:
svg.append("g")
.attr("transform", `translate(0, ${height - margin.bottom})`)
.attr("class", "x-axis")
.call(xAxis)
In your zoomed function, you can then update the x axis like that:
svg.selectAll(".x-axis").call(xAxis);

javascript D3 V6, struggling with chained syntax, specifically effects of '.call' and 'selectAll'

Below are two examples of similar D3 code, one works and the other doesn't. In this example, I want to change the color of the lines of an axis -
This doesn't work, stroke color lines of the axis do not get changed to green -
var x_axis = svg.append("g")
.attr("class", "axis")
.attr("transform", `translate(20, ${height - 50})`)
.call(d3.axisBottom(ordinalScale))
.selectAll("text")
.attr("transform", "translate(-5,5)rotate(-45)")
.style("text-anchor", "end")
.style("font-size", "8px")
.style("fill", "#102040");
x_axis.selectAll("line, path").style("stroke", "green");
BUT this works, the lines get changed to green:
var x_axis = svg.append("g")
.attr("class", "axis")
.attr("transform", `translate(20, ${height - 50})`)
.call(d3.axisBottom(ordinalScale));
x_axis.selectAll("text")
.attr("transform", "translate(-5,5)rotate(-45)")
.style("text-anchor", "end")
.style("font-size", "8px")
.style("fill", "#102040");
x_axis.selectAll("line, path").style("stroke", "green");
The difference being that in the first (failed) example, I chain the 'selectAll("text")' operations to the 'call(d3.axisBottom)' with the 'selectAll("line, path")' operations in a following expression, and in the second (successful) example, I have following seperate expressions for each of the text and line/path operations.
This is not critical since I can get the effect I want, but it seems to me that they should be equivalent but obviously there is some subtlety of the syntax that I do not understand. Does this have something to do with the '.call' operation?
The first code block doesn't work because x_axis doesn't contain what you think it does.
var x_axis = svg.append("g") // returns a selection of a g element
.attr("class", "axis") // returns the same selection of a g
...
.call(d3.axisBottom(ordinalScale)) // returns the same selection of a g
.selectAll("text") // returns a new selection of text elements
...
.style("fill", "#102040"); // returns the same selection of text elements
x_axis is defined by the last value returned by the chain. So,
x_axis in the above is a selection of text elements, text elements can't (and in this case don't) contain any child path or line elements, so x_axis.selectAll('line, path') will return an empty selection. Consequently, setting any property for an empty selection won't change anything.
The second code block works because x_axis remains a selection of a g - selection.call() returns the same selection that .call() was chained to, like .attr() or .style(), among other methods. Whereas selectAll() and select(), among other methods, return new selections.

d3 .ticks() .tickValues() not updating in chart

I have seen others with this question (What does it take for .ticks() to work?) but I still have not found a clear solution. My dataset is quite large and the x-axis tick labels (simple strings) are overlapping. I would like to set a maximum of maybe 10 ticks on the chart. A snippet of my code is below.
// Add the x Axis
svg.append("g")
.attr("transform", "translate(0," + (chart_height - margin["bottom"]) + ")")
.call(d3.axisBottom(x)
.ticks(5),
)
.selectAll("text")
.attr("y", (y(d3.extent(y_values)[0]) - y(0) + 30))
.style("font-family", "PT Sans")
.style("font-size", "14px");
I am referencing this: https://github.com/d3/d3-axis/blob/master/README.md#axis_ticks but no matter what I do, my number of ticks never changes. I tried both .ticks() and .tickValues().
Am I maybe adding .ticks() to wrong portion of code? I'm super new to d3.

Drawing Separate Domains for Axes under Multiple Small Barcharts in D3.js

My intention is to draw multiple small barcharts in one svg with different domains, building upon the example.
The main problem I am stuck on seems to be the problem of extracting values for a particular key from the output of d3.nest and defining the domain corresponding to each key. The problem arises when plotting all the values, for which the dates are drawn in the domain. Since not each key has a value corresponding to all possible dates, there is a tail plotted on the right, which breaks the order. Could you please tell me how it can be fixed, how can the inputs without a corresponding output be removed from the plotted set of values?
Here is the result:
https://plnkr.co/edit/KUnMSfJwWan3JaIpZutZ/
The data is in the following form:
CSC 20160919 4.0
MAT 20160923 16.0
In the example sample of data given, there are two dates, and two dates are going to be plotted for each small subgraph. It would be great to separate the dates corresponding to a particular key in such a way that only those dates for which there is a corresponding score are plotted.
There is a deficiency: the domains must be separate, while in the example above they are not.
Could you please tell me how I can plot the graphs for each key with the date corresponding to a particular key as defined in d3.nest?
The relevant part of the code seems to be here:
x.domain(data.map(function(d) { return d.date; }));
and here:
svg.selectAll(".bar")
.data(function(d) {return d.values;})
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.date); })
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.score); })
.attr("height", function(d) { return height - y(d.score); })
.attr("fill", function(d) {return color(d.score)})
APPENDIX
The following questions can be useful:
D3.js binding an object to data and appending for each key
X,Y domain data-binding in multiple grouped bar charts in d3.js
You can also find the following links helpful:
Grouped Bar Chart
Tutorial: Grouped Bar Chart
Grouping Data
HYPOTHESES
Can we nest the data twice, first by course, then by date, and then plot keys' keys on the axes? Is nesting the best way to approach the problem at all?
Here is my solution (warning: I'm the first one to admit that my code is too complicated. Let's see if someone else comes with a simpler solution).
First, we create an empty object:
var xScale = {};
And populate this new object with two functions, one for each different scale.
courses.forEach(function(d) {
xScale[d.key] = d3.scale.ordinal()
.rangeRoundBands([0, width], .1)
.domain(d.values.map(function(e) {
return e.date
}));
});
After this, we'll have two different scales:
xScale.HIS//this is the scale for the first SVG
xScale.PHY//this is the scale for the second SVG
Now you can use those scales in your bars, but not directly. We have to define what scale is going to be used in what SVG.
To do that, firstly, we'll get the key property in the data for each SVG, and then using this value to access the correct object inside xScale, which contains the proper scale:
.attr("x", function(d) {
var scale = d3.select(this.parentNode).datum().key;
return xScale[scale](d.date);
})
.attr("width", function() {
var scale = d3.select(this.parentNode).datum().key;
return xScale[scale].rangeBand()
})
The most complicated part is calling the axes. Here, we'll use each to call xAxis two times, using the first argument (the datum) to get the value of key:
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.each(function(d) {
return d3.select(this).call(xAxis.scale(xScale[d.key]))
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", "-.55em")
.attr("transform", "rotate(-90)");
})
All together, this is your updated plunker: https://plnkr.co/edit/XFKzQm0zp0MkhzK4Qt5d?p=preview

d3.js how to get minimum value of scale's domain when using nice()

I have a scatter plot where I am trying to plot a reference point that should appear directly on the x-axis, but I am creating the y-axis like so:
// give ourselves some space
yMin = yMin * 0.9;
yMax = yMax * 1.1;
// set up y
var yValue = function (d) {
return d.price;
},
yScale = d3.scale.linear()
.domain([yMin, yMax])
.range([height, 0])
.nice(),
yAxis = d3.svg.axis()
.scale(yScale)
.orient("left");
The issue is that I want to plot a reference point directly on the x-axis. If I don't use .nice(), I can easily plot the reference point like so:
var reference = svg.append('g').attr("class", "grid-reference");
reference.append("circle").attr("id", "some-reference-value")
.attr("class", "dot")
.attr("r", 9)
.attr("cx", xScale(someReferenceValue))
.attr("cy", yScale(yMin))
.style("fill", "grey")
.style("opacity", 1);
but I can't seem to figure out how to do this when using .nice(). Is there another way I could go about this such as somehow getting the y-coordinate of the x-axis line?
I started digging through the d3 code to see how I could calculate the nice value and I understand the code, but it seems wasteful to calculate the value again not to mention the unnecessary maintenance of duplicate code - especially considering I also have reference points I will need to plot along the y-axis and I am using a logarithmic scale for my x-axis (the logarithmic nice() function is different from the linear nice() function so this would mean additional duplicate code and unnecessary maintenance).
Ideas?
You can simply query the scale for its domain after .nice() and then get the minimum value of that:
var yMin = yScale.domain()[0];

Categories

Resources