How to set minimal step for y-axis in d3.js? - javascript

I have a bar chart where values can range from 0 to 5. The values can only be integers (0, 1, 2, 3, 4, 5).
However, the y-axis renders with smaller steps, for example 0, 0.5, 1, 1.5, 2 etc. I want to set the axis values to integers only, but playing with domain and range hasn't helped me at all.
I don't see an option to set something like minimalInterval = 1. How do I do this? I'm sure there's an option somewhere. Current code for the axes:
var x = d3.scaleBand().rangeRound([0, width]).padding(0.1),
y = d3.scaleLinear().rangeRound([height, 0]);
x.domain(data.map(function(d) { return d.day; }));
y.domain([0, d3.max(data, function(d) { return d.value; })]);
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x))
.selectAll("text")
.attr("y", 0)
.attr("x", 9)
.attr("dy", ".35em")
.attr("transform", "rotate(90)")
.style("text-anchor", "start");
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y))
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", "0.71em")
.attr("text-anchor", "end");

There is nothing like steps for a D3 generated axis.
However, in your case, the solution is simple: you can use tickValues with d3.range(6) and a formatter for integers or, even simpler, you can use ticks.
According to the API,
Sets the arguments that will be passed to scale.ticks and scale.tickFormat when the axis is rendered, and returns the axis generator. The meaning of the arguments depends on the axis’ scale type: most commonly, the arguments are a suggested count for the number of ticks (or a time interval for time scales), and an optional format specifier to customize how the tick values are formatted.
So, in your case:
axis.ticks(5, "f");
Where 5 is the count and f is the specifier for fixed point notation.
Here is a demo (with an horizontal axis):
var svg = d3.select("svg");
var scale = d3.scaleLinear()
.domain([0, 5])
.range([20, 280]);
var axis = d3.axisBottom(scale)
.ticks(5, "f")
var gX = svg.append("g")
.attr("transform", "translate(0,50)")
.call(axis)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
Just for completeness, the same code without ticks:
var svg = d3.select("svg");
var scale = d3.scaleLinear()
.domain([0, 5])
.range([20, 280]);
var axis = d3.axisBottom(scale);
var gX = svg.append("g")
.attr("transform", "translate(0,50)")
.call(axis)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>

Related

D3.js v3 Dot Plot Histogram Duplicating Values

