d3.timeMonth on axis is showing every date, instead of every month - javascript

I am newish to D3, and I've been trying to troubleshoot this issue for a few days now without luck. I'm not sure what to try next.
I have a JSON dataset with daily data, and I'm attempting to make a bar chart with one bar per day. That's all good. However, I'm having trouble with the x-axis. I'd like the x-axis to have ticks and labels only at the first of each month. It's as if d3.timeMonth thinks every data point is a new month:
I've set up the x-axis as scaleBand, because every time I tried to set it up as scaleTime, the bars displayed as huge overlapping bars. However, just before I set up the x-axis, I've printed my data to the console log, and it looks correctly formatted as Dates.
const data = [
{
date_facet: '2020-08-31',
published: 2,
not_published: 0,
},
{
date_facet: '2020-09-01',
published: 0,
not_published: 0,
},
{
date_facet: '2020-09-02',
published: 1,
not_published: 0,
},
{
date_facet: '2020-09-03',
published: 1,
not_published: 0,
},
{
date_facet: '2020-09-04',
published: 0,
not_published: 0,
},
{
date_facet: '2020-09-05',
published: 0,
not_published: 0,
},
];
// set the dimensions and margins of the graph
var margin = {
top: 10,
right: 30,
bottom: 80,
left: 40
},
width = 450 - margin.left - margin.right,
height = 350 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3.select("#graph")
.append("svg")
.attr("viewBox", '0 0 450 350')
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// parse the date / time
var parseTime = d3.timeParse("%Y-%m-%d");
// format the data
data.forEach(function(d) {
d.date_facet = parseTime(d.date_facet);
d.published = +d.published;
});
// order the data
data.sort(function(a, b) {
return a["date_facet"] - b["date_facet"];
})
// X axis
var x = d3.scaleBand()
.range([0, width])
.domain(data.map(function(d) {
return d.date_facet;
}))
.padding(0.2);
// Y axis
var y = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(data, function(d) {
return Math.max(d.published);
}) + 4]);
// Add X axis, ticks and labels
svg.append("g")
.attr("class", "axis axis-minor")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x)
.ticks(d3.timeMonth.every(1))
.tickFormat(d3.timeFormat("%b")))
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-45)");
svg.append("g")
.call(d3.axisLeft(y));
// Bars
svg.selectAll("mybar")
.data(data)
.enter()
.append("rect")
.attr("x", function(d) {
return x(d.date_facet);
})
.attr("width", x.bandwidth())
.attr("height", function(d) {
return height - y(d.published);
})
.attr("y", function(d) {
return y(d.published);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<div id="graph"></div>

Because you use scaleBand, all values are considered categorical. What I mean by that is that they're like labels, like "ball", "orange", "circle". Just terms, completely unrelated to each other. This contrasts with time or numbers, where you can say one value is bigger than the other, or one is closer to A than to B.
Change the values to scaleTime instead:
const data = [
{
date_facet: '2020-08-31',
published: 2,
not_published: 0,
},
{
date_facet: '2020-09-01',
published: 0,
not_published: 0,
},
{
date_facet: '2020-09-02',
published: 1,
not_published: 0,
},
{
date_facet: '2020-09-03',
published: 1,
not_published: 0,
},
{
date_facet: '2020-09-04',
published: 0,
not_published: 0,
},
{
date_facet: '2020-09-05',
published: 0,
not_published: 0,
},
];
// set the dimensions and margins of the graph
var margin = {
top: 10,
right: 30,
bottom: 80,
left: 40
},
width = 450 - margin.left - margin.right,
height = 350 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3.select("#graph")
.append("svg")
.attr("viewBox", '0 0 450 350')
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// parse the date / time
var parseTime = d3.timeParse("%Y-%m-%d");
// format the data
data.forEach(function(d) {
d.date_facet = parseTime(d.date_facet);
d.published = +d.published;
});
// order the data
data.sort(function(a, b) {
return a["date_facet"] - b["date_facet"];
})
// Extend the domain by 12 hours on each side to account for the bar widths
var xDomain = d3.extent(data.map(function(d) {
return d.date_facet;
}));
// Deep copy the date objects to make sure you can make safe modifications
xDomain = [new Date(xDomain[0]), new Date(xDomain[1])];
xDomain[0].setHours(xDomain[0].getHours() - 12);
xDomain[1].setHours(xDomain[1].getHours() + 12);
// X axis
var x = d3.scaleTime()
.range([0, width])
.domain(xDomain);
var xDomainInDays = (x.domain()[1] - x.domain()[0]) / (1000 * 60 * 60 * 24);
var xBarWidth = width / xDomainInDays;
var padding = 0.2;
// Y axis
var y = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(data, function(d) {
return Math.max(d.published);
}) + 4]);
// Add X axis, ticks and labels
svg.append("g")
.attr("class", "axis axis-minor")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x)
.ticks(d3.timeMonth.every(1))
.tickFormat(d3.timeFormat("%b")))
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-45)");
svg.append("g")
.call(d3.axisLeft(y));
// Bars
svg.selectAll("mybar")
.data(data)
.enter()
.append("rect")
.attr("x", function(d) {
// Get the x coordinate
// Then shift by half of xBarWidth so the middle of the bar is at the tick
// Then apply half of the padding (other half at the other side)
return x(d.date_facet) - (xBarWidth / 2) + (padding / 2) * xBarWidth;
})
// Make the bar "padding * xBarWidth" thinner so it applies the padding correctly
.attr("width", xBarWidth - padding * xBarWidth)
.attr("height", function(d) {
return height - y(d.published);
})
.attr("y", function(d) {
return y(d.published);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<div id="graph"></div>
This does come with some complications.
You need to calculate the bar width yourself. I did this by checking the size of the domain and the size of the range, so I found the width available to each bar that way;
The tick will be at the left edge of each bar. If you want to centre it (which I did here), you need to play with padding and centre the bar on the tick;
Now, the bar will exceed the axis by a bit. You can make the domain 12 hours larger in both directions, this solves the issue.

Related

d3 area stacked line chart

I'm working on modifying this stacked line chart example: https://bl.ocks.org/d3indepth/e4efd402b4d9fdb2088ccdf3135745c3
I'm adding a time x axis, but I'm struggling with this block of code:
var areaGenerator = d3.area()
.x(function(d, i) {
// return i * 100;
return i * 253.5;
})
.y0(function(d) {
return y(d[0]);
})
.y1(function(d) {
return y(d[1]);
});
The original example has the .x accessor as i * 100 which seems to be a random value. When I add the X axis the stacked line chart does not line up correctly with the date ticks. I can manually force it to line up by returning i * 253.5 but that is not ideal. I don't really understand how this area function is working - any help would be appreciated.
let height = 600;
let width = 800;
const yMax = 4000;
//var hEach = 40;
let margin = {top: 20, right: 15, bottom: 25, left: 25};
width = width - margin.left - margin.right;
height = height - margin.top - margin.bottom;
var svg = d3.select('body').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 + ")");
let formatDate = d3.timeFormat("%b-%Y")
let parseTime = d3.timeParse("%Y-%m-%d");
let data = [
{
"host_count": 2553,
"container_count": 875,
"hour": "2019-01-31",
"apm_host_count": 0,
"agent_host_count": 2208,
"gcp_host_count": 0,
"aws_host_count": 345
},
{
"host_count": 1553,
"container_count": 675,
"hour": "2019-02-01",
"apm_host_count": 0,
"agent_host_count": 1208,
"gcp_host_count": 0,
"aws_host_count": 445
},
{
"host_count": 716,
"container_count": 6234,
"hour": "2019-02-02",
"apm_host_count": 0,
"agent_host_count": 479,
"gcp_host_count": 0,
"aws_host_count": 237
},
{
"host_count": 516,
"container_count": 4234,
"hour": "2019-02-03",
"apm_host_count": 0,
"agent_host_count": 679,
"gcp_host_count": 0,
"aws_host_count": 137
}
];
// format the data
data.forEach(function(d) {
d.hour = parseTime(d.hour);
});
// set the ranges
var x = d3.scaleTime().range([0, width]);
x.domain(d3.extent(data, function(d) { return d.hour; }));
var xAxis = d3.axisBottom(x).ticks(11).tickFormat(d3.timeFormat("%y-%b-%d")).tickValues(data.map(d=>d.hour));
var y = d3.scaleLinear()
.domain([0, yMax])
.range([height, 0]);
var areaGenerator = d3.area()
.x(function(d, i) {
console.log(d);
return i * 100;
})
.y0(function(d) {
return y(d[0]);
})
.y1(function(d) {
return y(d[1]);
});
var colors = ['#FBB65B', '#513551', '#de3163']
var stack = d3.stack()
.keys(['agent_host_count', 'aws_host_count', 'container_count']);
var stackedSeries = stack(data);
d3.select('g')
.selectAll('path')
.data(stackedSeries)
.enter()
.append('path')
.style('fill', function(d, i) {
return colors[i];
})
.attr('d', areaGenerator)
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
<!doctype html>
<html lang="en">
<head>
<title>Usage</title>
</head>
<body>
<div id="svg"></div>
<script src="https://d3js.org/d3.v5.min.js"></script>
</body>
</html>
When using axes in d3, you actually need to use the axis variable to calculate scaling factors. The functions that do this calculation are returned by the scale*() methods. In your code you have this for the x-axis:
var x = d3.scaleTime().range([0, width]);
As such, the variable x now contains a function that will do interpolation for you. this is what your areaGenerator function should look like:
var areaGenerator = d3.area()
.x(function(d, i) {
return x(d.data.hour);
})
.y0(function(d) {
return y(d[0]);
})
.y1(function(d) {
return y(d[1]);
});
The only thing you need to remember is that when calculating the value you need to use the same variable that the axis is based on. I.e. your x-axis is a time axis so you need to calculate the interpolation using the time variable (d.data.hour).
As to where 100 comes from in the example, you are essentially correct. In that block the value of 100 is more or less arbitrary. It was likely chosen because the chart looks reasonably good at that scale. By choosing 100, each "tick" is spaced 100px apart and since there is no x-axis to be judged against it doesn't actually matter what is used as long as it changes for each data point.

How to align x axis with bars respectively in Combo Chart (Bars and Lines)?

I have a combo/ bars & lines chart based on D3.js. The x axis domain contains min and max dates, and bars are based on values. But the last bar (rect) is outside the chart. I can bring it in by forcing it (manually) but it won't reflect the data.
var data = [
{
fcst_valid_local: "2018-11-13T14:00:00-0600",
pop: 20,
rh: 67,
temp: 38,
wspd: 7
},
{
fcst_valid_local: "2018-11-14T15:00:00-0600",
pop: 15,
rh: 50,
temp: 39,
wspd: 8
},
{
fcst_valid_local: "2018-11-15T16:00:00-0600",
pop: 10,
rh: 90,
temp: 40,
wspd: 9
}
];
// Margins, width and height.
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = 500 - margin.left - margin.right,
height = 200 - margin.top - margin.bottom;
// Date parsing.
const parseDate = d3.timeParse("%Y-%m-%dT%H:%M:%S%Z");
data.forEach(function (d) {
d.date = parseDate(d.fcst_valid_local);
});
// Set scale domains.
var x = d3.scaleTime().range([0, width])
.domain(d3.extent(data, function (d) {
return d.date;
}));
var y0 = d3.scaleLinear().range([height, 0]).domain([0, 100]);
const y1 = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(data, (d) => d.pop)]);
// Construct our SVG object.
const svg = d3.select('svg')
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append('g').attr('class', 'container')
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Set x, y-left and y-right axis.
var xAxis = d3.axisBottom(x)
.ticks(d3.timeDay.every(1))
// .tickFormat(d3.timeFormat('%b %d, %H:%M'))
.tickSize(0).tickPadding(10);
var y0Axis = d3.axisLeft(y0)
.ticks(5).tickSize(0);
var y1Axis = d3.axisRight(y1).ticks(5).tickSize(0);
svg.append("g")
.attr("class", "x-axis axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y-axis axis")
.attr("transform", "translate(" + 0 + ", 0)")
.call(y0Axis);
svg.append("g")
.attr("class", "y-axis axis")
.attr("transform", "translate(" + width + ", 0)")
.call(y1Axis);
// Draw bars.
var bars = svg.selectAll(".precips")
.data(data);
bars.exit().remove();
bars.enter().append("rect")
.attr("class", "precip")
.attr("width", width / data.length - 50)
.attr("x", function (d) {
return x(d.date);
})
.attr("y", height)
.transition().duration(1000)
.attr("y", function (d) {
return y0(d.pop);
})
.attr("height", function (d) {
return height - y0(d.pop);
});
const lineRH = d3.line()
.x((d) => x(d['date']))
.y(d => y0(d['rh']));
svg.append('path')
.datum(data)
.attr('class', 'line')
.attr('fill', 'none')
.attr('stroke', 'red')
.attr('stroke-linejoin', 'round')
.attr('stroke-linecap', 'round')
.attr('stroke-width', 1.5)
.attr('d', lineRH);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
Although an answer has been accepted, I'd like to let you know that you don't have to manipulate the data (as it might be fetched from an API as well) but you can play around the x.domain() as it's all about setting the right domain here.
Try using d3 time_nice to round off the time scale domains
Play around with d3 time methods to change the dates (there are a lot here)
Here's an example of using the second approach from above and setting the x domain:
var x = d3.scaleTime().range([0, width])
.domain([d3.min(data, function (d) {
return d.date;
}), d3.timeDay.offset(d3.max(data, function (d) { return d.date; }), 1)]);
Explanation: This is offsetting the max date from the data by 1 day and so the new x.domain() would come out as:
(2) [Tue Nov 13 2018 15:00:00 GMT-0500 (Eastern Standard Time), Fri Nov 16 2018 17:00:00 GMT-0500 (Eastern Standard Time)]
which results in a chart as follows:
var data = [
{
fcst_valid_local: "2018-11-13T14:00:00-0600",
pop: 20,
rh: 67,
temp: 38,
wspd: 7
},
{
fcst_valid_local: "2018-11-14T15:00:00-0600",
pop: 15,
rh: 50,
temp: 39,
wspd: 8
},
{
fcst_valid_local: "2018-11-15T16:00:00-0600",
pop: 10,
rh: 90,
temp: 40,
wspd: 9
}
];
// Margins, width and height.
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = 500 - margin.left - margin.right,
height = 200 - margin.top - margin.bottom;
// Date parsing.
const parseDate = d3.timeParse("%Y-%m-%dT%H:%M:%S%Z");
data.forEach(function (d) {
d.date = parseDate(d.fcst_valid_local);
});
// Set scale domains.
var x = d3.scaleTime().range([0, width])
.domain([d3.min(data, function (d) {
return d.date;
}), d3.timeDay.offset(d3.max(data, function (d) { return d.date; }), 1)]);
var y0 = d3.scaleLinear().range([height, 0]).domain([0, 100]);
const y1 = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(data, (d) => d.pop)]);
//console.log(x.domain());
// Construct our SVG object.
const svg = d3.select('svg')
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append('g').attr('class', 'container')
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Set x, y-left and y-right axis.
var xAxis = d3.axisBottom(x)
.ticks(d3.timeDay.every(1))
// .tickFormat(d3.timeFormat('%b %d, %H:%M'))
.tickSize(0).tickPadding(10);
var y0Axis = d3.axisLeft(y0)
.ticks(5).tickSize(0);
var y1Axis = d3.axisRight(y1).ticks(5).tickSize(0);
svg.append("g")
.attr("class", "x-axis axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y-axis axis")
.attr("transform", "translate(" + 0 + ", 0)")
.call(y0Axis);
svg.append("g")
.attr("class", "y-axis axis")
.attr("transform", "translate(" + width + ", 0)")
.call(y1Axis);
// Draw bars.
var bars = svg.selectAll(".precips")
.data(data);
bars.exit().remove();
bars.enter().append("rect")
.attr("class", "precip")
.attr("width", width / data.length - 50)
.attr("x", function (d) {
return x(d.date);
})
.attr("y", height)
.transition().duration(1000)
.attr("y", function (d) {
return y0(d.pop);
})
.attr("height", function (d) {
return height - y0(d.pop);
});
const lineRH = d3.line()
.x((d) => x(d['date']) + (width / data.length - 50)/2)
.y(d => y0(d['rh']));
svg.append('path')
.datum(data)
.attr('class', 'line')
.attr('fill', 'none')
.attr('stroke', 'red')
.attr('stroke-linejoin', 'round')
.attr('stroke-linecap', 'round')
.attr('stroke-width', 1.5)
.attr('d', lineRH);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
I also tried with .nice() and a fun part would be to use the d3 time intervals within .nice(). Feel free to play around with those and let me know if you have any questions.
Also, I'm offsetting the line (path) by the barwidth/2 in the line generator fn.
d3.line()
.x((d) => x(d['date']) + (width / data.length - 50)/2)
Hope this helps as well.
Add a dummy data item that is a bit later then the last item
Here it is done hard coded but you can add it dynamic based on the date of the last item
var data = [
{
fcst_valid_local: "2018-11-13T14:00:00-0600",
pop: 20,
rh: 67,
temp: 38,
wspd: 7
},
{
fcst_valid_local: "2018-11-14T15:00:00-0600",
pop: 15,
rh: 50,
temp: 39,
wspd: 8
},
{
fcst_valid_local: "2018-11-15T16:00:00-0600",
pop: 10,
rh: 90,
temp: 40,
wspd: 9
},
{
fcst_valid_local: "2018-11-16T01:00:00-0600"
}
];
// Margins, width and height.
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = 500 - margin.left - margin.right,
height = 200 - margin.top - margin.bottom;
// Date parsing.
const parseDate = d3.timeParse("%Y-%m-%dT%H:%M:%S%Z");
data.forEach(function (d) {
d.date = parseDate(d.fcst_valid_local);
});
// Set scale domains.
var x = d3.scaleTime().range([0, width])
.domain(d3.extent(data, function (d) {
return d.date;
}));
var y0 = d3.scaleLinear().range([height, 0]).domain([0, 100]);
const y1 = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(data, (d) => d.pop)]);
// Construct our SVG object.
const svg = d3.select('svg')
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append('g').attr('class', 'container')
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Set x, y-left and y-right axis.
var xAxis = d3.axisBottom(x)
.ticks(d3.timeDay.every(1))
// .tickFormat(d3.timeFormat('%b %d, %H:%M'))
.tickSize(0).tickPadding(10);
var y0Axis = d3.axisLeft(y0)
.ticks(5).tickSize(0);
var y1Axis = d3.axisRight(y1).ticks(5).tickSize(0);
svg.append("g")
.attr("class", "x-axis axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y-axis axis")
.attr("transform", "translate(" + 0 + ", 0)")
.call(y0Axis);
svg.append("g")
.attr("class", "y-axis axis")
.attr("transform", "translate(" + width + ", 0)")
.call(y1Axis);
// Draw bars.
var bars = svg.selectAll(".precips")
.data(data);
bars.exit().remove();
bars.enter().append("rect")
.attr("class", "precip")
.attr("width", width / data.length - 50)
.attr("x", function (d) {
return x(d.date);
})
.attr("y", height)
.transition().duration(1000)
.attr("y", function (d) {
return y0(d.pop);
})
.attr("height", function (d) {
return height - y0(d.pop);
});
const lineRH = d3.line()
.x((d) => x(d['date']))
.y(d => y0(d['rh']));
svg.append('path')
.datum(data)
.attr('class', 'line')
.attr('fill', 'none')
.attr('stroke', 'red')
.attr('stroke-linejoin', 'round')
.attr('stroke-linecap', 'round')
.attr('stroke-width', 1.5)
.attr('d', lineRH);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>

