D3 axis label has to be added separately? - javascript

In this axis label example, which uses D3 v4, it adds the x axis and the text label as separate nodes under svg.
// Add the x Axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// text label for the x axis
svg.append("text")
.attr("transform",
"translate(" + (width/2) + " ," +
(height + margin.top + 20) + ")")
.style("text-anchor", "middle")
.text("Date");
When I chain the code above (hence moving the text element under the x axis group):
// Add the x Axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x))
.append("text")
.style("text-anchor", "middle")
.text("Date");
Then my axis title is not visible any more (see screenshot below). I can still find my text element in DOM, under the x axis group, but it's not there in the rendered HTML.
I want to know:
Is it by design that D3 wants me to add axis and its label separately (i.e., not chaining)?
Why is my text element not visible after I move it under the x axis group?

D3 axis label has to be added separately?
No, it doesn't. You can chain, that's not the problem. The problem here is the fill of the text element.
As you can see in the screenshot you linked, the container group has "none" as fill. Since the text inherits the parent's attributes/styles, you'll have to change its fill from "none" to any color you want:
var svg = d3.select("svg");
var xScale = d3.scaleLinear()
.domain([1, 10])
.range([10,390]);
var xAxis = d3.axisBottom(xScale);
var gX = svg.append("g")
.attr('class', 'axis')
.attr("transform","translate(0,40)")
.call(xAxis)
.append("text")
.attr("fill", "black")//set the fill here
.attr("transform","translate(120, 40)")
.text("Hello World!!!");
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="400" height="80"></svg>
PS: this problem wouldn't happen in D3 v3.x.

Related

X Axis text labels are not rotating in d3

I am working with a D3 stack bar chart.
The fiddle is here
When I am clicking on the Month button, the chart appears with a messy x axis label
I want to rotate the x-axis label's conditionally in case of a good amount of data (In the fiddle, for months data)
My first step is to rotate the label. I am appending the below part.
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-65)" );
I am not sure, why it is not working. Whats the problem here?
There's nothing wrong with your code - see below:
var margin = {top: 25, right: 25, bottom: 25, left: 25}
var width = 600 - margin.left - margin.right;
var height = 200 - margin.top - margin.bottom;
// x domain
var x = d3.timeDays(new Date(2020, 00, 01), new Date(2025, 11, 01));
// x scale
var xScale = d3.scaleTime()
.domain(d3.extent(x))
.range([0, width]);
// svg
var svg = d3.select("#scale")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// messy x-axis
svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${height - 50})`)
.call(d3.axisBottom(xScale)
.ticks(d3.timeMonth)
);
// x-axis with rotated labels
svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,0)`)
.call(d3.axisBottom(xScale)
.ticks(d3.timeMonth)
)
.selectAll("text") // YOUR CODE
.style("text-anchor", "end") // YOUR CODE
.attr("dx", "-.8em") // YOUR CODE
.attr("dy", ".15em") // YOUR CODE
.attr("transform", "rotate(-65)" ); // YOUR CODE
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.3.1/d3.min.js"></script>
<div id="scale"></div>
What you need to adjust in the fiddle is to apply the rotation on the axis labels within the draw function. So from this:
svg.selectAll("g.x.axis")
.transition(t).call(xAxis);
To this:
svg.selectAll("g.x.axis")
.transition(t).call(xAxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-65)" );
The axis is getting re-rendered depending on which button is clicked, and it isn't preserving the setting in the initial creation of the svg and two axis gs.
This answer might be useful.

Creating a categorical line chart in D3.js (V4)