I am building a dot plot histogram with d3.js v3 and I have pretty much finished everything up - except for whatever reason some of my data points are duplicating (certain circles repeating themselves - not all of them, just some). I tried tweaking the axis parameters, as well as the data itself [deleted rows with null values, etc]- however sadly to no avail.
Any help would be immensely appreciated.
Here's my relevant code:
<div id="dotHappy"></div>
var data = d3.csv('happy_dot_modified.csv', function(data) {
data.forEach(function(d) {
d["city"] = d["city"];
d["Happy"] = +d["Happy"];
d["thc"] = +d["thc"];
});
var margin = {
top: 30,
right: 20,
bottom: 30,
left: 50
},
width = 1560 - margin.left - margin.right,
height = 1260 - margin.top - margin.bottom;
I tried this coder block but it wasn't working. (Not sure if this is even what's giving me the issue anyways - perhaps not).
// var x = d3.scale.linear()
// .range([0, width]);
So I went with this:
var x = d3.scale.ordinal()
.rangePoints([0, width])
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var svg = d3.select("#dotHappy")
.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 + ")");
var chart = svg.append("g")
.attr("id", "chart");
Also tried tweaking this, which may or may not even be part of the problem.
x.domain(data.map(d => d.Happy));
y.domain([5, 33]);
// y.domain(data.map(d => d.city));
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
// .append("text")
.attr("class", "label")
.attr("x", width)
.attr("y", -6)
.style("text-anchor", "end")
.text("Happy");
svg.append("g")
.attr("class", "y axis")
// .attr("transform", "translate(0," + width + ")")
.call(yAxis)
// .append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("THC");
var groups = svg.selectAll(".groups")
.data(data)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate(" + x(d.Happy) + ".0)";
});
var dots = groups.selectAll("circle")
.data(function(d) {
return d3.range(1, +d.thc + 1)
// return d3.range(d.thc)
})
.enter().append("circle")
.transition().duration(1000)
.attr("class", "dot")
.attr("r", 10)
.attr("cy", function(d) {
return y(d)
})
.style("fill", "blue")
.style("opacity", 1);
})
Here is a snapshot of my csv file:
city. |. Happy. | thc
Boston. 37. 23
NYC. 22. 30
Chicago. 88. 5
Following is a screenshot of what it currently looks like. So in this case, the tooltip displaying the text box 'The Sister' should be only for one circle (because it should only be one data point), however if you hover over the other 10 orange circles below it, it's all the same - indicating it has been repeated 11 times total:
Actually, all of the circles are repeating vertically. You may not see them all because the repeated circles are being overlapped by other colored circles as these other circles get drawn. For example, the yellow data point "The Sister" is repeating all the way down to the bottom, but the data points below the yellow ones, in blue, pink, green, blue, etc., drew themselves on top of the yellow repeats.
The culprit is this code:
.selectAll("circle")
.data(function(d) {
return d3.range(1, +d.thc + 1)
// return d3.range(d.thc)
})
.enter().append("circle")
which, if you don't want it to repeat, should have been just one line:
.append("circle")
To explain what happened, this code:
var groups = svg.selectAll(".groups")
.data(data)
.enter()
.append("g")
.attr("class", "groups") //NOTE: you should add this line since you have 'selectAll(".groups")'
.attr("transform", function(d) {
return "translate(" + x(d.Happy) + ".0)";
});
already creates a g element for every row in the csv file. And for every g, you created an array using d3.range(1, +d.thc + 1), and appended a circle for each item in that array.
As an example, let's take the row representing "The Sister" data point that has a THC of 33. For that one data point, the code creates one <g>, inside of which it binds the array [1, 2, 3, ..., 33], and therefore appends 33 circles to the <g> element, with the cy attribute between y(1) and y(33).
Now, the question that follows is that, you specified a domain with a minimum of 5 with y.domain([5, 33]). Yet the data-bounded array, generated with d3.range, always begins with 1 and increments up to the value of THC. So some of the values in the array (1,2,3, and 4) always fall outside the y-axis, but d3 was able to translate it to a proper y-position. Is that possible? By default, yes, d3.scale extrapolates when the data is outside of the domain.
By default, clamping is disabled, such that if a value outside the input domain is passed to the scale, the scale may return a value outside the output range through linear extrapolation. For example, with the default domain and range of [0,1], an input value of 2 will return an output value of 2.

d3 v4 line chart transition not working