D3.JS Y-axis label issue

To start, I am fairly new to D3.Js. I have spent the past week or so working on a D3.JS issue-specifically making a graph with a Y-axis label. However, I cannot get the graph exactly how I want. It is almost there but inverted or my data comes out wrong. Now I will briefly show some of my code and images of my main problem before showing all of the code. I have spent time looking at other Stack Overflow posts with a similar issue and I do what is on those posts and still have the same issue.
For example, I thought that this post would have the solution: reversed Y-axis D3
The data is the following:
[0,20,3,8] (It is actually an array of objects but I think this may be all that is needed.
So, to start, when the yScale is like this:
var yScale = d3.scaleLinear()
.domain([0, maxPound]) //Value of maxpound is 20
.range([0, 350]);
The bar chart looks like this:
As one can see the Y chart starts with zero at the top and 20 at the bottom-which at first I thought was an easy fix of flipping the values in the domain around to this:
var yScale = d3.scaleLinear()
.domain([0, maxPound]) //Value of maxpound is 20
.range([0, 350]);
I get this image:
In the second image the y-axis is right-20 is on top-Yay! But the graphs are wrong. 0 now returns a value of 350 pixels-the height of the SVG element. That is the value that 20 should be returning! If I try to switch the image range values, I get the same problem!
Now the code:
var w = 350;
var h = 350;
var barPadding = 1;
var margin = {top: 5, right: 200, bottom: 70, left: 25}
var maxPound = d3.max(poundDataArray,
function(d) {return parseInt(d.Pounds)}
);
//Y-Axis Code
var yScale = d3.scaleLinear()
.domain([maxPound, 0])
.range([0, h]);
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5);
//Creating SVG element
var svg = d3.select(".pounds")
.append('svg')
.attr("width", w)
.attr('height', h)
.append("g")
.attr("transform", "translate(" + margin.left + "," +
margin.top + ")");
svg.selectAll("rect")
.data(poundDataArray)
.enter()
.append("rect")
.attr('x', function(d, i){
return i * (w / poundDataArray.length);
})
.attr('y', function(d) {
return 350 - yScale(d.Pounds);
})
.attr('width', (w / 4) - 25)
.attr('height', function(d){
return yScale(d.Pounds);
})
.attr('fill', 'steelblue');
//Create Y axis
svg.append("g")
.attr("class", "axis")
.call(yAxis);
Thank you for any help! I believe that the error may be in the y or height values and have spent time messing around there with no results.
That is not a D3 issue, but an SVG feature: in an SVG, the origin (0,0) is at the top left corner, not the bottom left, as in a common Cartesian plane. That's why using [0, h] as the range makes the axis seem to be inverted... actually, it is not inverted: that's the correct orientation in an SVG. By the way, HTML5 Canvas has the same coordinates system, and you would have the same issue using a canvas.
So, you have to flip the range, not the domain:
var yScale = d3.scaleLinear()
.domain([0, maxPound])
.range([h, 0]);//the range goes from the bottom to the top now
Or, in your case, using the margins:
var yScale = d3.scaleLinear()
.domain([0, maxPound])
.range([h - margin.bottom, margin.top]);
Besides that, the math for the y position and height is wrong. It should be:
.attr('y', function(d) {
return yScale(d.Pounds);
})
.attr('height', function(d) {
return h - margin.bottom - yScale(d.Pounds);
})
Also, as a bonus tip, don't hardcode the x position and the width. Use a band scale instead.
Here is your code with those changes:
var poundDataArray = [{
Pounds: 10
}, {
Pounds: 20
}, {
Pounds: 5
}, {
Pounds: 8
}, {
Pounds: 14
}, {
Pounds: 1
}, {
Pounds: 12
}];
var w = 350;
var h = 350;
var barPadding = 1;
var margin = {
top: 5,
right: 20,
bottom: 70,
left: 25
}
var maxPound = d3.max(poundDataArray,
function(d) {
return parseInt(d.Pounds)
}
);
//Y-Axis Code
var yScale = d3.scaleLinear()
.domain([0, maxPound])
.range([h - margin.bottom, margin.top]);
var xScale = d3.scaleBand()
.domain(d3.range(poundDataArray.length))
.range([margin.left, w - margin.right])
.padding(.2);
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5);
//Creating SVG element
var svg = d3.select("body")
.append('svg')
.attr("width", w)
.attr('height', h)
.append("g")
.attr("transform", "translate(" + margin.left + "," +
margin.top + ")");
svg.selectAll("rect")
.data(poundDataArray)
.enter()
.append("rect")
.attr('x', function(d, i) {
return xScale(i);
})
.attr('y', function(d) {
return yScale(d.Pounds);
})
.attr('width', xScale.bandwidth())
.attr('height', function(d) {
return h - margin.bottom - yScale(d.Pounds);
})
.attr('fill', 'steelblue');
//Create Y axis
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + margin.left + ",0)")
.call(yAxis);
<script src="https://d3js.org/d3.v4.min.js"></script>