I'm relatively new to D3.js and I'm visualising the 'PassengersIn' & 'PassengersOut' values from my busdatasimple.json file. For reference, one of the JSON objects looks like this;
{
"BusNo": "1",
"Date": "21 November 2016",
"Time": "09:10:34 AM",
"Destination": "Pier 50",
"Longitude": "-122.383262",
"Latitude": "37.773644",
"PassengersIn": "8",
"PassengersOut": "1"
}
I'm now trying to graph the PassengersIn & PassengersOut against the Destination using two lines on a line graph. I'm struggling with the axes as the x has only 2 ticks and the y is not scaling to my data. As seen below;
My code is as follows. I've removed the irrelevant Google Maps and jQuery.
//Setting The Dimensions Of The Canvas
var margin = {top: 20, right: 20, bottom: 70, left: 40},
width = 650 - margin.left - margin.right,
height = 350 - margin.top - margin.bottom;
//Setting X & Y Ranges
var x = d3.scaleOrdinal().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
//Define The Axes
var xAxis = d3.axisBottom().scale(x);
var yAxis = d3.axisLeft().scale(y).ticks(10);
//Add The SVG Element
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right + 50)
.attr("height", height + margin.top + margin.bottom + 200)
.attr("class", "svg")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//Load Data From JSON
d3.json("busdatasimple.json", function(error, data) {
//Functions for Y-Axis Grid Lines
function yGridLines() {
return d3.axisLeft().scale(y).ticks(5);
}
//Adding the Y-Axis Grid Lines
svg.append("g")
.attr("class", "grid-lines")
.call(yGridLines().tickSize(-width, 0, 0).tickFormat(""));
//Adding Y-Axis
svg.append("g")
.attr("class", "y axis").call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 5)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Passengers In");
//Adding X-Axis (Added to the end of the code so the label show over bottom bars)
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "middle")
//.attr("dx", "-.8em")
.attr("dy", "-.55em")
.attr("transform", "translate(-5, 15)")
.attr("font-family", "Arial")
.attr("font-weight", "bold")
.attr("font-size", "1.1em");
x.domain(data.map(function(d){return d.Destination;}));
y.domain([d3.min(data, function(d){return d.PassengersIn;}), d3.max(data, function(d) {return d.PassengersIn;})]);
var line = d3.line()
.x(function(d){return x(d.Destination);})
.y(function(d){return y(d.PassengersIn);});
svg.append("path").datum(data)
.attr("class", "line")
.attr("d", function(d){return d.PassengersIn;})
.attr("stroke", "green")
.attr("stroke-width", 2);
});
I've managed to find a few examples that use a categorical ordinal scale, however, they are all using v3 of d3.js and after reading through the v4 API countless times I still can't figure it out.
You want a categorical scale, that's right, but you don't want an ordinal scale here.
There was a lot of changes from v3 to v4. In v3, you could set .rangeBands, .rangeRoundBands, .rangePoints and .rangeRoundPoints to your ordinal scale, which therefore could accept an continuous range. Not anymore: in D3 v4 you have the brand new scaleBand and scalePoint.
In v4, in a regular ordinal scale (which is scaleOrdinal):
If range is specified, sets the range of the ordinal scale to the specified array of values. The first element in the domain will be mapped to the first element in range, the second domain value to the second range value, and so on. If there are fewer elements in the range than in the domain, the scale will reuse values from the start of the range. (emphases mine)
So, in an scaleOrdinal, the range need to have the same length (number of elements) of the domain.
That being said, you want a point scale (scalePoint) here. Band scales and point scales...
...are like ordinal scales except the output range is continuous and numeric. (emphasis mine)
Check this snippet, look at the console and compare the two scales:
var destinations = ["foo", "bar", "baz", "foobar", "foobaz"];
var scale1 = d3.scaleOrdinal()
.range([0, 100])
.domain(destinations);
var scale2 = d3.scalePoint()
.range([0, 100])
.domain(destinations);
destinations.forEach(d=>{
console.log(d + " in an ordinal scale: " + scale1(d) + " / in a point scale: " + scale2(d))
})
<script src="https://d3js.org/d3.v4.min.js"></script>
Regarding your y-axis problem:
Set the domain of the y scale before calling the axis. So, instead of this:
svg.append("g")
.attr("class", "y axis").call(yAxis);
y.domain([d3.min(data, function(d){
return d.PassengersIn;
}), d3.max(data, function(d){
return d.PassengersIn;
})]);
Change the order:
y.domain([d3.min(data, function(d){
return d.PassengersIn;
}), d3.max(data, function(d){
return d.PassengersIn;
})]);//set the domain first!
svg.append("g")
.attr("class", "y axis").call(yAxis);

