D3 x-axis for each day of the week - javascript

I have a JSON array like the following:
[
{
"day": "Monday",
"sales": 242
},
{
"day": "Tuesday",
"sales": 256
},
...
]
This data covers one week, so there is an object for each day Monday through Sunday.
I have built a bar graph with D3 with a bar for each day of the week. I am now attempting to add an x-axis with a tick label for each day.
I've done the following to set up my y-axis:
var yScale = d3.scaleLinear()
.domain([0, d3.max(data, function(d) {
return d.sales;
})])
.range([height, 0]);
var yAxis = d3.axisLeft(yScale)
.ticks(5);
svg.append("g")
.call(yAxis);
This works great, but for some reason, I am stumped on how to setup my x-axis with each day of the week under the corresponding bar. Note: I am using D3 version 4.

Once your days are just strings, you can use scaleBand instead of scaleTime:
var xScale = d3.scaleBand()
.domain(data.map(function(d){ return d.day}))
.range([0, width])//you can use rangeRound instead
.paddingInner(someValue);//the value of the inner padding, if any.
Then, set the x axis accordingly:
var xAxis = d3.axisBottom(xScale);
Check this snippet:
var width = 550, height = 200;
var data = [{day:"Monday"},
{day:"Tuesday"},
{day:"Wednesday"},
{day:"Thursday"},
{day:"Friday"},
{day:"Saturday"},
{day:"Sunday"}
];
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var xScale = d3.scaleBand()
.domain(data.map(function(d){ return d.day}))
.range([0, width*0.95])
var xAxis = d3.axisBottom(xScale)
.ticks(7);
svg.append("g")
.attr("transform", "translate(0,100)")
.call(xAxis);
text { font-size: 12px;}
<script src="https://d3js.org/d3.v4.min.js"></script>

Related

Grid lines in d3 js don't match the axes

I am trying to make a multi-line chart with d3.js in react. The plot looks fine and comes up well, but the gridlines are not aligned sometimes. It is very random, and sometimes some graphs have aligned gridlines, some don't.
This is how some of them look:
I have this code for my gridlines:
svg
.append('g')
.attr('class', 'grid')
.attr('transform', `translate(0,${height})`)
.call(
d3.axisBottom(xScale)
.tickSize(-height)
.tickFormat(() => ""),
);
svg
.append('g')
.attr('class', 'grid')
.call(
d3.axisLeft(yScale)
.tickSize(-width)
.tickFormat(() => ""),
);
I followed this example: https://betterprogramming.pub/react-d3-plotting-a-line-chart-with-tooltips-ed41a4c31f4f
Any help on how I can align those lines perfectly would be appreciated.
You may consider niceing your y-scale so that minima and maxima of your data sets are rounded down/ up such that the ticks are equally spaced.
In your tutorial this bit of code:
const yScale = d3
.scaleLinear()
.range([height, 0])
.domain([0, yMaxValue]);
Can become:
const yScale = d3
.scaleLinear()
.range([height, 0])
.domain([0, yMaxValue])
.nice(); // <--------------------------- here
Here's a basic example of using nice on an x-scale where the first example is 'not nice' and the second is 'nice'.
// note gaps of 10 between data points
// apart from first and last where gap is different
const data = [3, 4, 14, 24, 34, 44, 47];
// svg
const margin = 20;
const width = 360;
const height = 140;
const svg = d3.select("body")
.append("svg")
.attr("width", width + margin + margin)
.attr("height", height + margin + margin);
// scale without 'nice'
const xScale1 = d3.scaleLinear()
.range([0, width])
.domain(d3.extent(data));
// scale with nice
const xScale2 = d3.scaleLinear()
.range([0, width])
.domain(d3.extent(data))
.nice();
// plot axes with both scales for comparison
// not 'nice'
svg.append("g")
.attr("transform", `translate(${margin},${margin})`)
.call(d3.axisBottom(xScale1));
// 'nice'
svg.append("g")
.attr("transform", `translate(${margin},${margin + 50})`)
.call(d3.axisBottom(xScale2));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

D3: scale data on the x-axis every 7 years