Unneeded white space before the 1st bar in D3 Stack chart

I am trying to populate a data set into D3's Bar chart data. I am using this example from the d3:
https://bl.ocks.org/mbostock/1134768
var causes = ["wounds", "other", "disease"];
var parseDate = d3.time.format("%m/%Y").parse;
var margin = {top: 20, right: 50, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var svg = d3.select("body").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 + ")");
d3.tsv("data.csv", function(error, crimea) {
if (error) throw error;
var layers = d3.layout.stack()(causes.map(function(c) {
return crimea.map(function(d) {
return {x: parseDate(d.date), y: +d[c]};
});
}));
var x = d3.scale.ordinal()
.domain([0,1])
.rangeRoundBands([0, width], 0.1, 0);
var y = d3.scale.linear()
.rangeRound([height, 0]);
var z = d3.scale.category10();
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickFormat(d3.time.format("%b"));
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
console.log(layers);
x.domain(layers[0].map(function(d) { return d.x; }));
y.domain([0, d3.max(layers[layers.length - 1], function(d) { return d.y0 + d.y; })]).nice();
var ticks = x.domain().filter(function(d,i){ return !(i%20); } );
xAxis.tickValues( ticks );
var layer = svg.selectAll(".layer")
.data(layers)
.enter().append("g")
.attr("class", "layer")
.style("fill", function(d, i) { return z(i); });
layer.selectAll("rect")
.data(function(d) { return d; })
.enter().append("rect")
.attr("x", function(d) { return x(d.x); })
.attr("y", function(d) { return y(d.y + d.y0); })
.attr("height", function(d) { return y(d.y0) - y(d.y + d.y0); })
.attr("width", x.rangeBand() - 1);
svg.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "axis axis--y")
.attr("transform", "translate(" + 0 + ",0)")
.call(yAxis);
});
This issue I am getting is that I have some white space which looks ugly. This space come before the 1st bar and also after the last bar of the chart. I have tried tweaking the x value of the bar, But I think that is not a good way to do.
This space does not come when the data set is small. But when dataset is large then this space comes up. How can I remove this space from the start and from end.
JSFiddle For the Above code is
https://jsfiddle.net/7qnngbdc/
See here --> https://github.com/mbostock/d3/wiki/Ordinal-Scales#ordinal_rangeRoundBands
"Note that rounding necessarily introduces additional outer padding which is, on average, proportional to the length of the domain. For example, for a domain of size 50, an additional 25px of outer padding on either side may be required. Modifying the range extent to be closer to a multiple of the domain length may reduce the additional padding."
After you've set the domain, try this -->
var mult = Math.max (1, Math.floor (width / x.domain().length));
x.rangeRoundBands ([0, (x.domain().length * mult)], 0.1, 0);
Changed in https://jsfiddle.net/7qnngbdc/1/