In D3 v4, how do I align axes appropriately?

I am using D3 v4. I have a bar graph created with an x-axis using scaleBand(). Now, I have created a y-axis but my issue is that no matter how I position it, it is cutting into the actual bars of the graph.
At the top of my JS file, I have:
var width = 350;
var height = 300;
Then, the part where I actually create the Y-axis:
var y = d3.scaleLinear()
.domain([0, 300000])
.rangeRound([height, 0]);
var yAxis = d3.axisRight(y);
yAxis.ticks(6);
chart.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + (width - dist_from_right) + ", 0)")
.call(yAxis);
As you can see from the picture, the axis stretches the entirety of the height of the SVG, from bottom to top meaning that half of the 0 gets cut off and half off the 300,000 gets cut off.
First question: how do I "squish" (or scale) the y-axis so that it displays within the confines of the SVG?
Next, I want to translate the y-axis so that it is not cutting into my red bar. If I try to use the transform attribute, I can push the axis far to the right of the SVG borders but that means the numbers are off the SVG boundary. I've also tried to increase the width variable but that does nothing because it just stretches out the x-axis proportionally.
Second question: how do I move the y-axis so that it is not cutting into the x-axis and red bar and also remains visible in the SVG window?
Thanks!
In D3, axes are positioned according to the range of corresponding scales. So, you need a "padding" for the ranges. Right now, as your range goes from 0 to height (or vice versa, it doesn't matter), the axis starts at the very beginning of the SVG and ends at its very end.
I see you have a dist_from_right, but I don't know what are you doing with it in your x scale. So, for now, let's suppose you don't have any padding.
First, let's set the paddings:
var paddingLeft = 10, paddingRight = 10, paddingTop = 10, paddingBottom = 10;
Here, 10 is just a given number, change it accordingly.
After that, set the ranges using the paddings:
var y = d3.scaleLinear()
.domain([0, 300000])
.rangeRound([height - paddingBottom, paddingTop]);
The same for your x scale:
var x = d3.scaleLinear()
.domain([0, someValue])
.rangeRound([paddingLeft, width - paddingRight]);
Then you define the axis:
var xAxis = d3.axisBottom(x);//the same for the y axis
Having the ranges with the paddings, call the axes setting that paddings:
chart.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + (width - paddingRight) + ", 0)")
.call(yAxis);
And the same for the x axis:
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (height - paddingBottom) + ")")
.call(xAxis);
Here is a working example. I made the SVG light gray and the plotting area white, so you can see the paddings.:
var paddingLeft = 20, paddingRight = 40, paddingTop = 10, paddingBottom = 40;
var width = 300, height = 300;
var chart = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
chart.append("rect")
.attr("x", paddingLeft)
.attr("y", paddingTop)
.attr("width", width - paddingLeft - paddingRight)
.attr("height", height - paddingTop - paddingBottom)
.attr("fill", "white");
var y = d3.scaleLinear()
.domain([0, 100])
.rangeRound([height - paddingBottom, paddingTop]);
var x = d3.scaleLinear()
.domain([0, 100])
.rangeRound([paddingLeft, width - paddingRight]);
var yAxis = d3.axisRight(y);
var xAxis = d3.axisBottom(x);
chart.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + (width - paddingRight) + ", 0)")
.call(yAxis);
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (height - paddingBottom) + ")")
.call(xAxis);
svg {
background-color: lightgray;
}
<script src="https://d3js.org/d3.v4.min.js"></script>

How to append text to SVG