I have a d3 project where I want to include all my dates but only on certain intervals. Right now it displays everything which is too cluttered. I only want to display the labels on the x axis every 7 years. so for example 1947, 1954, 1961, 1968, etc. Pease help and thank you in advance.
Here is my code:
loadData = ()=> {
req = new XMLHttpRequest();
req.open("GET", "https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json" , true);
req.send();
req.onload= ()=>{
json = JSON.parse(req.responseText);
//dynmaic height
/*var margin = {top: 20, right: 200, bottom: 0, left: 20},
width = 300,
height = datajson.length * 20 + margin.top + margin.bottom;*/
//create measurements
const margin = 60
const width = 1000 - margin;
const height = 600 - margin;
const maxYScale = d3.max(json.data, (d) => d[1]);
//date formatter
const formatDate = d3.timeParse("%Y-%m-%d"); //convert from string to date format
const parseDate = d3.timeFormat("%Y"); //format date to cstring
//create svg
const svg = d3.select("svg");
const chart = svg.append("g")
.attr("transform", `translate(${margin}, ${margin})`);
//y-axis: split charts into 2 equal parts using scaling function
const yScale = d3.scaleLinear()
.range([height, 0]) //length
.domain([0, maxYScale]); //content
//create x-axis
const yAxis = d3.axisLeft(yScale);
//append y-axis
chart.append("g")
.call(yAxis);
//create x-scale
const xScale = d3.scaleBand()
.range([0, width]) //length
//.domain(json.data.filter((date, key) => { return (key % 20 === 0)}).map((d)=> parseDate(formatDate(d[0]))))
.domain(json.data.map((d)=> parseDate(formatDate(d[0]))))
.padding(0.2);
//create x-axis
const xAxis = d3.axisBottom(xScale);
//append x-axis
chart.append("g")
.attr(`transform`, `translate(0, ${height})`)
.call(xAxis);
//make bars
chart.selectAll("rect")
.data(json.data)
.enter()
.append("rect")
.attr("x", (d) => xScale(parseDate(formatDate(d[0]))))
.attr("y", (d) => yScale(d[1]))
.attr("height", (d) => height - yScale(d[1]))
.attr("width", xScale.bandwidth())
}
}
loadData();
Here is my codepen:
codepen
I am just going to answer my own question as I found the solution. In order to set intervals in the x axis I simply used tickValues. Then I used my scale and a filter function to filter the intervals based on the data I had. Below you may find the answer.
const xAxis = d3.axisBottom(xScale)
.tickValues(xScale.domain().filter(function(d) { return (d % 7 === 0)}));

scaleTime minimum tick value

Using d3 I have an xAxis defined as
var xScale = d3.scaleTime()
.domain([fromDate, toDate])
.range([0, width]);
var xAxis = d3.axisBottom(xScale);
function customXAxis(g) {
g.call(xAxis);
g.select(".domain").remove();
g.selectAll(".tick line").attr("stroke", "white");
g.selectAll(".tick text").attr("fill", "white");
}
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(customXAxis);
All is working very well indeed.
However, I would like the scale to show ticks at a minimum of a day. As it stands if fromData and toDate are only a few days difference it show ticks....
Mon-10___12PM___Tue-11___12PM___Wed-13___12PM___Thu-14___12PM
How can I get it to not show the time of day values?
Any help appreciated.
You can set the intervals in the axis generator:
Constructs a new custom interval given the specified floor and offset functions and an optional count function.
For instance, this is your code as it is (the domain here has just 3 days):
var svg = d3.select("svg");
var xScale = d3.scaleTime()
.domain([new Date("January 1, 2017 00:00:00"), new Date("January 4, 2017 00:00:00")])
.range([20, 480]);
var xAxis = d3.axisBottom(xScale);
svg.append("g")
.attr("transform", "translate(0,100)")
.call(xAxis);
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="500"></svg>
Now the same code, with d3.timeDay:
var svg = d3.select("svg");
var xScale = d3.scaleTime()
.domain([new Date("January 1, 2017 00:00:00"), new Date("January 4, 2017 00:00:00")])
.range([20, 480]);
var xAxis = d3.axisBottom(xScale)
.ticks(d3.timeDay)
.tickFormat(d=>d3.timeFormat("%a %d")(d));
svg.append("g")
.attr("transform", "translate(0,100)")
.call(xAxis);
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="500"></svg>
PS: the tickFormat is just to change the first tick, don't pay attention to it.

Dynamic tickValues on time.scale in D3