Ordinal scale's rangeRoundBands()

I'm building a bar chart that will have a varied number of data points. All the examples I've seen use an ordinal scale for the x axis, then call rangeRoundBands() to get the width for the bars.
I'm getting the error "Uncaught TypeError: Cannot read property '1' of undefined" when I try to called .rangeRoundBands() and as far as I can tell I'm setting the scale up properly.
http://jsfiddle.net/rLzbbec0/1/
var globOb = {
bar: {
height: 390,
width: 675,
innerHeight: 300,
innerWidth: 615,
margin: {
bottom: 70,
left: 40,
right: 20,
top: 20
}
},
barData: [
{ task: "meetingsTask", val: 2.5 },
{ task: "reportsTask", val: 3 },
{ task: "emailsTask", val: 2 },
{ task: "planningTask", val: 1.5 },
{ task: "clientsTask", val: 4 },
{ task: "dataAnalysisTask", val: 3 }
]};
//Set X Scale
var xScale = d3.scale.ordinal()
.rangeRoundBands([0, globOb.bar.innerWidth], .05, .1);
xScale.domain(globOb.barData.map(function(d) {
return d.task
}));
//Set Y Scale
var yScale = d3.scale.linear()
.domain([0, d3.max(globOb.barData, function(d) {
return d.val + 1
})])
.range([globOb.bar.innerHeight, 0]);
//Grab SVG and set dimensions
var svg = d3.select("#viewport").append("svg")
.attr("width", globOb.bar.width)
.attr("height", globOb.bar.height);
//Set the drawing area of the SVG
var inner = svg.append("g")
.attr("transform", "translate(" + globOb.bar.margin.left + "," +
globOb.bar.margin.top + ")")
.attr("width", globOb.bar.innerWidth)
.attr("height", globOb.bar.innerHeight);
//Setup Axes
var xAxis = d3.svg.axis()
.scale(xScale)
.innerTickSize(0)
.outerTickSize(-globOb.bar.innerHeight)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(yScale)
.innerTickSize(-globOb.bar.innerWidth)
.outerTickSize(-globOb.bar.innerWidth)
.orient("left");
//Add X Axis
inner.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (globOb.bar.innerHeight) + ")")
.call(xAxis);
//Add Y axis
inner.append("g")
.attr("class", "axis")
.attr("transform", "translate(0,0)")
.call(yAxis);
//draw data
inner.selectAll("bar")
.data(globOb.barData)
.enter()
.append("rect")
.attr("class", function(d) {
return d.task.substring(0, d.task.length - 4) + "-bar"
})
.attr("x", function(d) {
return xScale(d.task)
})
.attr("width", 50) //xScale.rangeRoundBands())
.attr("y", function(d) {
return yScale(d.val)
})
.attr("height", function(d) {
return globOb.bar.innerHeight - yScale(d.val)
});
console.log(xScale.rangeRoundBands());
In the fiddle I have changed the call of rangeRoundBands() to a set number to show the chart otherwise draws fine. At the end I'm using console.log() to show the error with .rangeRoundBands().
ordinal.rangeRoundBands is a configuration setter, and you cannot call it without arguments, so it generates an error while trying to access the second item x[1] (indexed from zero) in the first argument x. You can take a look at the source.
You probably want to use ordinal.rangeBand() instead, which:
Returns the band width. When the scale’s range is configured with rangeBands or rangeRoundBands, the scale returns the lower value for the given input. The upper value can then be computed by offsetting by the band width. If the scale’s range is set using range or rangePoints, the band width is zero.

Categories

Resources