I would like my line to draw like this example:
https://bl.ocks.org/shimizu/f7ef798894427a99efe5e173e003260d
The code below does not make any transitions, the chart just appears.
I'm aware of browser caching and that is not the issue. I've also tried changing the duration and that doesn't help either. I feel like I'm probably not being explicit about how I want d3 to transition, but I'm unsure how to give d3 what it wants. Your help is greatly appreciated.
EDIT: x-axis domain: [0, 1]. y-axis domain: [-18600, -3300].
// Here's just a few rows of the data
data = [{"threshold": 0.0, "loss": -18600},
{"threshold": 0.008571428571428572, "loss": -18600},
{"threshold": 0.017142857142857144, "loss": -18600}]
var svg = d3.select("svg"),
margin = {top: 20, right: 20, bottom: 30, left: 20},
width = +svg.attr("width") - 400 - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
var x = d3.scaleLinear()
.range([0, width]);
var y = d3.scaleLinear()
.range([0, height]);
var line = d3.line()
.x(d => x(d.threshold))
.y(d => y(d.loss));
var g = svg.append("g")
.attr("transform", "translate(" + (margin.left + 50) + "," + margin.top + ")");
d3.json("static/data/thresh_losses.json", function(thisData) {
draw(thisData);
});
let draw = function(data) {
$("svg").empty()
var x = d3.scaleLinear()
.range([0, width]);
var y = d3.scaleLinear()
.range([0, height]);
var line = d3.line()
.x(d => x(d.threshold))
.y(d => y(d.loss));
var g = svg.append("g")
.attr("transform", "translate(" + (margin.left + 50) + "," + margin.top + ")");
d3.selectAll("g").transition().duration(3000).ease(d3.easeLinear);
x.domain([0, d3.max(data, d => d.threshold)]);
y.domain([d3.max(data, d => d.loss), d3.min(data, d => d.loss)]);
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x))
.append("text")
.attr("class", "axis-title")
.attr("y", 18)
.attr("dy", "1em")
.attr("x", (height/2) - 40)
.attr("dx", "1em")
.style("text-anchor", "start")
.attr("fill", "#5D6971")
.text("Threshold");
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y))
.append("text")
.attr("class", "axis-title")
.attr("transform", "rotate(-90)")
.attr("y", -40)
.attr("dy", ".71em")
.attr("x", -height/2 + 40)
.attr("dx", ".71em")
.style("text-anchor", "end")
.attr("fill", "#5D6971")
.text("Profit ($)");
var line_stuff = g.selectAll(".line")
.data([data]);
line_stuff.enter().append("path").classed("line", true)
.merge(line_stuff);
g.selectAll(".line")
.transition()
.duration(10000)
.ease(d3.easeLinear)
.attr("d", line);
};
From the D3 documentation:
To apply a transition, select elements, call selection.transition, and then make the desired changes.
I found this in the code:
d3.selectAll("g").transition().duration(3000).ease(d3.easeLinear);
This won't animate anything, because there's no .attr() or .style() at the end—no "desired changes" are being made. It's a transition with no changes to make.
Now, let's look at this:
g.selectAll(".line")
.transition()
.duration(10000)
.ease(d3.easeLinear)
.attr("d", line);
This almost fulfills the requirements. It selects .line, creates the transition (and customizes it), and sets the d attribute. If you have d set elsewhere, then this would to transition the path from being empty to having all the data, only...
D3 doesn't transition strings that way. After first checking if the attribute is a number or color, D3 settles on using something called interpolateString. You'd think interpolateString would change characters from a to ab to abc, but actually, all it does is look for numbers within the string, and interpolate those, leaving the rest of the string constant. The upshot is, you just can't animate a string like d from empty to having data unless you do it yourself.
Here's how you can do that, using attrTween (note: not a good idea):
.attrTween("d", function() {
return function(t) {
const l = line(data);
return l.substring(0, Math.ceil(l.length * t));
};
})
This will actually transition between no text to the entire text of the d attribute. However, because of the way SVG paths work, this doesn't look very good.
There is another way, as demonstrated in the example you linked to (and also mentioned by Ryan Morton in a comment): transitioning the stroke-dashoffset. Here's how you would do that:
line_stuff.enter().append("path").classed("line", true)
.merge(line_stuff)
.attr('d', line)
.attr("fill", "none")
.attr("stroke", "black")
.attr("stroke-dasharray", function(d) {
return this.getTotalLength()
})
.attr("stroke-dashoffset", function(d) {
return this.getTotalLength()
});
g.selectAll(".line")
.transition()
.duration(10000)
.ease(d3.easeLinear)
.attr("stroke-dashoffset", 0);
Essentially, the first part tells D3 to:
create the line, make the fill invisible (so you can see the line)
make the stroke dashes equal to the total length of the line
offset the dashes, so that the line is completely hidden at the start
The next part sets up the transition and tells it to transition the offset to 0 (at which point the line will be completely visible because each dash is the same length as the line itself).
If you want to transition the fill, you could change .attr("fill", "none") to .attr("fill", "#fff"), and then do something like this:
g.selectAll(".line")
.transition()
.delay(10000)
.duration(2000)
.ease(d3.easeLinear)
.attr('fill', '#000');
This would use .delay() to wait for the first transition to finish before changing the background from white to black. Note that opacity might be better to animate for performance.

Log axis scale not formatting data

I am trying to create a log-normal plot with an array of data, but when I apply a log scale to the y axis, it only scales the axis ticks and not the actual data being plotted. In summary, all the data is plotted linearly, but the axis scale is shown as log. Below is my axis code:
var y = d3.scale.log()
.domain([.001,maxData])
.range([graphHeight, 0]);
var yAxis = d3.svg.axis()
.scale(y)
.orient("right")
.ticks(20, ".2")
.tickSize(-graphWidth,0,0);
svg.append("g")
.attr("class", "yaxis")
.attr("transform", "translate(" +graphWidth + ",0)")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 70)
.attr("x", -graphHeight/2)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("YLabel");
var line = d3.svg.line()
.x(function(d,i) {
return (i-0.5)*horizontalBarDistance;
})
.y(function(d) {
return graphHeight - d*100;
})
for (names in dataArrays)
{
svg.append("svg:path").attr("class","line").attr("d", line(dataArrays[names]));
}
You have to use the scale in your code for it to make a difference. At the moment, your code contains no reference to it apart from when you're assigning it to the axis. You probably want something like
.y(function(d) {
return y(d);
})
in the definition of your line generator.