I built a graph in D3, to show spending based on date time and amount of spending, but I can't dynamically set tickValues() on my time.scale to show only ticks for data points.
My scale set up is:
var dateScale = d3.time.scale()
.domain(d3.extent(newData, function(d) { return d.Date; }))
.range([padding, width - padding]);
var amountScale = d3.scale.linear()
.domain([0, d3.max(newData, function(d) { return d.Amount; })])
.range([window.height - padding, padding]);
// Define date Axis
var dateAxis = d3.svg.axis().scale(dateScale)
.tickFormat(d3.time.format('%a %d %m'))
.tickSize(100 - height)
.orient("bottom");
// Draw date Axis
svg.append("g")
.attr({
"class": "axis date-axis",
"transform": "translate(" + [0, height -padding] + ")"
}).call(dateAxis);
// Define amount Axis
var amountAxis = d3.svg.axis().scale(amountScale)
.tickSize(1)
.orient("left");
// Draw amount Axis
svg.append("g")
.attr({
"class": "axis amount-axis",
"transform": "translate(" + padding + ",0)"
}).call(amountAxis);
I've tried to set
dateAxis.tickValues(d3.extent(newData, function(d) { return d.Date; }))
but this only returns min and max value for date axis.
Also tickValues() doesn't accept newData itself - what else I can try here?
I want to achieve this:
to have only ticks highlighted, associated with corresponding data.
I ended up with creating new array of all date points out of my data, to pass it to tickValues() method. My code now is:
var newData = toArray(uniqueBy(data, function(x){return x.Date;}, function(x, y){ x.Amount += y.Amount; return x; }));
// Create array of data date points
var tickValues = newData.map(function(d){return d.Date;});
// Sorting data ascending
newData = newData.sort(sortByDateAscending);
var dateScale = d3.time.scale()
.domain(d3.extent(newData, function(d) { return d.Date; }))
.range([padding, width - padding]);
var amountScale = d3.scale.linear()
.domain([0, d3.max(newData, function(d) { return d.Amount; })])
.range([window.height - padding, padding]);
// Define date Axis
var dateAxis = d3.svg.axis().scale(dateScale)
.tickFormat(d3.time.format('%d %m %Y'))
.tickValues(tickValues)
.orient("bottom");
So now whole graph looks as:
I found this SO (#AmeliaBR) answer explaining tickValues() function, and here is documentation for it too.

d3 can I recalculate data in place?

I have a line chart and the full array of data is attached to the line. I want to change from using the value column to the pct (percent) column in the data. Is there a way of doing this in place, ie. using the values already in the DOM without passing it a new set of data?
as far as I've got - http://bl.ocks.org/3099307
var width = 700, // width of svg
height = 400, // height of svg
padding = 100; // space around the chart, not including labels
var data=[{"date":new Date(2012,0,1), "value": 3, 'pct': 55},
{"date":new Date(2012,0,3), "value": 2, "pct": 30 },
{"date":new Date(2012,0,12), "value": 33, "pct": 10},
{"date":new Date(2012,0,21), "value": 13, "pct": 29},
{"date":new Date(2012,0,30), "value": 23, "pct": 22}];
var x_domain = d3.extent(data, function(d) {
return d.date; }),
y_domain = d3.extent(data, function(d) { return d.value; });
// define the y scale (vertical)
var yScale = d3.scale.linear()
.domain(y_domain)
.range([height - padding, padding]); // map these top and bottom of the chart
var xScale = d3.time.scale()
.domain(x_domain)
.range([padding, width - padding]); // map these sides of the chart, in this case 100 and 600
// define the y axis
var yAxis = d3.svg.axis()
.orient("left")
.scale(yScale);
// define the x axis
var xAxis = d3.svg.axis()
.orient("bottom")
.scale(xScale);
// create the svg
var div = d3.select("body");
div.select("svg").remove();
var vis = div.append("svg")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + padding + "," + padding + ")");
// draw y axis with labels and move in from the size by the amount of padding
vis.append("g")
.attr("class", "axis yaxis")
.attr("transform", "translate("+padding+",0)")
.call(yAxis);
// draw x axis with labels and move to the bottom of the chart area
vis.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (height - padding) + ")")
.call(xAxis);
// DRAW LINES
var line = d3.svg.line()
.x(function(d) {
return xScale(d.date); })
.y(function(d) {
return yScale(d.value); })
.interpolate("basis");
vis.selectAll(".lines")
.data([data])
.enter()
.append("svg:path")
.attr("d", line)
.attr("class", "lines");
function rescale() {
// change the y axis to show percentage
yScale.domain([0,100]) // redraw as percentage outstanding
vis.select(".yaxis")
.transition().duration(1500).ease("sin-in-out") // https://github.com/mbostock/d3/wiki/Transitions#wiki-d3_ease
.call(yAxis);
What happens here?
// now redraw the line to use pct
line.y(function(d) {
return yScale(d.pct); });
vis.selectAll("lines")
.transition()
.duration(500)
.ease("linear");
}
Your data is already joined, so you just need to update your selection:
var yPctScale = d3.scale.linear()
.domain([0, 100])
.range([height - padding, padding]);
var pct_line = d3.svg.line()
.x(function(d) {
return xScale(d.date); })
.y(function(d) {
return yPctScale(d.pct); })
.interpolate("basis");
vis.selectAll(".lines")
.transition().duration(1500).ease("sin-in-out")
.attr("d", pct_line);

Categories