I am trying to append text to the svg by below code for that I the following code but it's not working.
var svg = d3.select("#chart");
xScale.domain(data.map(function(d){return d.key;}))
yScale.domain([0,d3.max(data,function(d){return d.doc_count;})])
svg.append("g")
.attr("class","axis")
.attr("transform","translate("+margin.left+","+height+")")
.call(xAxis);
svg.append("g")
.attr("class","axis")
.attr("transform","translate("+margin.left+",0)")
.call(yAxis);
svg.attr("width",width + margin.left + margin.right)
.attr("height",height + margin.top + margin.bottom)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x",function(d){return margin.left + xScale(d.key)})
.attr("y",function(d){return yScale(d.doc_count)})
.attr("width", xScale.rangeBand())
.attr("height", function(d) { return height - yScale(d.doc_count); })
.attr("fill","teal");
svg.selectAll("text")
.data(data)
.enter()
.append("text")
.text(function(d){return d.doc_count;})
At the end when I see the dom there are no text element tags.
You are calling call(xAxis) and call(yAxis) which will create <text> elements so when you say d3.selectAll("text") it selects those elements created by call(xAxis) and call(yAxis).
so suppose your data count is 5 then it has already 5 <text> elements and it won't append a new one.
Update your code to
svg.append("g")
.attr("class","axis")
.attr("transform","translate("+margin.left+","+height+")")
.call(xAxis);
svg.append("g")
.attr("class","axis")
.attr("transform","translate("+margin.left+",0)")
.call(yAxis);
after below code
svg.selectAll("text")
.data(data)
.enter()
.append("text")
.text(function(d){return d.doc_count;})
The issue in your code is the same as Soham mentioned in his answer. When you use text element selector, it will return all elements inside your SVG including the axis texts which got created automatically by d3 while creating axes.
So the best possible selection would be to use a different selector for the additional text labels.
svg.selectAll("text.label") //Or simply (".label")
.data(data)
.enter()
.append("text")
.attr("class","label")
.text(function(d){return d.doc_count;})
You will also have to specify the text position attributes in this case.
If you would like to position text elements relative to the rect elements, you can create group elements for grouping rect and text elements and set the position attribute (transform) of group elements.

Moving the axes in d3.js

I have two axes in my graph right now, that are stuck at the very left and bottom of the graph. I want to make the axes line up with the (0,0) coordinate, or in other words I want the axes to be at x=0 and y=0
Here's my axes code:
//setup x
var xAxis = d3.svg.axis()
.scale(xRange)
.tickSize(5)
.tickSubdivide(true),
//setup y
yAxis = d3.svg.axis()
.scale(yRange)
.tickSize(5)
.orient("left")
.tickSubdivide(true);
I was thinking that the way to do it might just be to make a smaller svg underneath the one that I have, that starts at zero, and put the axes there, and remove them from the one I have right now.
Here's the full code: http://jsfiddle.net/v92q26L8/
The key part of your code is the bit where you attach the axes
vis.append("svg:g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (HEIGHT - MARGINS.bottom) + ")")
.call(xAxis);
vis.append("svg:g")
.attr("class", "y axis")
.attr("transform", "translate(" + (MARGINS.left) + ",0)")
.call(yAxis);
At the moment you are positioning the axes bottom and left using transforms on the groups (svg:g nodes) which contain them.
To reposition the axes you simply need to adjust these transforms using your defined scales.
For your x axis
.attr("transform", "translate(0," + (HEIGHT - MARGINS.bottom) + ")")
becomes
.attr("transform", "translate(0," + yRange(0) + ")")
for your y axis
.attr("transform", "translate(" + (MARGINS.left) + ",0)")
becomes
.attr("transform", "translate(" + xRange(0) + ",0)")
Additionally, it may be sensible to change the names of your scales. The term 'range' has a very particular meaning in D3, I'd suggest xScale and yScale.
JS Fiddle

Categories

Resources