How do I make a basic column(vertical) chart in d3js?

I'm trying out d3js and I have a problem with getting my first basic column(vertical bar) chart work. The only thing I find a bit difficult to understand is the scaling thing. I want to make the x and y axis ticks with labels but I have the following problems:
First of all here is my data:
{
"regions":
["Federal","Tigray","Afar","Amhara","Oromia","Gambella","Addis Ababa","Dire Dawa","Harar","Benishangul-Gumuz","Somali","SNNPR "],
"institutions":
[0,0,34,421,738,0,218,22,22,109,0,456]
}
On the y-axis the values are there but the order is reversed. Here is the code:
var y = d3.scale.linear().domain([0, d3.max(data.institutions)]).range([0, height]);
then I use this scale to create a y-axis:
var yAxis = d3.svg.axis().scale(y).orient("left");
and add this axis to the svg element
svgContainer.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Institutions");
the problem here is that the y-axis start from 0 at the top and with 700 at the bottom which is OK but it should be in reverse order.
The other problem I have it the x-axis. I want to have an ordinal scale since the values I want to put are in the regions names I have above. So here's what I've done.
var x = d3.scale.ordinal()
.domain(data.regions.map(function(d) { return d.substring(0, 2); }))
.rangeRoundBands([0, width], .1);
then the axis
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
and finally add it to the svg element
svgContainer.append("g")
.attr("class", "x axis")
.attr("transform", "translate( 0," + height + ")")
.call(xAxis);
Here the problem is the ticks as well as the labels appear but they are not spaced out evenly and do not correspond with the center of the rectangles I'm drawing. Here is the complete code so you can see what's happening.
$(document).ready(function(){
d3.json("institutions.json", draw);
});
function draw(data){
var margin = {"top": 10, "right": 10, "bottom": 30, "left": 50}, width = 700, height = 300;
var x = d3.scale.ordinal()
.domain(data.regions.map(function(d) { return d.substring(0, 2); }))
.rangeRoundBands([0, width], .1);
var y = d3.scale.linear()
.domain([0, d3.max(data.institutions)])
.range([0, height]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var svgContainer = d3.select("div.container").append("svg")
.attr("class", "chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" +margin.left+ "," +margin.right+ ")");
svgContainer.append("g")
.attr("class", "x axis")
.attr("transform", "translate( 0," + height + ")")
.call(xAxis);
svgContainer.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Institutions");
svgContainer.selectAll(".bar")
.data(data.institutions)
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", function(d, i) {return i* 41;})
.attr("y", function(d){return height - y(d);})
.attr("width", x.rangeBand())
.attr("height", function(d){return y(d);});
}
I put the code to Fiddle: http://jsfiddle.net/GmhCr/4/
Feel free to edit it! I already fixed both problems.
To fix the upside-down y-axis just swap the values of the range function arguments:
var y = d3.scale.linear().domain([0, d3.max(data.institutions)]).range([height, 0]);
Do not forget to adjust the code for the bars if you change the scale!
The source of the mismatch between bars and the x-axis can be found here:
var x = d3.scale.ordinal()
.domain(data.regions.map(function(d) {
return d.substring(0, 2);}))
.rangeRoundBands([0, width], .1);
svgContainer.selectAll(".bar")
.data(data.institutions)
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", function(d, i) {return i* 41;})
.attr("y", function(d){return height - y(d);})
.attr("width", x.rangeBand())
.attr("height", function(d){return y(d);});
You specify the padding for rangeRoundBands at 0.1 but you ignore the padding when computing the x and width values for the bars. This for example is correct with a padding of 0:
var x = d3.scale.ordinal()
.domain(data.regions.map(function(d) {
return d.substring(0, 2);}))
.rangeRoundBands([0, width], 0);
svgContainer.selectAll(".bar").data(data.institutions).enter().append("rect")
.attr("class", "bar")
.attr("x", function(d, i) {
return i * x.rangeBand();
})
.attr("y", function(d) {
return y(d);
})
.attr("width", function(){
return x.rangeBand();
})
.attr("height", function(d) {
return height -y(d);
});
The padding determines how much of the domain is reserved for padding. When using a width of 700 and a padding of 0.1 exactly 70 pixels are used for padding. This means you have to add 70 / data["regions"].length pixels to every bar's x value to make this work with a padding.

d3 axis labeling

How do I add text labels to axes in d3?
For instance, I have a simple line graph with an x and y axis.
On my x-axis, I have ticks from 1 to 10. I want the word "days" to appear underneath it so people know the x axis is counting days.
Similarly, on the y-axis, I have the numbers 1-10 as ticks, and I want the words "sandwiches eaten" to appear sideways.
Is there a simple way to do this?
Axis labels aren't built-in to D3's axis component, but you can add labels yourself simply by adding an SVG text element. A good example of this is my recreation of Gapminder’s animated bubble chart, The Wealth & Health of Nations. The x-axis label looks like this:
svg.append("text")
.attr("class", "x label")
.attr("text-anchor", "end")
.attr("x", width)
.attr("y", height - 6)
.text("income per capita, inflation-adjusted (dollars)");
And the y-axis label like this:
svg.append("text")
.attr("class", "y label")
.attr("text-anchor", "end")
.attr("y", 6)
.attr("dy", ".75em")
.attr("transform", "rotate(-90)")
.text("life expectancy (years)");
You can also use a stylesheet to style these labels as you like, either together (.label) or individually (.x.label, .y.label).
In the new D3js version (version 3 onwards), when you create a chart axis via d3.svg.axis() function you have access to two methods called tickValues and tickFormat which are built-in inside the function so that you can specifies which values you need the ticks for and in what format you want the text to appear:
var formatAxis = d3.format(" 0");
var axis = d3.svg.axis()
.scale(xScale)
.tickFormat(formatAxis)
.ticks(3)
.tickValues([100, 200, 300]) //specify an array here for values
.orient("bottom");
If you want the y-axis label in the middle of the y-axis like I did:
Rotate text 90 degrees with text-anchor middle
Translate the text by its midpoint
x position: to prevent overlap of y-axis tick labels (-50)
y position: to match the midpoint of the y-axis (chartHeight / 2)
Code sample:
var axisLabelX = -50;
var axisLabelY = chartHeight / 2;
chartArea
.append('g')
.attr('transform', 'translate(' + axisLabelX + ', ' + axisLabelY + ')')
.append('text')
.attr('text-anchor', 'middle')
.attr('transform', 'rotate(-90)')
.text('Y Axis Label')
;
This prevents rotating the whole coordinate system as mentioned by lubar above.
If you work in d3.v4, as suggested, you can use this instance offering everything you need.
You might just want to replace the X-axis data by your "days" but remember to parse string values correctly and not apply concatenate.
parseTime might as well do the trick for days scaling with a date format ?
d3.json("data.json", function(error, data) {
if (error) throw error;
data.forEach(function(d) {
d.year = parseTime(d.year);
d.value = +d.value;
});
x.domain(d3.extent(data, function(d) { return d.year; }));
y.domain([d3.min(data, function(d) { return d.value; }) / 1.005, d3.max(data, function(d) { return d.value; }) * 1.005]);
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y).ticks(6).tickFormat(function(d) { return parseInt(d / 1000) + "k"; }))
.append("text")
.attr("class", "axis-title")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.attr("fill", "#5D6971")
.text("Population)");
fiddle with global css / js
D3 provides a pretty low-level set of components that you can use to assemble charts. You are given the building blocks, an axis component, data join, selection and SVG. It's your job to put them together to form a chart!
If you want a conventional chart, i.e. a pair of axes, axis labels, a chart title and a plot area, why not have a look at d3fc? it is an open source set of more high-level D3 components. It includes a cartesian chart component that might be what you need:
var chart = fc.chartSvgCartesian(
d3.scaleLinear(),
d3.scaleLinear()
)
.xLabel('Value')
.yLabel('Sine / Cosine')
.chartLabel('Sine and Cosine')
.yDomain(yExtent(data))
.xDomain(xExtent(data))
.plotArea(multi);
// render
d3.select('#sine')
.datum(data)
.call(chart);
You can see a more complete example here: https://d3fc.io/examples/simple/index.html
chart.xAxis.axisLabel('Label here');
or
xAxis: {
axisLabel: 'Label here'
},

Categories